Skip to content

Commit

Permalink
Optional
Browse files Browse the repository at this point in the history
  • Loading branch information
ardatan committed Jul 5, 2024
1 parent 8c2d542 commit 07b2779
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 13 deletions.
2 changes: 1 addition & 1 deletion .changeset/yellow-weeks-refuse.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
"@graphql-tools/utils": patch
---

Make the executor disposable lazily
Make the executor disposable optional
78 changes: 68 additions & 10 deletions packages/executors/http/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { DocumentNode, GraphQLResolveInfo } from 'graphql';
import { ValueOrPromise } from 'value-or-promise';
import {
AsyncExecutor,
createGraphQLError,
DisposableAsyncExecutor,
DisposableExecutor,
Expand All @@ -9,6 +10,7 @@ import {
ExecutionResult,
Executor,
getOperationASTFromRequest,
SyncExecutor,
} from '@graphql-tools/utils';
import { fetch as defaultFetch } from '@whatwg-node/fetch';
import { createFormDataFromVariables } from './createFormDataFromVariables.js';
Expand Down Expand Up @@ -85,29 +87,84 @@ export interface HTTPExecutorOptions {
* Print function for DocumentNode
*/
print?: (doc: DocumentNode) => string;
/**
* Enable [Explicit Resource Management](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html#using-declarations-and-explicit-resource-management)
* @default false
*/
disposable?: boolean;
}

export type HeadersConfig = Record<string, string>;

export function buildHTTPExecutor(
options?: Omit<HTTPExecutorOptions, 'fetch'> & { fetch: SyncFetchFn },
options?: Omit<HTTPExecutorOptions, 'fetch' | 'disposable'> & {
fetch: SyncFetchFn;
disposable: true;
},
): DisposableSyncExecutor<any, HTTPExecutorOptions>;

export function buildHTTPExecutor(
options?: Omit<HTTPExecutorOptions, 'fetch' | 'disposable'> & {
fetch: SyncFetchFn;
disposable: false;
},
): SyncExecutor<any, HTTPExecutorOptions>;

export function buildHTTPExecutor(
options?: Omit<HTTPExecutorOptions, 'fetch'> & { fetch: SyncFetchFn },
): SyncExecutor<any, HTTPExecutorOptions>;

export function buildHTTPExecutor(
options?: Omit<HTTPExecutorOptions, 'fetch' | 'disposable'> & {
fetch: AsyncFetchFn;
disposable: true;
},
): DisposableAsyncExecutor<any, HTTPExecutorOptions>;

export function buildHTTPExecutor(
options?: Omit<HTTPExecutorOptions, 'fetch' | 'disposable'> & {
fetch: AsyncFetchFn;
disposable: false;
},
): AsyncExecutor<any, HTTPExecutorOptions>;

export function buildHTTPExecutor(
options?: Omit<HTTPExecutorOptions, 'fetch'> & { fetch: AsyncFetchFn },
): AsyncExecutor<any, HTTPExecutorOptions>;

export function buildHTTPExecutor(
options?: Omit<HTTPExecutorOptions, 'fetch' | 'disposable'> & {
fetch: RegularFetchFn;
disposable: true;
},
): DisposableAsyncExecutor<any, HTTPExecutorOptions>;

export function buildHTTPExecutor(
options?: Omit<HTTPExecutorOptions, 'fetch' | 'disposable'> & {
fetch: RegularFetchFn;
disposable: false;
},
): AsyncExecutor<any, HTTPExecutorOptions>;

export function buildHTTPExecutor(
options?: Omit<HTTPExecutorOptions, 'fetch'> & { fetch: RegularFetchFn },
): AsyncExecutor<any, HTTPExecutorOptions>;

export function buildHTTPExecutor(
options?: Omit<HTTPExecutorOptions, 'fetch' | 'disposable'> & { disposable: true },
): DisposableAsyncExecutor<any, HTTPExecutorOptions>;

export function buildHTTPExecutor(
options?: Omit<HTTPExecutorOptions, 'fetch' | 'disposable'> & { disposable: false },
): AsyncExecutor<any, HTTPExecutorOptions>;

export function buildHTTPExecutor(
options?: Omit<HTTPExecutorOptions, 'fetch'>,
): DisposableAsyncExecutor<any, HTTPExecutorOptions>;
): AsyncExecutor<any, HTTPExecutorOptions>;

export function buildHTTPExecutor(
options?: HTTPExecutorOptions,
): DisposableExecutor<any, HTTPExecutorOptions> {
): DisposableExecutor<any, HTTPExecutorOptions> | Executor<any, HTTPExecutorOptions> {
const printFn = options?.print ?? defaultPrintFn;
let disposeCtrl: AbortController | undefined;
const baseExecutor = (request: ExecutionRequest<any, any, any, HTTPExecutorOptions>) => {
Expand Down Expand Up @@ -428,30 +485,31 @@ export function buildHTTPExecutor(
};
}

if (!options?.disposable) {
disposeCtrl = undefined;
return executor;
}

disposeCtrl = new AbortController();

Object.defineProperties(executor, {
[Symbol.dispose]: {
get() {
if (!disposeCtrl) {
disposeCtrl = new AbortController();
}
return function dispose() {
return disposeCtrl!.abort(createAbortErrorReason());
};
},
},
[Symbol.asyncDispose]: {
get() {
if (!disposeCtrl) {
disposeCtrl = new AbortController();
}
return function asyncDispose() {
return disposeCtrl!.abort(createAbortErrorReason());
};
},
},
});

return executor as DisposableExecutor;
return executor;
}

function createAbortErrorReason() {
Expand Down
5 changes: 3 additions & 2 deletions packages/executors/http/tests/buildHTTPExecutor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,16 +223,16 @@ describe('buildHTTPExecutor', () => {
await new Promise<void>(resolve => server.listen(0, resolve));
const executor = buildHTTPExecutor({
endpoint: `http://localhost:${(server.address() as any).port}`,
disposable: true,
});
const disposeFn = executor[Symbol.asyncDispose];
const result = executor({
document: parse(/* GraphQL */ `
query {
hello
}
`),
});
await disposeFn();
await executor[Symbol.asyncDispose]();
await expect(result).resolves.toEqual({
errors: [
createGraphQLError('The operation was aborted. reason: Error: Executor was disposed.'),
Expand All @@ -242,6 +242,7 @@ describe('buildHTTPExecutor', () => {
it('does not allow new requests when the executor is disposed', async () => {
const executor = buildHTTPExecutor({
fetch: () => Response.json({ data: { hello: 'world' } }),
disposable: true,
});
executor[Symbol.dispose]?.();
const result = await executor({
Expand Down

0 comments on commit 07b2779

Please sign in to comment.