Skip to content

Commit 12a5ec9

Browse files
authored
feat: pass abortSignal to resolvers (#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 fbb191a commit 12a5ec9

File tree

3 files changed

+54
-5
lines changed

3 files changed

+54
-5
lines changed

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,53 @@ describe('Execute: Cancellation', () => {
109109
});
110110
});
111111

112+
it('should provide access to the abort signal within resolvers', async () => {
113+
const abortController = new AbortController();
114+
const document = parse(`
115+
query {
116+
todo {
117+
id
118+
}
119+
}
120+
`);
121+
122+
const cancellableAsyncFn = async (abortSignal: AbortSignal) => {
123+
await resolveOnNextTick();
124+
abortSignal.throwIfAborted();
125+
};
126+
127+
const resultPromise = execute({
128+
document,
129+
schema,
130+
abortSignal: abortController.signal,
131+
rootValue: {
132+
todo: {
133+
id: (_args: any, _context: any, _info: any, signal: AbortSignal) =>
134+
cancellableAsyncFn(signal),
135+
},
136+
},
137+
});
138+
139+
abortController.abort();
140+
141+
const result = await resultPromise;
142+
143+
expectJSON(result).toDeepEqual({
144+
data: {
145+
todo: {
146+
id: null,
147+
},
148+
},
149+
errors: [
150+
{
151+
message: 'This operation was aborted',
152+
path: ['todo', 'id'],
153+
locations: [{ line: 4, column: 11 }],
154+
},
155+
],
156+
});
157+
});
158+
112159
it('should stop the execution when aborted during object field completion with a custom error', async () => {
113160
const abortController = new AbortController();
114161
const document = parse(`

src/execution/execute.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -798,7 +798,7 @@ function executeField(
798798
deferMap: ReadonlyMap<DeferUsage, DeferredFragmentRecord> | undefined,
799799
): PromiseOrValue<GraphQLWrappedResult<unknown>> | undefined {
800800
const validatedExecutionArgs = exeContext.validatedExecutionArgs;
801-
const { schema, contextValue, variableValues, hideSuggestions } =
801+
const { schema, contextValue, variableValues, hideSuggestions, abortSignal } =
802802
validatedExecutionArgs;
803803
const fieldName = fieldDetailsList[0].node.name.value;
804804
const fieldDef = schema.getField(parentType, fieldName);
@@ -833,7 +833,7 @@ function executeField(
833833
// The resolve function's optional third argument is a context value that
834834
// is provided to every resolve function within an execution. It is commonly
835835
// used to represent an authenticated user, or request-specific caches.
836-
const result = resolveFn(source, args, contextValue, info);
836+
const result = resolveFn(source, args, contextValue, info, abortSignal);
837837

838838
if (isPromise(result)) {
839839
return completePromisedValue(
@@ -1955,12 +1955,12 @@ export const defaultTypeResolver: GraphQLTypeResolver<unknown, unknown> =
19551955
* of calling that function while passing along args and context value.
19561956
*/
19571957
export const defaultFieldResolver: GraphQLFieldResolver<unknown, unknown> =
1958-
function (source: any, args, contextValue, info) {
1958+
function (source: any, args, contextValue, info, abortSignal) {
19591959
// ensure source is a value for which property access is acceptable.
19601960
if (isObjectLike(source) || typeof source === 'function') {
19611961
const property = source[info.fieldName];
19621962
if (typeof property === 'function') {
1963-
return source[info.fieldName](args, contextValue, info);
1963+
return source[info.fieldName](args, contextValue, info, abortSignal);
19641964
}
19651965
return property;
19661966
}
@@ -2115,6 +2115,7 @@ function executeSubscription(
21152115
operation,
21162116
variableValues,
21172117
hideSuggestions,
2118+
abortSignal,
21182119
} = validatedExecutionArgs;
21192120

21202121
const rootType = schema.getSubscriptionType();
@@ -2180,7 +2181,7 @@ function executeSubscription(
21802181
// The resolve function's optional third argument is a context value that
21812182
// is provided to every resolve function within an execution. It is commonly
21822183
// used to represent an authenticated user, or request-specific caches.
2183-
const result = resolveFn(rootValue, args, contextValue, info);
2184+
const result = resolveFn(rootValue, args, contextValue, info, abortSignal);
21842185

21852186
if (isPromise(result)) {
21862187
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)