Skip to content

Commit fba352d

Browse files
authored
feat: pass abortSignal to resolvers via GraphQLResolveInfo (#4425)
In #4261 (not yet released in v17) we made abortSignal available to resolvers via a fifth argument to the field resolver. Among other things, this means that any code that processes schemas to wrap resolvers in other functions would have to be aware of this one new feature and specially thread through the new behavior. It also changed the TypeScript signature of GraphQLFieldResolver to *require* passing the fifth argument (even if undefined). But the field resolver interface already has a place for GraphQL-JS to put a grab-bag of helpful named objects for use by resolvers: `GraphQLResolveInfo`. This PR (which is not backwards compatible with v17.0.0-alpha.8, but is backwards-compatible with v16) moves the abortSignal into `GraphQLResolveInfo`. It also improves the test of this feature to actually make use of the AbortSignal API (the previous test actually passes when this change is made, without changing the test to find the AbortSignal in the new location).
1 parent ef042cf commit fba352d

File tree

5 files changed

+26
-9
lines changed

5 files changed

+26
-9
lines changed

src/execution/__tests__/cancellation-test.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,19 @@ describe('Execute: Cancellation', () => {
130130
}
131131
`);
132132

133+
let aborted = false;
133134
const cancellableAsyncFn = async (abortSignal: AbortSignal) => {
135+
if (abortSignal.aborted) {
136+
aborted = true;
137+
} else {
138+
abortSignal.addEventListener('abort', () => {
139+
aborted = true;
140+
});
141+
}
142+
// We are in an async function so it gets cancelled and the field ends up
143+
// resolving with the abort signal's error.
134144
await resolveOnNextTick();
135-
abortSignal.throwIfAborted();
145+
throw Error('some random other error that does not show up in response');
136146
};
137147

138148
const resultPromise = execute({
@@ -141,8 +151,8 @@ describe('Execute: Cancellation', () => {
141151
abortSignal: abortController.signal,
142152
rootValue: {
143153
todo: {
144-
id: (_args: any, _context: any, _info: any, signal: AbortSignal) =>
145-
cancellableAsyncFn(signal),
154+
id: (_args: any, _context: any, info: { abortSignal: AbortSignal }) =>
155+
cancellableAsyncFn(info.abortSignal),
146156
},
147157
},
148158
});
@@ -165,6 +175,8 @@ describe('Execute: Cancellation', () => {
165175
},
166176
],
167177
});
178+
179+
expect(aborted).to.equal(true);
168180
});
169181

170182
it('should stop the execution when aborted during object field completion with a custom error', async () => {

src/execution/__tests__/executor-test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ describe('Execute: Handles basic execution tasks', () => {
223223
'rootValue',
224224
'operation',
225225
'variableValues',
226+
'abortSignal',
226227
);
227228

228229
const operation = document.definitions[0];

src/execution/execute.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -875,6 +875,7 @@ function executeField(
875875
toNodes(fieldDetailsList),
876876
parentType,
877877
path,
878+
abortSignal,
878879
);
879880

880881
// Get the resolve function, regardless of if its result is normal or abrupt (error).
@@ -893,7 +894,7 @@ function executeField(
893894
// The resolve function's optional third argument is a context value that
894895
// is provided to every resolve function within an execution. It is commonly
895896
// used to represent an authenticated user, or request-specific caches.
896-
const result = resolveFn(source, args, contextValue, info, abortSignal);
897+
const result = resolveFn(source, args, contextValue, info);
897898

898899
if (isPromise(result)) {
899900
return completePromisedValue(
@@ -960,6 +961,7 @@ export function buildResolveInfo(
960961
fieldNodes: ReadonlyArray<FieldNode>,
961962
parentType: GraphQLObjectType,
962963
path: Path,
964+
abortSignal: AbortSignal | undefined,
963965
): GraphQLResolveInfo {
964966
const { schema, fragmentDefinitions, rootValue, operation, variableValues } =
965967
validatedExecutionArgs;
@@ -976,6 +978,7 @@ export function buildResolveInfo(
976978
rootValue,
977979
operation,
978980
variableValues,
981+
abortSignal,
979982
};
980983
}
981984

@@ -2079,12 +2082,12 @@ export const defaultTypeResolver: GraphQLTypeResolver<unknown, unknown> =
20792082
* of calling that function while passing along args and context value.
20802083
*/
20812084
export const defaultFieldResolver: GraphQLFieldResolver<unknown, unknown> =
2082-
function (source: any, args, contextValue, info, abortSignal) {
2085+
function (source: any, args, contextValue, info) {
20832086
// ensure source is a value for which property access is acceptable.
20842087
if (isObjectLike(source) || typeof source === 'function') {
20852088
const property = source[info.fieldName];
20862089
if (typeof property === 'function') {
2087-
return source[info.fieldName](args, contextValue, info, abortSignal);
2090+
return source[info.fieldName](args, contextValue, info);
20882091
}
20892092
return property;
20902093
}
@@ -2293,6 +2296,7 @@ function executeSubscription(
22932296
fieldNodes,
22942297
rootType,
22952298
path,
2299+
abortSignal,
22962300
);
22972301

22982302
try {
@@ -2317,7 +2321,7 @@ function executeSubscription(
23172321
// The resolve function's optional third argument is a context value that
23182322
// is provided to every resolve function within an execution. It is commonly
23192323
// used to represent an authenticated user, or request-specific caches.
2320-
const result = resolveFn(rootValue, args, contextValue, info, abortSignal);
2324+
const result = resolveFn(rootValue, args, contextValue, info);
23212325

23222326
if (isPromise(result)) {
23232327
const abortSignalListener = abortSignal

src/type/definition.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -997,7 +997,6 @@ export type GraphQLFieldResolver<
997997
args: TArgs,
998998
context: TContext,
999999
info: GraphQLResolveInfo,
1000-
abortSignal: AbortSignal | undefined,
10011000
) => TResult;
10021001

10031002
export interface GraphQLResolveInfo {
@@ -1011,6 +1010,7 @@ export interface GraphQLResolveInfo {
10111010
readonly rootValue: unknown;
10121011
readonly operation: OperationDefinitionNode;
10131012
readonly variableValues: VariableValues;
1013+
readonly abortSignal: AbortSignal | undefined;
10141014
}
10151015

10161016
/**

website/pages/upgrade-guides/v16-v17.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ Use the `validateInputValue` helper to retrieve the actual errors.
178178

179179
- Added `hideSuggestions` option to `execute`/`validate`/`subscribe`/... to hide schema-suggestions in error messages
180180
- Added `abortSignal` option to `graphql()`, `execute()`, and `subscribe()` allows cancellation of these methods;
181-
the `abortSignal` can also be passed to field resolvers to cancel asynchronous work that they initiate.
181+
`info.abortSignal` can also be used in field resolvers to cancel asynchronous work that they initiate.
182182
- `extensions` support `symbol` keys, in addition to the normal string keys.
183183
- Added ability for resolver functions to return async iterables.
184184
- Added `perEventExecutor` execution option to allows specifying a custom executor for subscription source stream events, which can be useful for preparing a per event execution context argument.

0 commit comments

Comments
 (0)