Skip to content

feat: pass abortSignal to resolvers via GraphQLResolveInfo #4425

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions src/execution/__tests__/cancellation-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,19 @@ describe('Execute: Cancellation', () => {
}
`);

let aborted = false;
const cancellableAsyncFn = async (abortSignal: AbortSignal) => {
if (abortSignal.aborted) {
aborted = true;
} else {
abortSignal.addEventListener('abort', () => {
aborted = true;
});
}
// We are in an async function so it gets cancelled and the field ends up
// resolving with the abort signal's error.
await resolveOnNextTick();
abortSignal.throwIfAborted();
throw Error('some random other error that does not show up in response');
};

const resultPromise = execute({
Expand All @@ -141,8 +151,8 @@ describe('Execute: Cancellation', () => {
abortSignal: abortController.signal,
rootValue: {
todo: {
id: (_args: any, _context: any, _info: any, signal: AbortSignal) =>
cancellableAsyncFn(signal),
id: (_args: any, _context: any, info: { abortSignal: AbortSignal }) =>
cancellableAsyncFn(info.abortSignal),
},
},
});
Expand All @@ -165,6 +175,8 @@ describe('Execute: Cancellation', () => {
},
],
});

expect(aborted).to.equal(true);
});

it('should stop the execution when aborted during object field completion with a custom error', async () => {
Expand Down
1 change: 1 addition & 0 deletions src/execution/__tests__/executor-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ describe('Execute: Handles basic execution tasks', () => {
'rootValue',
'operation',
'variableValues',
'abortSignal',
);

const operation = document.definitions[0];
Expand Down
12 changes: 8 additions & 4 deletions src/execution/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -875,6 +875,7 @@ function executeField(
toNodes(fieldDetailsList),
parentType,
path,
abortSignal,
);

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

if (isPromise(result)) {
return completePromisedValue(
Expand Down Expand Up @@ -960,6 +961,7 @@ export function buildResolveInfo(
fieldNodes: ReadonlyArray<FieldNode>,
parentType: GraphQLObjectType,
path: Path,
abortSignal: AbortSignal | undefined,
): GraphQLResolveInfo {
const { schema, fragmentDefinitions, rootValue, operation, variableValues } =
validatedExecutionArgs;
Expand All @@ -976,6 +978,7 @@ export function buildResolveInfo(
rootValue,
operation,
variableValues,
abortSignal,
};
}

Expand Down Expand Up @@ -2079,12 +2082,12 @@ export const defaultTypeResolver: GraphQLTypeResolver<unknown, unknown> =
* of calling that function while passing along args and context value.
*/
export const defaultFieldResolver: GraphQLFieldResolver<unknown, unknown> =
function (source: any, args, contextValue, info, abortSignal) {
function (source: any, args, contextValue, info) {
// ensure source is a value for which property access is acceptable.
if (isObjectLike(source) || typeof source === 'function') {
const property = source[info.fieldName];
if (typeof property === 'function') {
return source[info.fieldName](args, contextValue, info, abortSignal);
return source[info.fieldName](args, contextValue, info);
}
return property;
}
Expand Down Expand Up @@ -2293,6 +2296,7 @@ function executeSubscription(
fieldNodes,
rootType,
path,
abortSignal,
);

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

if (isPromise(result)) {
const abortSignalListener = abortSignal
Expand Down
2 changes: 1 addition & 1 deletion src/type/definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -997,7 +997,6 @@ export type GraphQLFieldResolver<
args: TArgs,
context: TContext,
info: GraphQLResolveInfo,
abortSignal: AbortSignal | undefined,
) => TResult;

export interface GraphQLResolveInfo {
Expand All @@ -1011,6 +1010,7 @@ export interface GraphQLResolveInfo {
readonly rootValue: unknown;
readonly operation: OperationDefinitionNode;
readonly variableValues: VariableValues;
readonly abortSignal: AbortSignal | undefined;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion website/pages/upgrade-guides/v16-v17.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ Use the `validateInputValue` helper to retrieve the actual errors.

- Added `hideSuggestions` option to `execute`/`validate`/`subscribe`/... to hide schema-suggestions in error messages
- Added `abortSignal` option to `graphql()`, `execute()`, and `subscribe()` allows cancellation of these methods;
the `abortSignal` can also be passed to field resolvers to cancel asynchronous work that they initiate.
`info.abortSignal` can also be used in field resolvers to cancel asynchronous work that they initiate.
- `extensions` support `symbol` keys, in addition to the normal string keys.
- Added ability for resolver functions to return async iterables.
- 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.
Expand Down
Loading