Skip to content

Commit 1f2c843

Browse files
robrichardlilianammmatosglasser
authored
Reference implementation of defer and stream spec (#3659)
Co-authored-by: Liliana Matos <[email protected]> Co-authored-by: David Glasser <[email protected]>
1 parent 29bf39f commit 1f2c843

29 files changed

+4887
-90
lines changed

src/execution/__tests__/defer-test.ts

+701
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import { expect } from 'chai';
2+
import { describe, it } from 'mocha';
3+
4+
import { flattenAsyncIterable } from '../flattenAsyncIterable';
5+
6+
describe('flattenAsyncIterable', () => {
7+
it('flatten nested async generators', async () => {
8+
async function* source() {
9+
yield await Promise.resolve(
10+
(async function* nested(): AsyncGenerator<number, void, void> {
11+
yield await Promise.resolve(1.1);
12+
yield await Promise.resolve(1.2);
13+
})(),
14+
);
15+
yield await Promise.resolve(
16+
(async function* nested(): AsyncGenerator<number, void, void> {
17+
yield await Promise.resolve(2.1);
18+
yield await Promise.resolve(2.2);
19+
})(),
20+
);
21+
}
22+
23+
const doubles = flattenAsyncIterable(source());
24+
25+
const result = [];
26+
for await (const x of doubles) {
27+
result.push(x);
28+
}
29+
expect(result).to.deep.equal([1.1, 1.2, 2.1, 2.2]);
30+
});
31+
32+
it('allows returning early from a nested async generator', async () => {
33+
async function* source() {
34+
yield await Promise.resolve(
35+
(async function* nested(): AsyncGenerator<number, void, void> {
36+
yield await Promise.resolve(1.1);
37+
yield await Promise.resolve(1.2);
38+
})(),
39+
);
40+
yield await Promise.resolve(
41+
(async function* nested(): AsyncGenerator<number, void, void> {
42+
yield await Promise.resolve(2.1); /* c8 ignore start */
43+
// Not reachable, early return
44+
yield await Promise.resolve(2.2);
45+
})(),
46+
);
47+
// Not reachable, early return
48+
yield await Promise.resolve(
49+
(async function* nested(): AsyncGenerator<number, void, void> {
50+
yield await Promise.resolve(3.1);
51+
yield await Promise.resolve(3.2);
52+
})(),
53+
);
54+
}
55+
/* c8 ignore stop */
56+
57+
const doubles = flattenAsyncIterable(source());
58+
59+
expect(await doubles.next()).to.deep.equal({ value: 1.1, done: false });
60+
expect(await doubles.next()).to.deep.equal({ value: 1.2, done: false });
61+
expect(await doubles.next()).to.deep.equal({ value: 2.1, done: false });
62+
63+
// Early return
64+
expect(await doubles.return()).to.deep.equal({
65+
value: undefined,
66+
done: true,
67+
});
68+
69+
// Subsequent next calls
70+
expect(await doubles.next()).to.deep.equal({
71+
value: undefined,
72+
done: true,
73+
});
74+
expect(await doubles.next()).to.deep.equal({
75+
value: undefined,
76+
done: true,
77+
});
78+
});
79+
80+
it('allows throwing errors from a nested async generator', async () => {
81+
async function* source() {
82+
yield await Promise.resolve(
83+
(async function* nested(): AsyncGenerator<number, void, void> {
84+
yield await Promise.resolve(1.1);
85+
yield await Promise.resolve(1.2);
86+
})(),
87+
);
88+
yield await Promise.resolve(
89+
(async function* nested(): AsyncGenerator<number, void, void> {
90+
yield await Promise.resolve(2.1); /* c8 ignore start */
91+
// Not reachable, early return
92+
yield await Promise.resolve(2.2);
93+
})(),
94+
);
95+
// Not reachable, early return
96+
yield await Promise.resolve(
97+
(async function* nested(): AsyncGenerator<number, void, void> {
98+
yield await Promise.resolve(3.1);
99+
yield await Promise.resolve(3.2);
100+
})(),
101+
);
102+
}
103+
/* c8 ignore stop */
104+
105+
const doubles = flattenAsyncIterable(source());
106+
107+
expect(await doubles.next()).to.deep.equal({ value: 1.1, done: false });
108+
expect(await doubles.next()).to.deep.equal({ value: 1.2, done: false });
109+
expect(await doubles.next()).to.deep.equal({ value: 2.1, done: false });
110+
111+
// Throw error
112+
let caughtError;
113+
try {
114+
await doubles.throw('ouch'); /* c8 ignore start */
115+
} catch (e) {
116+
caughtError = e;
117+
}
118+
expect(caughtError).to.equal('ouch');
119+
});
120+
it('completely yields sub-iterables even when next() called in parallel', async () => {
121+
async function* source() {
122+
yield await Promise.resolve(
123+
(async function* nested(): AsyncGenerator<number, void, void> {
124+
yield await Promise.resolve(1.1);
125+
yield await Promise.resolve(1.2);
126+
})(),
127+
);
128+
yield await Promise.resolve(
129+
(async function* nested(): AsyncGenerator<number, void, void> {
130+
yield await Promise.resolve(2.1);
131+
yield await Promise.resolve(2.2);
132+
})(),
133+
);
134+
}
135+
136+
const result = flattenAsyncIterable(source());
137+
138+
const promise1 = result.next();
139+
const promise2 = result.next();
140+
expect(await promise1).to.deep.equal({ value: 1.1, done: false });
141+
expect(await promise2).to.deep.equal({ value: 1.2, done: false });
142+
expect(await result.next()).to.deep.equal({ value: 2.1, done: false });
143+
expect(await result.next()).to.deep.equal({ value: 2.2, done: false });
144+
expect(await result.next()).to.deep.equal({
145+
value: undefined,
146+
done: true,
147+
});
148+
});
149+
});

src/execution/__tests__/mutations-test.ts

+141-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { expect } from 'chai';
1+
import { assert, expect } from 'chai';
22
import { describe, it } from 'mocha';
33

44
import { expectJSON } from '../../__testUtils__/expectJSON';
@@ -10,7 +10,11 @@ import { GraphQLObjectType } from '../../type/definition';
1010
import { GraphQLInt } from '../../type/scalars';
1111
import { GraphQLSchema } from '../../type/schema';
1212

13-
import { execute, executeSync } from '../execute';
13+
import {
14+
execute,
15+
executeSync,
16+
experimentalExecuteIncrementally,
17+
} from '../execute';
1418

1519
class NumberHolder {
1620
theNumber: number;
@@ -50,6 +54,13 @@ class Root {
5054
const numberHolderType = new GraphQLObjectType({
5155
fields: {
5256
theNumber: { type: GraphQLInt },
57+
promiseToGetTheNumber: {
58+
type: GraphQLInt,
59+
resolve: async (root) => {
60+
await new Promise((resolve) => setTimeout(resolve, 0));
61+
return root.theNumber;
62+
},
63+
},
5364
},
5465
name: 'NumberHolder',
5566
});
@@ -191,4 +202,132 @@ describe('Execute: Handles mutation execution ordering', () => {
191202
],
192203
});
193204
});
205+
it('Mutation fields with @defer do not block next mutation', async () => {
206+
const document = parse(`
207+
mutation M {
208+
first: promiseToChangeTheNumber(newNumber: 1) {
209+
...DeferFragment @defer(label: "defer-label")
210+
},
211+
second: immediatelyChangeTheNumber(newNumber: 2) {
212+
theNumber
213+
}
214+
}
215+
fragment DeferFragment on NumberHolder {
216+
promiseToGetTheNumber
217+
}
218+
`);
219+
220+
const rootValue = new Root(6);
221+
const mutationResult = await experimentalExecuteIncrementally({
222+
schema,
223+
document,
224+
rootValue,
225+
});
226+
const patches = [];
227+
228+
assert('initialResult' in mutationResult);
229+
patches.push(mutationResult.initialResult);
230+
for await (const patch of mutationResult.subsequentResults) {
231+
patches.push(patch);
232+
}
233+
234+
expect(patches).to.deep.equal([
235+
{
236+
data: {
237+
first: {},
238+
second: { theNumber: 2 },
239+
},
240+
hasNext: true,
241+
},
242+
{
243+
incremental: [
244+
{
245+
label: 'defer-label',
246+
path: ['first'],
247+
data: {
248+
promiseToGetTheNumber: 2,
249+
},
250+
},
251+
],
252+
hasNext: false,
253+
},
254+
]);
255+
});
256+
it('Mutation inside of a fragment', async () => {
257+
const document = parse(`
258+
mutation M {
259+
...MutationFragment
260+
second: immediatelyChangeTheNumber(newNumber: 2) {
261+
theNumber
262+
}
263+
}
264+
fragment MutationFragment on Mutation {
265+
first: promiseToChangeTheNumber(newNumber: 1) {
266+
theNumber
267+
},
268+
}
269+
`);
270+
271+
const rootValue = new Root(6);
272+
const mutationResult = await execute({ schema, document, rootValue });
273+
274+
expect(mutationResult).to.deep.equal({
275+
data: {
276+
first: { theNumber: 1 },
277+
second: { theNumber: 2 },
278+
},
279+
});
280+
});
281+
it('Mutation with @defer is not executed serially', async () => {
282+
const document = parse(`
283+
mutation M {
284+
...MutationFragment @defer(label: "defer-label")
285+
second: immediatelyChangeTheNumber(newNumber: 2) {
286+
theNumber
287+
}
288+
}
289+
fragment MutationFragment on Mutation {
290+
first: promiseToChangeTheNumber(newNumber: 1) {
291+
theNumber
292+
},
293+
}
294+
`);
295+
296+
const rootValue = new Root(6);
297+
const mutationResult = await experimentalExecuteIncrementally({
298+
schema,
299+
document,
300+
rootValue,
301+
});
302+
const patches = [];
303+
304+
assert('initialResult' in mutationResult);
305+
patches.push(mutationResult.initialResult);
306+
for await (const patch of mutationResult.subsequentResults) {
307+
patches.push(patch);
308+
}
309+
310+
expect(patches).to.deep.equal([
311+
{
312+
data: {
313+
second: { theNumber: 2 },
314+
},
315+
hasNext: true,
316+
},
317+
{
318+
incremental: [
319+
{
320+
label: 'defer-label',
321+
path: [],
322+
data: {
323+
first: {
324+
theNumber: 1,
325+
},
326+
},
327+
},
328+
],
329+
hasNext: false,
330+
},
331+
]);
332+
});
194333
});

src/execution/__tests__/nonnull-test.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { describe, it } from 'mocha';
33

44
import { expectJSON } from '../../__testUtils__/expectJSON';
55

6+
import type { PromiseOrValue } from '../../jsutils/PromiseOrValue';
7+
68
import { parse } from '../../language/parser';
79

810
import { GraphQLNonNull, GraphQLObjectType } from '../../type/definition';
@@ -109,7 +111,7 @@ const schema = buildSchema(`
109111
function executeQuery(
110112
query: string,
111113
rootValue: unknown,
112-
): ExecutionResult | Promise<ExecutionResult> {
114+
): PromiseOrValue<ExecutionResult> {
113115
return execute({ schema, document: parse(query), rootValue });
114116
}
115117

0 commit comments

Comments
 (0)