Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 61 additions & 11 deletions packages/libraries/apollo/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import { GraphQLError, type DocumentNode } from 'graphql';
import type { ApolloServerPlugin, HTTPGraphQLRequest } from '@apollo/server';
import {
autoDisposeSymbol,
createCDNArtifactFetcher,
createHive as createHiveClient,
createSupergraphSDLFetcher,
HiveClient,
HivePluginOptions,
isHiveClient,
SupergraphSDLFetcherOptions,
joinUrl,
Logger,
type CDNArtifactFetcherCircuitBreakerConfiguration,
} from '@graphql-hive/core';
import { version } from './version.js';

Expand All @@ -17,14 +19,62 @@ export {
createServicesFetcher,
createSupergraphSDLFetcher,
} from '@graphql-hive/core';

/** @deprecated Use {CreateSupergraphManagerArgs} instead */
export type { SupergraphSDLFetcherOptions } from '@graphql-hive/core';

export function createSupergraphManager({
pollIntervalInMs,
...superGraphFetcherOptions
}: { pollIntervalInMs?: number } & SupergraphSDLFetcherOptions) {
pollIntervalInMs = pollIntervalInMs ?? 30_000;
const fetchSupergraph = createSupergraphSDLFetcher(superGraphFetcherOptions);
/**
* Configuration for {createSupergraphManager}.
*/
export type CreateSupergraphManagerArgs = {
/**
* The artifact endpoint to poll.
* E.g. `https://cdn.graphql-hive.com/<uuid>/supergraph`
*/
endpoint: string;
/**
* The CDN access key for fetching artifact.
*/
key: string;
logger?: Logger;
/**
* The supergraph poll interval in milliseconds
* Default: 30_000
*/
pollIntervalInMs?: number;
/** Circuit breaker configuration override. */
circuitBreaker?: CDNArtifactFetcherCircuitBreakerConfiguration;
fetchImplementation?: typeof fetch;
/**
* Client name override
* Default: `@graphql-hive/apollo`
*/
name?: string;
/**
* Client version override
* Default: currents package version
*/
version?: string;
};

export function createSupergraphManager(args: CreateSupergraphManagerArgs) {
const pollIntervalInMs = args.pollIntervalInMs ?? 30_000;
const endpoint = args.endpoint.endsWith('/supergraph')
? args.endpoint
: joinUrl(args.endpoint, 'supergraph');

const fetchSupergraph = createCDNArtifactFetcher({
endpoint,
accessKey: args.key,
client: {
name: args.name ?? '@graphql-hive/apollo',
version: args.version ?? version,
},
logger: args.logger,
fetch: args.fetchImplementation,
circuitBreaker: args.circuitBreaker,
});

let timer: ReturnType<typeof setTimeout> | null = null;

return {
Expand All @@ -38,8 +88,8 @@ export function createSupergraphManager({
timer = setTimeout(async () => {
try {
const result = await fetchSupergraph();
if (result.supergraphSdl) {
hooks.update?.(result.supergraphSdl);
if (result.contents) {
hooks.update?.(result.contents);
}
} catch (error) {
console.error(
Expand All @@ -53,7 +103,7 @@ export function createSupergraphManager({
poll();

return {
supergraphSdl: initialResult.supergraphSdl,
supergraphSdl: initialResult.contents,
cleanup: async () => {
if (timer) {
clearTimeout(timer);
Expand Down
32 changes: 6 additions & 26 deletions packages/libraries/core/src/client/agent.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,15 @@
import CircuitBreaker from '../circuit-breaker/circuit.js';
import { version } from '../version.js';
import {
CircuitBreakerConfiguration,
defaultCircuitBreakerConfiguration,
} from './circuit-breaker.js';
import { http } from './http-client.js';
import type { Logger } from './types.js';
import { createHiveLogger } from './utils.js';

type ReadOnlyResponse = Pick<Response, 'status' | 'text' | 'json' | 'statusText'>;

export type AgentCircuitBreakerConfiguration = {
/**
* Percentage after what the circuit breaker should kick in.
* Default: 50
*/
errorThresholdPercentage: number;
/**
* Count of requests before starting evaluating.
* Default: 5
*/
volumeThreshold: number;
/**
* After what time the circuit breaker is attempting to retry sending requests in milliseconds
* Default: 30_000
*/
resetTimeout: number;
};

const defaultCircuitBreakerConfiguration: AgentCircuitBreakerConfiguration = {
errorThresholdPercentage: 50,
volumeThreshold: 10,
resetTimeout: 30_000,
};

export interface AgentOptions {
enabled?: boolean;
name?: string;
Expand Down Expand Up @@ -76,7 +56,7 @@ export interface AgentOptions {
* false -> Disable
* object -> use custom configuration see {AgentCircuitBreakerConfiguration}
*/
circuitBreaker?: boolean | AgentCircuitBreakerConfiguration;
circuitBreaker?: boolean | CircuitBreakerConfiguration;
/**
* WHATWG Compatible fetch implementation
* used by the agent to send reports
Expand All @@ -101,7 +81,7 @@ export function createAgent<TEvent>(
},
) {
const options: Required<Omit<AgentOptions, 'fetch' | 'debug' | 'logger' | 'circuitBreaker'>> & {
circuitBreaker: AgentCircuitBreakerConfiguration | null;
circuitBreaker: CircuitBreakerConfiguration | null;
} = {
timeout: 30_000,
enabled: true,
Expand Down
124 changes: 124 additions & 0 deletions packages/libraries/core/src/client/artifacts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import CircuitBreaker from '../circuit-breaker/circuit.js';
import { version } from '../version.js';
import {
CircuitBreakerConfiguration,
defaultCircuitBreakerConfiguration,
} from './circuit-breaker.js';
import { http } from './http-client.js';
import type { Logger } from './types.js';
import { createHash, createHiveLogger } from './utils.js';

type CreateCDNArtifactFetcherArgs = {
endpoint: string;
accessKey: string;
/** client meta data */
client?: {
name: string;
version: string;
};
circuitBreaker?: CircuitBreakerConfiguration;
logger?: Logger;
fetch?: typeof fetch;
};

type CDNFetcherArgs = {
logger?: Logger;
fetch?: typeof fetch;
};

type CDNFetchResult = {
/** Text contents of the artifact */
contents: string;
/** SHA-256 Hash */
hash: string;
/** Schema Version ID as on Hive Console (optional) */
schemaVersionId: null | string;
};

function isRequestOk(response: Response) {
return response.status === 304 || response.ok;
}

/**
* Create a handler for fetching a CDN artifact with built-in cache and circuit breaker.
* It is intended for polling supergraph, schema sdl or services.
*/
export function createCDNArtifactFetcher(args: CreateCDNArtifactFetcherArgs) {
let cacheETag: string | null = null;
let cached: CDNFetchResult | null = null;
const clientInfo = args.client ?? { name: 'hive-client', version };
const circuitBreakerConfig = args.circuitBreaker ?? defaultCircuitBreakerConfiguration;

const circuitBreaker = new CircuitBreaker(
function runFetch(fetchArgs?: CDNFetcherArgs) {
const signal = circuitBreaker.getSignal();
const logger = createHiveLogger(fetchArgs?.logger ?? args.logger ?? console, '');
const fetchImplementation = fetchArgs?.fetch ?? args.fetch;

const headers: {
[key: string]: string;
} = {
'X-Hive-CDN-Key': args.accessKey,
'User-Agent': `${clientInfo.name}/${clientInfo.version}`,
};

if (cacheETag) {
headers['If-None-Match'] = cacheETag;
}

return http.get(args.endpoint, {
headers,
isRequestOk,
retry: {
retries: 10,
maxTimeout: 200,
minTimeout: 1,
},
logger,
fetchImplementation,
signal,
});
},
{
...circuitBreakerConfig,
timeout: false,
autoRenewAbortController: true,
},
);

return async function fetchArtifact(fetchArgs?: CDNFetcherArgs): Promise<CDNFetchResult> {
try {
const response = await circuitBreaker.fire(fetchArgs);

if (response.status === 304) {
if (cached !== null) {
return cached;
}
throw new Error('This should never happen.');
}

const contents = await response.text();
const result: CDNFetchResult = {
hash: await createHash('SHA-256').update(contents).digest('base64'),
contents,
schemaVersionId: response.headers.get('x-hive-schema-version-id') || null,
};

const etag = response.headers.get('etag');
if (etag) {
cached = result;
cacheETag = etag;
}

return result;
} catch (err) {
if (err instanceof Error && 'code' in err && err.code === 'EOPENBREAKER') {
if (cached) {
return cached;
}
}

throw err;
}
};
}
23 changes: 23 additions & 0 deletions packages/libraries/core/src/client/circuit-breaker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export type CircuitBreakerConfiguration = {
/**
* Percentage after what the circuit breaker should kick in.
* Default: 50
*/
errorThresholdPercentage: number;
/**
* Count of requests before starting evaluating.
* Default: 5
*/
volumeThreshold: number;
/**
* After what time the circuit breaker is attempting to retry sending requests in milliseconds
* Default: 30_000
*/
resetTimeout: number;
};

export const defaultCircuitBreakerConfiguration: CircuitBreakerConfiguration = {
errorThresholdPercentage: 50,
volumeThreshold: 10,
resetTimeout: 30_000,
};
Loading
Loading