Last active
February 25, 2025 05:47
-
-
Save mvantellingen/9bfeb69df502911b5b508e5982916b18 to your computer and use it in GitHub Desktop.
Hive `createSupergraphManager`
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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