From 70f780ec44f9e9f61c9eb8fe88e76122e8da5fe8 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sun, 27 Oct 2024 21:14:48 +0200 Subject: [PATCH 1/3] feat: pass abortSignal to resolvers this allows e.g. passing the signal to fetch --- src/execution/execute.ts | 7 ++++--- src/type/definition.ts | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 855cfcfed4..17a8704bb2 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -798,7 +798,7 @@ function executeField( deferMap: ReadonlyMap | undefined, ): PromiseOrValue> | undefined { const validatedExecutionArgs = exeContext.validatedExecutionArgs; - const { schema, contextValue, variableValues, hideSuggestions } = + const { schema, contextValue, variableValues, hideSuggestions, abortSignal } = validatedExecutionArgs; const fieldName = fieldDetailsList[0].node.name.value; const fieldDef = schema.getField(parentType, fieldName); @@ -833,7 +833,7 @@ function executeField( // The resolve function's optional third argument is a context value that // is provided to every resolve function within an execution. It is commonly // used to represent an authenticated user, or request-specific caches. - const result = resolveFn(source, args, contextValue, info); + const result = resolveFn(source, args, contextValue, info, abortSignal); if (isPromise(result)) { return completePromisedValue( @@ -2115,6 +2115,7 @@ function executeSubscription( operation, variableValues, hideSuggestions, + abortSignal, } = validatedExecutionArgs; const rootType = schema.getSubscriptionType(); @@ -2180,7 +2181,7 @@ function executeSubscription( // The resolve function's optional third argument is a context value that // is provided to every resolve function within an execution. It is commonly // used to represent an authenticated user, or request-specific caches. - const result = resolveFn(rootValue, args, contextValue, info); + const result = resolveFn(rootValue, args, contextValue, info, abortSignal); if (isPromise(result)) { return result diff --git a/src/type/definition.ts b/src/type/definition.ts index 71db2e66a6..8d51201070 100644 --- a/src/type/definition.ts +++ b/src/type/definition.ts @@ -990,6 +990,7 @@ export type GraphQLFieldResolver< args: TArgs, context: TContext, info: GraphQLResolveInfo, + abortSignal: AbortSignal | undefined, ) => TResult; export interface GraphQLResolveInfo { From c17544e3a831c4e2f352576c3430a4fa7a1dedcc Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Mon, 28 Oct 2024 13:07:16 +0200 Subject: [PATCH 2/3] update defaultFieldResolver and add test --- src/execution/__tests__/abort-signal-test.ts | 47 ++++++++++++++++++++ src/execution/execute.ts | 4 +- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/execution/__tests__/abort-signal-test.ts b/src/execution/__tests__/abort-signal-test.ts index 6675b5c54d..617031c084 100644 --- a/src/execution/__tests__/abort-signal-test.ts +++ b/src/execution/__tests__/abort-signal-test.ts @@ -109,6 +109,53 @@ describe('Execute: Cancellation', () => { }); }); + it('should provide access to the abort signal within resolvers', async () => { + const throwIfAborted = async (abortSignal: AbortSignal) => { + await resolveOnNextTick(); + abortSignal.throwIfAborted(); + }; + + const abortController = new AbortController(); + const document = parse(` + query { + todo { + id + } + } + `); + + const resultPromise = execute({ + document, + schema, + abortSignal: abortController.signal, + rootValue: { + todo: { + id: (_args: any, _context: any, _info: any, signal: AbortSignal) => + throwIfAborted(signal), + }, + }, + }); + + abortController.abort(); + + const result = await resultPromise; + + expectJSON(result).toDeepEqual({ + data: { + todo: { + id: null, + }, + }, + errors: [ + { + message: 'This operation was aborted', + path: ['todo', 'id'], + locations: [{ line: 4, column: 11 }], + }, + ], + }); + }); + it('should stop the execution when aborted during object field completion with a custom error', async () => { const abortController = new AbortController(); const document = parse(` diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 17a8704bb2..beef2fbb80 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -1955,12 +1955,12 @@ export const defaultTypeResolver: GraphQLTypeResolver = * of calling that function while passing along args and context value. */ export const defaultFieldResolver: GraphQLFieldResolver = - function (source: any, args, contextValue, info) { + function (source: any, args, contextValue, info, abortSignal) { // ensure source is a value for which property access is acceptable. if (isObjectLike(source) || typeof source === 'function') { const property = source[info.fieldName]; if (typeof property === 'function') { - return source[info.fieldName](args, contextValue, info); + return source[info.fieldName](args, contextValue, info, abortSignal); } return property; } From 2e0ce19c72ca31ab32db60fabe226d2d9bfb8130 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Mon, 28 Oct 2024 13:12:57 +0200 Subject: [PATCH 3/3] change fn name to be more generic --- src/execution/__tests__/abort-signal-test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/execution/__tests__/abort-signal-test.ts b/src/execution/__tests__/abort-signal-test.ts index 617031c084..ad9ba6c332 100644 --- a/src/execution/__tests__/abort-signal-test.ts +++ b/src/execution/__tests__/abort-signal-test.ts @@ -110,11 +110,6 @@ describe('Execute: Cancellation', () => { }); it('should provide access to the abort signal within resolvers', async () => { - const throwIfAborted = async (abortSignal: AbortSignal) => { - await resolveOnNextTick(); - abortSignal.throwIfAborted(); - }; - const abortController = new AbortController(); const document = parse(` query { @@ -124,6 +119,11 @@ describe('Execute: Cancellation', () => { } `); + const cancellableAsyncFn = async (abortSignal: AbortSignal) => { + await resolveOnNextTick(); + abortSignal.throwIfAborted(); + }; + const resultPromise = execute({ document, schema, @@ -131,7 +131,7 @@ describe('Execute: Cancellation', () => { rootValue: { todo: { id: (_args: any, _context: any, _info: any, signal: AbortSignal) => - throwIfAborted(signal), + cancellableAsyncFn(signal), }, }, });