Skip to content

Instantly share code, notes, and snippets.

@mvantellingen
Last active February 25, 2025 05:47
Show Gist options
  • Save mvantellingen/9bfeb69df502911b5b508e5982916b18 to your computer and use it in GitHub Desktop.
Save mvantellingen/9bfeb69df502911b5b508e5982916b18 to your computer and use it in GitHub Desktop.
Hive `createSupergraphManager`
/**
* This file re-implements hive's createSupergraphManager to return a fetcher
* which uses node's native fetch api. This is required due to errors when
* the fetch library used by hive is instrumented by the observability package.
*
* See https://github.com/graphql-hive/console/issues/6359#issuecomment-2679100600
*/
import type { SupergraphSDLFetcherOptions } from "@graphql-hive/apollo";
import { createHash } from "@graphql-hive/core";
import fetchRetry from "fetch-retry";
type SupergraphManagerResult = {
initialize(hooks: { update(supergraphSdl: string): void }): Promise<{
supergraphSdl: string;
cleanup?: () => Promise<void>;
}>;
};
export function createSupergraphManager(
options: { pollIntervalInMs?: number } & SupergraphSDLFetcherOptions,
): SupergraphManagerResult {
const pollIntervalInMs = options.pollIntervalInMs ?? 30_000;
const fetchSupergraph = createSupergraphSDLFetcher({
endpoint: options.endpoint,
key: options.key,
logger: options.logger,
});
let timer: ReturnType<typeof setTimeout> | null = null;
return {
async initialize(hooks: { update(supergraphSdl: string): void }): Promise<{
supergraphSdl: string;
cleanup?: () => Promise<void>;
}> {
const initialResult = await fetchSupergraph();
function poll(): void {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
timer = setTimeout(async () => {
try {
const result = await fetchSupergraph();
if (result.supergraphSdl) {
hooks.update?.(result.supergraphSdl);
}
} catch (err) {
options.logger.error({
msg: "Failed to update supergraph",
err,
});
}
poll();
}, pollIntervalInMs);
}
poll();
return {
supergraphSdl: initialResult.supergraphSdl,
cleanup: async (): Promise<void> => {
if (timer) {
clearTimeout(timer);
}
},
};
},
};
}
export function createSupergraphSDLFetcher(
options: SupergraphSDLFetcherOptions,
) {
let cacheETag: string | null = null;
let cached: {
id: string;
supergraphSdl: string;
} | null = null;
const endpoint = options.endpoint.endsWith("/supergraph")
? options.endpoint
: joinUrl(options.endpoint, "supergraph");
// Wrap node-fetch with fetch-retry.
const fetchWithRetry = fetchRetry(fetch);
return function supergraphSDLFetcher(): Promise<{
id: string;
supergraphSdl: string;
}> {
const headers: {
[key: string]: string;
} = {
"X-Hive-CDN-Key": options.key,
"User-Agent": "hive-client/fix-for-issue-6359",
};
if (cacheETag) {
headers["If-None-Match"] = cacheETag;
}
return fetchWithRetry(endpoint, {
headers,
retries: 10,
signal: AbortSignal.timeout(20_000),
// Use a random delay between 1 and 200ms for each retry.
retryDelay: () => Math.floor(Math.random() * 200) + 1,
// Custom logic to decide whether to retry.
retryOn: (attempt, error, response) => {
if (error) {
if (logger) {
logger.warn(
`Attempt ${attempt} encountered error: ${error.message}. Retrying...`,
);
}
return true;
}
if (response) {
// Treat a 304 (Not Modified) or a successful response as acceptable.
if (response.status === 304 || response.ok) {
return false;
}
if (logger) {
logger.warn(
`Attempt ${attempt} received status: ${response.status}. Retrying...`,
);
}
return true;
}
return false;
},
}).then(async (response) => {
// If the response status is 304 and we have a cached result, return it.
if (response.status === 304 && cached !== null) {
return cached;
}
// Process a successful response.
if (response.ok) {
const supergraphSdl = await response.text();
const result = {
id: await createHash("SHA-256")
.update(supergraphSdl)
.digest("base64"),
supergraphSdl,
};
const etag = response.headers.get("etag");
if (etag) {
cached = result;
cacheETag = etag;
}
return result;
}
// Throw an error if the response is not acceptable.
throw new Error(
`Failed to GET ${endpoint}, received: ${response.status} ${response.statusText || "Internal Server Error"}`,
);
});
};
}
function joinUrl(url: string, subdirectory: string): string {
const normalizedUrl = url.endsWith("/") ? url.slice(0, -1) : url;
const normalizedSubdirectory = subdirectory.startsWith("/")
? subdirectory.slice(1)
: subdirectory;
return normalizedUrl + "/" + normalizedSubdirectory;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment