Skip to content

Commit 960b0b4

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 f92420a commit 960b0b4

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 fieldNodes = toNodes(fieldDetailsList);
590590
const firstFieldNode = fieldNodes[0];
@@ -621,7 +621,7 @@ function executeField(
621621
// The resolve function's optional third argument is a context value that
622622
// is provided to every resolve function within an execution. It is commonly
623623
// used to represent an authenticated user, or request-specific caches.
624-
const result = resolveFn(source, args, contextValue, info);
624+
const result = resolveFn(source, args, contextValue, info, abortSignal);
625625

626626
if (isPromise(result)) {
627627
return completePromisedValue(
@@ -1408,12 +1408,12 @@ export const defaultTypeResolver: GraphQLTypeResolver<unknown, unknown> =
14081408
* of calling that function while passing along args and context value.
14091409
*/
14101410
export const defaultFieldResolver: GraphQLFieldResolver<unknown, unknown> =
1411-
function (source: any, args, contextValue, info) {
1411+
function (source: any, args, contextValue, info, abortSignal) {
14121412
// ensure source is a value for which property access is acceptable.
14131413
if (isObjectLike(source) || typeof source === 'function') {
14141414
const property = source[info.fieldName];
14151415
if (typeof property === 'function') {
1416-
return source[info.fieldName](args, contextValue, info);
1416+
return source[info.fieldName](args, contextValue, info, abortSignal);
14171417
}
14181418
return property;
14191419
}
@@ -1563,6 +1563,7 @@ function executeSubscription(
15631563
operation,
15641564
variableValues,
15651565
hideSuggestions,
1566+
abortSignal,
15661567
} = validatedExecutionArgs;
15671568

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

16331634
if (isPromise(result)) {
16341635
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)