Skip to content

Commit

Permalink
enhance(@graphql-hive/yoga): improvements with Yoga's features
Browse files Browse the repository at this point in the history
  • Loading branch information
ardatan committed Jan 2, 2025
1 parent dc606ff commit fc18fe7
Show file tree
Hide file tree
Showing 22 changed files with 581 additions and 1,335 deletions.
8 changes: 8 additions & 0 deletions .changeset/hip-pumpkins-fold.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@graphql-hive/apollo': patch
'@graphql-hive/core': patch
'@graphql-hive/yoga': patch
---

Remove internal `_testing_` option to replace the underlying `fetch` implementation,
and add `fetch` option to do the same as part of the public API.
17 changes: 17 additions & 0 deletions .changeset/spotty-toes-help.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'@graphql-hive/yoga': patch
---

Align with GraphQL Yoga >=5.10.4 which has the relevant features mentioned below;

- Remove \`tiny-lru\` dependency, and use `createLruCache` from `graphql-yoga`
- - Yoga already provides a LRU cache implementation, so we can use that instead of having a separate dependency
- Use Explicit Resource Management of GraphQL Yoga plugins for disposal which already respect Node.js process termination
- - The feature has been implemented in `@whatwg-node/server` which is used by GraphQL Yoga. [Learn more about this feature](https://github.com/ardatan/whatwg-node/pull/1830)
- Use \`context.waitUntil\` which is handled by the environment automatically, if not `@whatwg-node/server` already takes care of it with above.
- - The feature has been implemented in `@whatwg-node/server` which is used by GraphQL Yoga. [Learn more about this feature](
https://github.com/ardatan/whatwg-node/pull/1830)
- Respect the given `fetchAPI` by GraphQL Yoga(might be Hive Gateway) for the `fetch` function
- - Hive Gateway uses `fetchAPI` given to GraphQL Yoga for the entire Fetch API implementation, so if Hive Client respects that, we have a control over HTTP calls done internally by the gateway
- Respect Yoga's \`logger\` for logging
- - Similar to above, we want to respect the logger provided by Yoga to have a better control over logging
8 changes: 2 additions & 6 deletions packages/libraries/apollo/tests/apollo.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,7 @@ test('should capture client name and version headers', async () => {
maxRetries: 0,
sendInterval: 10,
timeout: 50,
__testing: {
fetch: fetchSpy,
},
fetch: fetchSpy,
},
reporting: false,
usage: {
Expand Down Expand Up @@ -186,9 +184,7 @@ test('send usage reports in intervals', async () => {
maxRetries: 0,
sendInterval: 10,
timeout: 50,
__testing: {
fetch: fetchSpy,
},
fetch: fetchSpy,
},
reporting: false,
usage: {
Expand Down
2 changes: 1 addition & 1 deletion packages/libraries/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
},
"dependencies": {
"@graphql-tools/utils": "^10.0.0",
"@whatwg-node/fetch": "0.9.22",
"@whatwg-node/fetch": "0.10.1",
"async-retry": "1.3.3",
"lodash.sortby": "4.7.0",
"tiny-lru": "8.0.2"
Expand Down
11 changes: 5 additions & 6 deletions packages/libraries/core/src/client/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,10 @@ export interface AgentOptions {
*/
logger?: Logger;
/**
* Testing purposes only
* WHATWG Compatible fetch implementation
* used by the agent to send reports
*/
__testing?: {
fetch?: typeof fetch;
};
fetch?: typeof fetch;
}

export function createAgent<TEvent>(
Expand All @@ -67,7 +66,7 @@ export function createAgent<TEvent>(
headers?(): Record<string, string>;
},
) {
const options: Required<Omit<AgentOptions, '__testing'>> = {
const options: Required<Omit<AgentOptions, 'fetch'>> = {
timeout: 30_000,
debug: false,
enabled: true,
Expand Down Expand Up @@ -174,7 +173,7 @@ export function createAgent<TEvent>(
factor: 2,
},
logger: options.logger,
fetchImplementation: pluginOptions.__testing?.fetch,
fetchImplementation: pluginOptions.fetch,
})
.then(res => {
debugLog(`Report sent!`);
Expand Down
184 changes: 92 additions & 92 deletions packages/libraries/core/src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,19 +54,19 @@ export function createHive(options: HivePluginOptions): HiveClient {

const info = printTokenInfo
? async () => {
try {
let endpoint = 'https://app.graphql-hive.com/graphql';
try {
let endpoint = 'https://app.graphql-hive.com/graphql';

// Look for the reporting.endpoint for the legacy reason.
if (options.reporting && options.reporting.endpoint) {
endpoint = options.reporting.endpoint;
}
// Look for the reporting.endpoint for the legacy reason.
if (options.reporting && options.reporting.endpoint) {
endpoint = options.reporting.endpoint;
}

if (options.selfHosting?.graphqlEndpoint) {
endpoint = options.selfHosting.graphqlEndpoint;
}
if (options.selfHosting?.graphqlEndpoint) {
endpoint = options.selfHosting.graphqlEndpoint;
}

const query = /* GraphQL */ `
const query = /* GraphQL */ `
query myTokenInfo {
tokenInfo {
__typename
Expand Down Expand Up @@ -95,90 +95,90 @@ export function createHive(options: HivePluginOptions): HiveClient {
}
`;

infoLogger.info('Fetching token details...');

const response = await http.post(
endpoint,
JSON.stringify({
query,
operationName: 'myTokenInfo',
}),
{
headers: {
'content-type': 'application/json',
Authorization: `Bearer ${options.token}`,
'user-agent': `hive-client/${version}`,
'graphql-client-name': 'Hive Client',
'graphql-client-version': version,
},
timeout: 30_000,
fetchImplementation: options?.agent?.__testing?.fetch,
logger: infoLogger,
infoLogger.info('Fetching token details...');

const response = await http.post(
endpoint,
JSON.stringify({
query,
operationName: 'myTokenInfo',
}),
{
headers: {
'content-type': 'application/json',
Authorization: `Bearer ${options.token}`,
'user-agent': `hive-client/${version}`,
'graphql-client-name': 'Hive Client',
'graphql-client-version': version,
},
);

if (response.ok) {
const result: ExecutionResult<any> = await response.json();

if (result.data?.tokenInfo.__typename === 'TokenInfo') {
const { tokenInfo } = result.data;

const {
organization,
project,
target,
canReportSchema,
canCollectUsage,
canReadOperations,
} = tokenInfo;
const print = createPrinter([
tokenInfo.token.name,
organization.name,
project.name,
target.name,
]);

const appUrl =
options.selfHosting?.applicationUrl?.replace(/\/$/, '') ??
'https://app.graphql-hive.com';
const organizationUrl = `${appUrl}/${organization.slug}`;
const projectUrl = `${organizationUrl}/${project.slug}`;
const targetUrl = `${projectUrl}/${target.slug}`;

infoLogger.info(
[
'Token details',
'',
`Token name: ${print(tokenInfo.token.name)}`,
`Organization: ${print(organization.name, organizationUrl)}`,
`Project: ${print(project.name, projectUrl)}`,
`Target: ${print(target.name, targetUrl)}`,
'',
`Can report schema? ${print(canReportSchema ? 'Yes' : 'No')}`,
`Can collect usage? ${print(canCollectUsage ? 'Yes' : 'No')}`,
`Can read operations? ${print(canReadOperations ? 'Yes' : 'No')}`,
'',
].join('\n'),
);
} else if (result.data?.tokenInfo.message) {
infoLogger.error(`Token not found. Reason: ${result.data?.tokenInfo.message}`);
infoLogger.info(
`How to create a token? https://docs.graphql-hive.com/features/tokens`,
);
} else {
infoLogger.error(`${result.errors![0].message}`);
infoLogger.info(
`How to create a token? https://docs.graphql-hive.com/features/tokens`,
);
}
timeout: 30_000,
fetchImplementation: options?.agent?.fetch,
logger: infoLogger,
},
);

if (response.ok) {
const result: ExecutionResult<any> = await response.json();

if (result.data?.tokenInfo.__typename === 'TokenInfo') {
const { tokenInfo } = result.data;

const {
organization,
project,
target,
canReportSchema,
canCollectUsage,
canReadOperations,
} = tokenInfo;
const print = createPrinter([
tokenInfo.token.name,
organization.name,
project.name,
target.name,
]);

const appUrl =
options.selfHosting?.applicationUrl?.replace(/\/$/, '') ??
'https://app.graphql-hive.com';
const organizationUrl = `${appUrl}/${organization.slug}`;
const projectUrl = `${organizationUrl}/${project.slug}`;
const targetUrl = `${projectUrl}/${target.slug}`;

infoLogger.info(
[
'Token details',
'',
`Token name: ${print(tokenInfo.token.name)}`,
`Organization: ${print(organization.name, organizationUrl)}`,
`Project: ${print(project.name, projectUrl)}`,
`Target: ${print(target.name, targetUrl)}`,
'',
`Can report schema? ${print(canReportSchema ? 'Yes' : 'No')}`,
`Can collect usage? ${print(canCollectUsage ? 'Yes' : 'No')}`,
`Can read operations? ${print(canReadOperations ? 'Yes' : 'No')}`,
'',
].join('\n'),
);
} else if (result.data?.tokenInfo.message) {
infoLogger.error(`Token not found. Reason: ${result.data?.tokenInfo.message}`);
infoLogger.info(
`How to create a token? https://docs.graphql-hive.com/features/tokens`,
);
} else {
infoLogger.error(`Error ${response.status}: ${response.statusText}`);
infoLogger.error(`${result.errors![0].message}`);
infoLogger.info(
`How to create a token? https://docs.graphql-hive.com/features/tokens`,
);
}
} catch (error) {
infoLogger.error(`Error ${(error as Error)?.message ?? error}`);
} else {
infoLogger.error(`Error ${response.status}: ${response.statusText}`);
}
} catch (error) {
infoLogger.error(`Error ${(error as Error)?.message ?? error}`);
}
: () => {};
}
: () => { };

function createInstrumentedExecute(
executeImpl: typeof ExecuteImplementation,
Expand Down Expand Up @@ -218,9 +218,9 @@ export function createHive(options: HivePluginOptions): HiveClient {
createInstrumentedExecute,
experimental__persistedDocuments: options.experimental__persistedDocuments
? createPersistedDocuments({
...options.experimental__persistedDocuments,
logger,
})
...options.experimental__persistedDocuments,
logger,
})
: null,
};
}
Expand Down
4 changes: 2 additions & 2 deletions packages/libraries/core/src/client/persisted-documents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export function createPersistedDocuments(
logger: Logger;
},
): null | {
resolve(documentId: string): Promise<string | null>;
resolve(documentId: string): PromiseOrValue<string | null>;
allowArbitraryDocuments(context: { headers?: HeadersObject }): PromiseOrValue<boolean>;
} {
const persistedDocumentsCache = LRU<string>(config.cache ?? 10_000);
Expand All @@ -32,7 +32,7 @@ export function createPersistedDocuments(
const fetchCache = new Map<string, Promise<string | null>>();

/** Batch load a persisted documents */
async function loadPersistedDocument(documentId: string) {
function loadPersistedDocument(documentId: string) {
const document = persistedDocumentsCache.get(documentId);
if (document) {
return document;
Expand Down
2 changes: 1 addition & 1 deletion packages/libraries/core/src/client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export interface HiveClient {
createInstrumentedSubscribe(executeImpl: any): any;
dispose(): Promise<void>;
experimental__persistedDocuments: null | {
resolve(documentId: string): Promise<string | null>;
resolve(documentId: string): PromiseOrValue<string | null>;
allowArbitraryDocuments(context: { headers?: HeadersObject }): PromiseOrValue<boolean>;
};
}
Expand Down
Loading

0 comments on commit fc18fe7

Please sign in to comment.