Skip to content

Commit ae8f4fe

Browse files
committed
WIP
1 parent f531737 commit ae8f4fe

File tree

2 files changed

+246
-0
lines changed

2 files changed

+246
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
import { expect } from 'chai';
2+
import { describe, it } from 'mocha';
3+
4+
import { expectJSON } from '../../__testUtils__/expectJSON.js';
5+
import { resolveOnNextTick } from '../../__testUtils__/resolveOnNextTick.js';
6+
7+
import { parse } from '../../language/parser.js';
8+
9+
import {
10+
GraphQLObjectType,
11+
GraphQLSchema,
12+
GraphQLString,
13+
} from '../../type/index.js';
14+
15+
import { buildSchema } from '../../utilities/buildASTSchema.js';
16+
17+
import { execute } from '../execute.js';
18+
19+
const schema = buildSchema(/* GraphQL */ `
20+
type Todo {
21+
id: ID!
22+
text: String!
23+
completed: Boolean!
24+
author: User
25+
}
26+
27+
type User {
28+
id: ID!
29+
name: String!
30+
}
31+
32+
type Query {
33+
todo: Todo
34+
}
35+
36+
type Mutation {
37+
foo: String
38+
bar: String
39+
}
40+
`);
41+
42+
describe('Abort Signal', () => {
43+
it('should stop the execution when aborted in resolver', async () => {
44+
const abortController = new AbortController();
45+
const document = parse(/* GraphQL */ `
46+
query {
47+
todo {
48+
id
49+
author {
50+
id
51+
}
52+
}
53+
}
54+
`);
55+
const result = await execute({
56+
document,
57+
schema,
58+
abortSignal: abortController.signal,
59+
rootValue: {
60+
todo() {
61+
abortController.abort('Aborted');
62+
return {
63+
id: '1',
64+
text: 'Hello, World!',
65+
completed: false,
66+
/* c8 ignore next 3 */
67+
author: () => {
68+
expect.fail('Should not be called');
69+
},
70+
};
71+
},
72+
},
73+
});
74+
75+
expectJSON(result).toDeepEqual({
76+
data: {
77+
todo: null,
78+
},
79+
errors: [
80+
{
81+
locations: [
82+
{
83+
column: 9,
84+
line: 3,
85+
},
86+
],
87+
message: 'Aborted',
88+
path: ['todo'],
89+
},
90+
],
91+
});
92+
});
93+
94+
it('should stop the for serial mutation execution', async () => {
95+
const abortController = new AbortController();
96+
const document = parse(/* GraphQL */ `
97+
mutation {
98+
foo
99+
bar
100+
}
101+
`);
102+
const result = await execute({
103+
document,
104+
schema,
105+
abortSignal: abortController.signal,
106+
rootValue: {
107+
foo() {
108+
abortController.abort('Aborted');
109+
return 'baz';
110+
},
111+
/* c8 ignore next 3 */
112+
bar() {
113+
expect.fail('Should not be called');
114+
},
115+
},
116+
});
117+
118+
expectJSON(result).toDeepEqual({
119+
data: null,
120+
errors: [
121+
{
122+
message: 'Aborted',
123+
},
124+
],
125+
});
126+
});
127+
128+
it('should stop the execution when aborted pre-execute', async () => {
129+
const abortController = new AbortController();
130+
const document = parse(/* GraphQL */ `
131+
query {
132+
todo {
133+
id
134+
author {
135+
id
136+
}
137+
}
138+
}
139+
`);
140+
abortController.abort('Aborted');
141+
const result = await execute({
142+
document,
143+
schema,
144+
abortSignal: abortController.signal,
145+
rootValue: {
146+
/* c8 ignore next 3 */
147+
todo() {
148+
return {};
149+
},
150+
},
151+
});
152+
153+
expectJSON(result).toDeepEqual({
154+
data: null,
155+
errors: [
156+
{
157+
message: 'Aborted',
158+
},
159+
],
160+
});
161+
});
162+
163+
it('exits early on abort mid-execution', async () => {
164+
const asyncObjectType = new GraphQLObjectType({
165+
name: 'AsyncObject',
166+
fields: {
167+
field: {
168+
type: GraphQLString,
169+
/* c8 ignore next 3 */
170+
resolve() {
171+
expect.fail('Should not be called');
172+
},
173+
},
174+
},
175+
});
176+
177+
const newSchema = new GraphQLSchema({
178+
query: new GraphQLObjectType({
179+
name: 'Query',
180+
fields: {
181+
asyncObject: {
182+
type: asyncObjectType,
183+
async resolve() {
184+
await resolveOnNextTick();
185+
return {};
186+
},
187+
},
188+
},
189+
}),
190+
});
191+
192+
const document = parse(`
193+
{
194+
asyncObject {
195+
field
196+
}
197+
}
198+
`);
199+
200+
const abortController = new AbortController();
201+
202+
const result = execute({
203+
schema: newSchema,
204+
document,
205+
abortSignal: abortController.signal,
206+
});
207+
208+
abortController.abort();
209+
210+
expectJSON(await result).toDeepEqual({
211+
data: { asyncObject: null },
212+
errors: [
213+
{
214+
message: 'AbortError: This operation was aborted',
215+
locations: [{ line: 3, column: 9 }],
216+
path: ['asyncObject'],
217+
},
218+
],
219+
});
220+
});
221+
});

src/execution/execute.ts

+25
Original file line numberDiff line numberDiff line change
@@ -157,12 +157,14 @@ export interface ValidatedExecutionArgs {
157157
) => PromiseOrValue<ExecutionResult>;
158158
enableEarlyExecution: boolean;
159159
hideSuggestions: boolean;
160+
abortSignal: AbortSignal | undefined;
160161
}
161162

162163
export interface ExecutionContext {
163164
validatedExecutionArgs: ValidatedExecutionArgs;
164165
errors: Array<GraphQLError> | undefined;
165166
cancellableStreams: Set<CancellableStreamRecord> | undefined;
167+
abortSignal: AbortSignal | undefined;
166168
}
167169

168170
interface IncrementalContext {
@@ -187,6 +189,7 @@ export interface ExecutionArgs {
187189
>;
188190
enableEarlyExecution?: Maybe<boolean>;
189191
hideSuggestions?: Maybe<boolean>;
192+
abortSignal?: AbortSignal | undefined;
190193
}
191194

192195
export interface StreamUsage {
@@ -309,6 +312,7 @@ export function experimentalExecuteQueryOrMutationOrSubscriptionEvent(
309312
validatedExecutionArgs,
310313
errors: undefined,
311314
cancellableStreams: undefined,
315+
abortSignal: validatedExecutionArgs.abortSignal,
312316
};
313317
try {
314318
const {
@@ -318,7 +322,13 @@ export function experimentalExecuteQueryOrMutationOrSubscriptionEvent(
318322
operation,
319323
variableValues,
320324
hideSuggestions,
325+
abortSignal,
321326
} = validatedExecutionArgs;
327+
328+
if (abortSignal?.aborted) {
329+
throw new GraphQLError(abortSignal.reason);
330+
}
331+
322332
const rootType = schema.getRootType(operation.operation);
323333
if (rootType == null) {
324334
throw new GraphQLError(
@@ -592,6 +602,7 @@ export function validateExecutionArgs(
592602
perEventExecutor: perEventExecutor ?? executeSubscriptionEvent,
593603
enableEarlyExecution: enableEarlyExecution === true,
594604
hideSuggestions,
605+
abortSignal: args.abortSignal,
595606
};
596607
}
597608

@@ -656,6 +667,9 @@ function executeFieldsSerially(
656667
groupedFieldSet,
657668
(graphqlWrappedResult, [responseName, fieldDetailsList]) => {
658669
const fieldPath = addPath(path, responseName, parentType.name);
670+
if (exeContext.abortSignal?.aborted) {
671+
throw new GraphQLError(exeContext.abortSignal.reason);
672+
}
659673
const result = executeField(
660674
exeContext,
661675
parentType,
@@ -706,6 +720,12 @@ function executeFields(
706720
try {
707721
for (const [responseName, fieldDetailsList] of groupedFieldSet) {
708722
const fieldPath = addPath(path, responseName, parentType.name);
723+
724+
if (exeContext.abortSignal?.aborted) {
725+
// We might want to leverage a GraphQL error here
726+
throw new GraphQLError(exeContext.abortSignal.reason);
727+
}
728+
709729
const result = executeField(
710730
exeContext,
711731
parentType,
@@ -1069,6 +1089,11 @@ async function completePromisedValue(
10691089
if (isPromise(completed)) {
10701090
completed = await completed;
10711091
}
1092+
1093+
if (exeContext.abortSignal?.aborted) {
1094+
throw new GraphQLError(exeContext.abortSignal.reason);
1095+
}
1096+
10721097
return completed;
10731098
} catch (rawError) {
10741099
handleFieldError(

0 commit comments

Comments
 (0)