Skip to content

Commit d99357f

Browse files
committed
feat: pass abortSignal to resolvers (graphql#4261)
this allows e.g. passing the signal to fetch Note: the `abortSignal` is now the fifth argument to a GraphQLFieldResolverFn. If no resolver if provided, and the parent is an object with a key for the field name with a value that is a function, the `abortSignal` will be the fourth argument, as in the included test, with the `parent` accessible via the `this` keyword.
1 parent 892c09c commit d99357f

File tree

3 files changed

+55
-5
lines changed

3 files changed

+55
-5
lines changed

src/execution/__tests__/abort-signal-test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { expect } from 'chai';
22
import { describe, it } from 'mocha';
33

44
import { expectJSON } from '../../__testUtils__/expectJSON.js';
5+
import { resolveOnNextTick } from '../../__testUtils__/resolveOnNextTick.js';
56

67
import { parse } from '../../language/parser.js';
78

@@ -80,6 +81,53 @@ describe('Execute: Cancellation', () => {
8081
});
8182
});
8283

84+
it('should provide access to the abort signal within resolvers', async () => {
85+
const abortController = new AbortController();
86+
const document = parse(`
87+
query {
88+
todo {
89+
id
90+
}
91+
}
92+
`);
93+
94+
const cancellableAsyncFn = async (abortSignal: AbortSignal) => {
95+
await resolveOnNextTick();
96+
abortSignal.throwIfAborted();
97+
};
98+
99+
const resultPromise = execute({
100+
document,
101+
schema,
102+
abortSignal: abortController.signal,
103+
rootValue: {
104+
todo: {
105+
id: (_args: any, _context: any, _info: any, signal: AbortSignal) =>
106+
cancellableAsyncFn(signal),
107+
},
108+
},
109+
});
110+
111+
abortController.abort();
112+
113+
const result = await resultPromise;
114+
115+
expectJSON(result).toDeepEqual({
116+
data: {
117+
todo: {
118+
id: null,
119+
},
120+
},
121+
errors: [
122+
{
123+
message: 'This operation was aborted',
124+
path: ['todo', 'id'],
125+
locations: [{ line: 4, column: 11 }],
126+
},
127+
],
128+
});
129+
});
130+
83131
it('should stop the execution when aborted during object field completion with a custom error', async () => {
84132
const abortController = new AbortController();
85133
const document = parse(`

src/execution/execute.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -584,7 +584,7 @@ function executeField(
584584
path: Path,
585585
): PromiseOrValue<unknown> {
586586
const validatedExecutionArgs = exeContext.validatedExecutionArgs;
587-
const { schema, contextValue, variableValues, hideSuggestions } =
587+
const { schema, contextValue, variableValues, hideSuggestions, abortSignal } =
588588
validatedExecutionArgs;
589589
const firstFieldDetails = fieldDetailsList[0];
590590
const firstFieldNode = firstFieldDetails.node;
@@ -622,7 +622,7 @@ function executeField(
622622
// The resolve function's optional third argument is a context value that
623623
// is provided to every resolve function within an execution. It is commonly
624624
// used to represent an authenticated user, or request-specific caches.
625-
const result = resolveFn(source, args, contextValue, info);
625+
const result = resolveFn(source, args, contextValue, info, abortSignal);
626626

627627
if (isPromise(result)) {
628628
return completePromisedValue(
@@ -1409,12 +1409,12 @@ export const defaultTypeResolver: GraphQLTypeResolver<unknown, unknown> =
14091409
* of calling that function while passing along args and context value.
14101410
*/
14111411
export const defaultFieldResolver: GraphQLFieldResolver<unknown, unknown> =
1412-
function (source: any, args, contextValue, info) {
1412+
function (source: any, args, contextValue, info, abortSignal) {
14131413
// ensure source is a value for which property access is acceptable.
14141414
if (isObjectLike(source) || typeof source === 'function') {
14151415
const property = source[info.fieldName];
14161416
if (typeof property === 'function') {
1417-
return source[info.fieldName](args, contextValue, info);
1417+
return source[info.fieldName](args, contextValue, info, abortSignal);
14181418
}
14191419
return property;
14201420
}
@@ -1564,6 +1564,7 @@ function executeSubscription(
15641564
operation,
15651565
variableValues,
15661566
hideSuggestions,
1567+
abortSignal,
15671568
} = validatedExecutionArgs;
15681569

15691570
const rootType = schema.getSubscriptionType();
@@ -1629,7 +1630,7 @@ function executeSubscription(
16291630
// The resolve function's optional third argument is a context value that
16301631
// is provided to every resolve function within an execution. It is commonly
16311632
// used to represent an authenticated user, or request-specific caches.
1632-
const result = resolveFn(rootValue, args, contextValue, info);
1633+
const result = resolveFn(rootValue, args, contextValue, info, abortSignal);
16331634

16341635
if (isPromise(result)) {
16351636
return result

src/type/definition.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -990,6 +990,7 @@ export type GraphQLFieldResolver<
990990
args: TArgs,
991991
context: TContext,
992992
info: GraphQLResolveInfo,
993+
abortSignal: AbortSignal | undefined,
993994
) => TResult;
994995

995996
export interface GraphQLResolveInfo {

0 commit comments

Comments
 (0)