Skip to content

Commit b10a5f5

Browse files
glasseryaacovCR
authored andcommitted
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 690e614 commit b10a5f5

5 files changed

Lines changed: 26 additions & 9 deletions

File tree

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
@@ -630,6 +630,7 @@ function executeField(
630630
toNodes(fieldDetailsList),
631631
parentType,
632632
path,
633+
abortSignal,
633634
);
634635

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

653654
if (isPromise(result)) {
654655
return completePromisedValue(
@@ -707,6 +708,7 @@ export function buildResolveInfo(
707708
fieldNodes: ReadonlyArray<FieldNode>,
708709
parentType: GraphQLObjectType,
709710
path: Path,
711+
abortSignal: AbortSignal | undefined,
710712
): GraphQLResolveInfo {
711713
const { schema, fragmentDefinitions, rootValue, operation, variableValues } =
712714
validatedExecutionArgs;
@@ -723,6 +725,7 @@ export function buildResolveInfo(
723725
rootValue,
724726
operation,
725727
variableValues,
728+
abortSignal,
726729
};
727730
}
728731

@@ -1442,12 +1445,12 @@ export const defaultTypeResolver: GraphQLTypeResolver<unknown, unknown> =
14421445
* of calling that function while passing along args and context value.
14431446
*/
14441447
export const defaultFieldResolver: GraphQLFieldResolver<unknown, unknown> =
1445-
function (source: any, args, contextValue, info, abortSignal) {
1448+
function (source: any, args, contextValue, info) {
14461449
// ensure source is a value for which property access is acceptable.
14471450
if (isObjectLike(source) || typeof source === 'function') {
14481451
const property = source[info.fieldName];
14491452
if (typeof property === 'function') {
1450-
return source[info.fieldName](args, contextValue, info, abortSignal);
1453+
return source[info.fieldName](args, contextValue, info);
14511454
}
14521455
return property;
14531456
}
@@ -1651,6 +1654,7 @@ function executeSubscription(
16511654
toNodes(fieldDetailsList),
16521655
rootType,
16531656
path,
1657+
abortSignal,
16541658
);
16551659

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

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