Skip to content

Commit 8c174c3

Browse files
glasseryaacovCR
authored andcommitted
feat: pass abortSignal to resolvers via GraphQLResolveInfo (graphql#4425)
In graphql#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 71f50c7 commit 8c174c3

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
@@ -98,9 +98,19 @@ describe('Execute: Cancellation', () => {
9898
}
9999
`);
100100

101+
let aborted = false;
101102
const cancellableAsyncFn = async (abortSignal: AbortSignal) => {
103+
if (abortSignal.aborted) {
104+
aborted = true;
105+
} else {
106+
abortSignal.addEventListener('abort', () => {
107+
aborted = true;
108+
});
109+
}
110+
// We are in an async function so it gets cancelled and the field ends up
111+
// resolving with the abort signal's error.
102112
await resolveOnNextTick();
103-
abortSignal.throwIfAborted();
113+
throw Error('some random other error that does not show up in response');
104114
};
105115

106116
const resultPromise = execute({
@@ -109,8 +119,8 @@ describe('Execute: Cancellation', () => {
109119
abortSignal: abortController.signal,
110120
rootValue: {
111121
todo: {
112-
id: (_args: any, _context: any, _info: any, signal: AbortSignal) =>
113-
cancellableAsyncFn(signal),
122+
id: (_args: any, _context: any, info: { abortSignal: AbortSignal }) =>
123+
cancellableAsyncFn(info.abortSignal),
114124
},
115125
},
116126
});
@@ -133,6 +143,8 @@ describe('Execute: Cancellation', () => {
133143
},
134144
],
135145
});
146+
147+
expect(aborted).to.equal(true);
136148
});
137149

138150
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
@@ -219,6 +219,7 @@ describe('Execute: Handles basic execution tasks', () => {
219219
'rootValue',
220220
'operation',
221221
'variableValues',
222+
'abortSignal',
222223
);
223224

224225
const operation = document.definitions[0];

src/execution/execute.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -629,6 +629,7 @@ function executeField(
629629
fieldNodes,
630630
parentType,
631631
path,
632+
abortSignal,
632633
);
633634

634635
// Get the resolve function, regardless of if its result is normal or abrupt (error).
@@ -647,7 +648,7 @@ function executeField(
647648
// The resolve function's optional third argument is a context value that
648649
// is provided to every resolve function within an execution. It is commonly
649650
// used to represent an authenticated user, or request-specific caches.
650-
const result = resolveFn(source, args, contextValue, info, abortSignal);
651+
const result = resolveFn(source, args, contextValue, info);
651652

652653
if (isPromise(result)) {
653654
return completePromisedValue(
@@ -702,6 +703,7 @@ export function buildResolveInfo(
702703
fieldNodes: ReadonlyArray<FieldNode>,
703704
parentType: GraphQLObjectType,
704705
path: Path,
706+
abortSignal: AbortSignal | undefined,
705707
): GraphQLResolveInfo {
706708
const { schema, fragmentDefinitions, rootValue, operation, variableValues } =
707709
validatedExecutionArgs;
@@ -718,6 +720,7 @@ export function buildResolveInfo(
718720
rootValue,
719721
operation,
720722
variableValues,
723+
abortSignal,
721724
};
722725
}
723726

@@ -1441,12 +1444,12 @@ export const defaultTypeResolver: GraphQLTypeResolver<unknown, unknown> =
14411444
* of calling that function while passing along args and context value.
14421445
*/
14431446
export const defaultFieldResolver: GraphQLFieldResolver<unknown, unknown> =
1444-
function (source: any, args, contextValue, info, abortSignal) {
1447+
function (source: any, args, contextValue, info) {
14451448
// ensure source is a value for which property access is acceptable.
14461449
if (isObjectLike(source) || typeof source === 'function') {
14471450
const property = source[info.fieldName];
14481451
if (typeof property === 'function') {
1449-
return source[info.fieldName](args, contextValue, info, abortSignal);
1452+
return source[info.fieldName](args, contextValue, info);
14501453
}
14511454
return property;
14521455
}
@@ -1650,6 +1653,7 @@ function executeSubscription(
16501653
fieldNodes,
16511654
rootType,
16521655
path,
1656+
abortSignal,
16531657
);
16541658

16551659
try {
@@ -1674,7 +1678,7 @@ function executeSubscription(
16741678
// The resolve function's optional third argument is a context value that
16751679
// is provided to every resolve function within an execution. It is commonly
16761680
// used to represent an authenticated user, or request-specific caches.
1677-
const result = resolveFn(rootValue, args, contextValue, info, abortSignal);
1681+
const result = resolveFn(rootValue, args, contextValue, info);
16781682

16791683
if (isPromise(result)) {
16801684
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)