Skip to content

Commit 44fdaf5

Browse files
committed
support multiple endpoints
1 parent 9d5b12a commit 44fdaf5

File tree

2 files changed

+86
-50
lines changed

2 files changed

+86
-50
lines changed

packages/libraries/core/src/client/artifacts.ts

Lines changed: 84 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,39 @@ import type { Logger } from './types.js';
99
import { createHash, createHiveLogger } from './utils.js';
1010

1111
type CreateCDNArtifactFetcherArgs = {
12-
endpoint: string;
12+
/**
13+
* The endpoint that should be fetched.
14+
*
15+
* It is possible to provide an endpoint list. The first endpoint will be treated as the primary source.
16+
* The secondary endpoint will be used in case the first endpoint fails to respond.
17+
*
18+
* Example:
19+
*
20+
* ```
21+
* [
22+
* "https://cdn.graphql-hive.com/artifacts/v1/9fb37bc4-e520-4019-843a-0c8698c25688/supergraph",
23+
* "https://cdn-mirror.graphql-hive.com/artifacts/v1/9fb37bc4-e520-4019-843a-0c8698c25688/supergraph"
24+
* ]
25+
* ```
26+
*/
27+
endpoint: string | [string, string];
28+
/**
29+
* The access key that is used for authenticating on the endpoints (via the `X-Hive-CDN-Key` header).
30+
*/
1331
accessKey: string;
14-
/** client meta data */
32+
logger?: Logger;
33+
circuitBreaker?: CircuitBreakerConfiguration;
34+
/**
35+
* Custom fetch implementation used for calling the endpoint.
36+
*/
37+
fetch?: typeof fetch;
38+
/**
39+
* Optional client meta configuration.
40+
**/
1541
client?: {
1642
name: string;
1743
version: string;
1844
};
19-
circuitBreaker?: CircuitBreakerConfiguration;
20-
logger?: Logger;
21-
fetch?: typeof fetch;
22-
};
23-
24-
type CDNFetcherArgs = {
25-
logger?: Logger;
26-
fetch?: typeof fetch;
2745
};
2846

2947
type CDNFetchResult = {
@@ -48,47 +66,67 @@ export function createCDNArtifactFetcher(args: CreateCDNArtifactFetcherArgs) {
4866
let cached: CDNFetchResult | null = null;
4967
const clientInfo = args.client ?? { name: 'hive-client', version };
5068
const circuitBreakerConfig = args.circuitBreaker ?? defaultCircuitBreakerConfiguration;
69+
const logger = createHiveLogger(args.logger ?? console, '');
5170

52-
const circuitBreaker = new CircuitBreaker(
53-
function runFetch(fetchArgs?: CDNFetcherArgs) {
54-
const signal = circuitBreaker.getSignal();
55-
const logger = createHiveLogger(fetchArgs?.logger ?? args.logger ?? console, '');
56-
const fetchImplementation = fetchArgs?.fetch ?? args.fetch;
57-
58-
const headers: {
59-
[key: string]: string;
60-
} = {
61-
'X-Hive-CDN-Key': args.accessKey,
62-
'User-Agent': `${clientInfo.name}/${clientInfo.version}`,
63-
};
71+
const endpoints = Array.isArray(args.endpoint) ? args.endpoint : [args.endpoint];
72+
73+
// TODO: we should probably do some endpoint validation
74+
// And print some errors if the enpoint paths do not match?
75+
// e.g. the only difference should be the domain name
76+
77+
const circuitBreakers = endpoints.map(endpoint => {
78+
const circuitBreaker = new CircuitBreaker(
79+
function runFetch() {
80+
const signal = circuitBreaker.getSignal();
81+
82+
const headers: {
83+
[key: string]: string;
84+
} = {
85+
'X-Hive-CDN-Key': args.accessKey,
86+
'User-Agent': `${clientInfo.name}/${clientInfo.version}`,
87+
};
88+
89+
if (cacheETag) {
90+
headers['If-None-Match'] = cacheETag;
91+
}
92+
93+
return http.get(endpoint, {
94+
headers,
95+
isRequestOk,
96+
retry: {
97+
retries: 10,
98+
maxTimeout: 200,
99+
minTimeout: 1,
100+
},
101+
logger,
102+
fetchImplementation: args.fetch,
103+
signal,
104+
});
105+
},
106+
{
107+
...circuitBreakerConfig,
108+
timeout: false,
109+
autoRenewAbortController: true,
110+
},
111+
);
112+
return circuitBreaker;
113+
});
114+
115+
return async function fetchArtifact(): Promise<CDNFetchResult> {
116+
// TODO: we can probably do that better...
117+
// If an items is half open, we would probably want to try both the opened and half opened one
118+
const fetcher = circuitBreakers.find(item => item.opened || item.halfOpen);
64119

65-
if (cacheETag) {
66-
headers['If-None-Match'] = cacheETag;
120+
if (!fetcher) {
121+
if (cached !== null) {
122+
return cached;
67123
}
68124

69-
return http.get(args.endpoint, {
70-
headers,
71-
isRequestOk,
72-
retry: {
73-
retries: 10,
74-
maxTimeout: 200,
75-
minTimeout: 1,
76-
},
77-
logger,
78-
fetchImplementation,
79-
signal,
80-
});
81-
},
82-
{
83-
...circuitBreakerConfig,
84-
timeout: false,
85-
autoRenewAbortController: true,
86-
},
87-
);
88-
89-
return async function fetchArtifact(fetchArgs?: CDNFetcherArgs): Promise<CDNFetchResult> {
125+
throw new Error('Failed to retrieve artifact.');
126+
}
127+
90128
try {
91-
const response = await circuitBreaker.fire(fetchArgs);
129+
const response = await fetcher.fire();
92130

93131
if (response.status === 304) {
94132
if (cached !== null) {

packages/libraries/core/src/index.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,5 @@ export { isHiveClient, isAsyncIterable, createHash, joinUrl } from './client/uti
1313
export { http, URL } from './client/http-client.js';
1414
export { createSupergraphSDLFetcher } from './client/supergraph.js';
1515
export type { SupergraphSDLFetcherOptions } from './client/supergraph.js';
16-
export {
17-
createCDNArtifactFetcher,
18-
type CDNArtifactFetcherCircuitBreakerConfiguration,
19-
} from './client/artifacts.js';
16+
export { createCDNArtifactFetcher } from './client/artifacts.js';
17+
export type { CircuitBreakerConfiguration } from './client/circuit-breaker.js';

0 commit comments

Comments
 (0)