Skip to content

Commit 14a001e

Browse files
authored
abort promise and sync for error (#6009)
* abort promise and sync for error * add changeset * test
1 parent 8dbf2e4 commit 14a001e

File tree

3 files changed

+99
-16
lines changed

3 files changed

+99
-16
lines changed

.changeset/ninety-rats-walk.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@graphql-tools/executor": patch
3+
---
4+
5+
correctly raise abort exception for Promise and sync execution

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

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ describe('Abort Signal', () => {
232232
},
233233
});
234234

235-
const result = await normalizedExecutor({
235+
const result$ = normalizedExecutor({
236236
schema,
237237
document: parse(/* GraphQL */ `
238238
query {
@@ -241,20 +241,7 @@ describe('Abort Signal', () => {
241241
`),
242242
signal: controller.signal,
243243
});
244-
expect(result).toMatchObject({
245-
errors: [
246-
{
247-
message: 'Execution aborted',
248-
path: ['counter'],
249-
locations: [
250-
{
251-
line: 3,
252-
column: 11,
253-
},
254-
],
255-
},
256-
],
257-
});
244+
await expect(result$).rejects.toMatchInlineSnapshot(`DOMException {}`);
258245
expect(isAborted).toEqual(true);
259246
});
260247
it('stops pending stream execution for incremental delivery', async () => {
@@ -319,4 +306,67 @@ describe('Abort Signal', () => {
319306
await expect(next$).rejects.toMatchInlineSnapshot(`DOMException {}`);
320307
expect(isReturnInvoked).toEqual(true);
321308
});
309+
it('stops promise execution', async () => {
310+
const controller = new AbortController();
311+
const d = createDeferred();
312+
313+
const schema = makeExecutableSchema({
314+
typeDefs: /* GraphQL */ `
315+
type Query {
316+
number: Int!
317+
}
318+
`,
319+
resolvers: {
320+
Query: {
321+
number: () => d.promise.then(() => 1),
322+
},
323+
},
324+
});
325+
326+
const result$ = normalizedExecutor({
327+
schema,
328+
document: parse(/* GraphQL */ `
329+
query {
330+
number
331+
}
332+
`),
333+
signal: controller.signal,
334+
});
335+
336+
expect(result$).toBeInstanceOf(Promise);
337+
controller.abort();
338+
await expect(result$).rejects.toMatchInlineSnapshot(`DOMException {}`);
339+
});
340+
it('does not even try to execute if the signal is already aborted', async () => {
341+
const controller = new AbortController();
342+
let resolverGotInvoked = false;
343+
const schema = makeExecutableSchema({
344+
typeDefs: /* GraphQL */ `
345+
type Query {
346+
number: Int!
347+
}
348+
`,
349+
resolvers: {
350+
Query: {
351+
number: () => {
352+
resolverGotInvoked = true;
353+
return 1;
354+
},
355+
},
356+
},
357+
});
358+
controller.abort();
359+
expect(() =>
360+
normalizedExecutor({
361+
schema,
362+
document: parse(/* GraphQL */ `
363+
query {
364+
number
365+
}
366+
`),
367+
signal: controller.signal,
368+
}),
369+
).toThrowErrorMatchingInlineSnapshot(`"This operation was aborted"`);
370+
expect(resolverGotInvoked).toBe(false);
371+
});
322372
});

packages/executor/src/execution/execute.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,10 @@ export function execute<TData = any, TVariables = any, TContext = any>(
286286
function executeImpl<TData = any, TVariables = any, TContext = any>(
287287
exeContext: ExecutionContext<TVariables, TContext>,
288288
): MaybePromise<SingularExecutionResult<TData> | IncrementalExecutionResults<TData>> {
289+
if (exeContext.signal?.aborted) {
290+
throw exeContext.signal.reason;
291+
}
292+
289293
// Return a Promise that will eventually resolve to the data described by
290294
// The "Response" section of the GraphQL specification.
291295
//
@@ -297,7 +301,7 @@ function executeImpl<TData = any, TVariables = any, TContext = any>(
297301
// Errors from sub-fields of a NonNull type may propagate to the top level,
298302
// at which point we still log the error and null the parent field, which
299303
// in this case is the entire response.
300-
return new ValueOrPromise(() => executeOperation<TData, TVariables, TContext>(exeContext))
304+
const result = new ValueOrPromise(() => executeOperation<TData, TVariables, TContext>(exeContext))
301305
.then(
302306
data => {
303307
const initialResult = buildResponse(data, exeContext.errors);
@@ -318,6 +322,30 @@ function executeImpl<TData = any, TVariables = any, TContext = any>(
318322
},
319323
)
320324
.resolve()!;
325+
326+
if (!exeContext.signal || 'initialResult' in result || 'then' in result === false) {
327+
return result;
328+
}
329+
330+
let resolve: () => void;
331+
let reject: (reason: any) => void;
332+
const abortP = new Promise<never>((_resolve, _reject) => {
333+
resolve = _resolve as any;
334+
reject = _reject;
335+
});
336+
337+
function abortListener() {
338+
reject(exeContext.signal?.reason);
339+
}
340+
341+
exeContext.signal.addEventListener('abort', abortListener);
342+
343+
result.then(() => {
344+
exeContext.signal?.removeEventListener('abort', abortListener);
345+
resolve();
346+
});
347+
348+
return Promise.race([abortP, result]);
321349
}
322350

323351
/**

0 commit comments

Comments
 (0)