Skip to content

Commit 6c0e52d

Browse files
committed
incremental delivery without branching
1 parent 74c6bd1 commit 6c0e52d

File tree

10 files changed

+2499
-461
lines changed

10 files changed

+2499
-461
lines changed

src/execution/__tests__/defer-test.ts

Lines changed: 1192 additions & 5 deletions
Large diffs are not rendered by default.

src/execution/__tests__/executor-test.ts

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ import { expectJSON } from '../../__testUtils__/expectJSON.js';
55
import { resolveOnNextTick } from '../../__testUtils__/resolveOnNextTick.js';
66

77
import { inspect } from '../../jsutils/inspect.js';
8+
import type { Path } from '../../jsutils/Path.js';
89

910
import { Kind } from '../../language/kinds.js';
1011
import { parse } from '../../language/parser.js';
1112

13+
import type { GraphQLResolveInfo } from '../../type/definition.js';
1214
import {
1315
GraphQLInterfaceType,
1416
GraphQLList,
@@ -191,7 +193,7 @@ describe('Execute: Handles basic execution tasks', () => {
191193
});
192194

193195
it('provides info about current execution state', () => {
194-
let resolvedInfo;
196+
let resolvedInfo: GraphQLResolveInfo | undefined;
195197
const testType = new GraphQLObjectType({
196198
name: 'Test',
197199
fields: {
@@ -239,13 +241,22 @@ describe('Execute: Handles basic execution tasks', () => {
239241
const field = operation.selectionSet.selections[0];
240242
expect(resolvedInfo).to.deep.include({
241243
fieldNodes: [field],
242-
path: { prev: undefined, key: 'result', typename: 'Test' },
243244
variableValues: { var: 'abc' },
244245
});
246+
247+
expect(resolvedInfo?.path).to.deep.include({
248+
prev: undefined,
249+
key: 'result',
250+
});
251+
252+
expect(resolvedInfo?.path.info).to.deep.include({
253+
parentType: testType,
254+
fieldName: 'test',
255+
});
245256
});
246257

247258
it('populates path correctly with complex types', () => {
248-
let path;
259+
let path: Path<unknown> | undefined;
249260
const someObject = new GraphQLObjectType({
250261
name: 'SomeObject',
251262
fields: {
@@ -288,18 +299,31 @@ describe('Execute: Handles basic execution tasks', () => {
288299

289300
executeSync({ schema, document, rootValue });
290301

291-
expect(path).to.deep.equal({
302+
expect(path).to.deep.include({
292303
key: 'l2',
293-
typename: 'SomeObject',
294-
prev: {
295-
key: 0,
296-
typename: undefined,
297-
prev: {
298-
key: 'l1',
299-
typename: 'SomeQuery',
300-
prev: undefined,
301-
},
302-
},
304+
});
305+
306+
expect(path?.info).to.deep.include({
307+
parentType: someObject,
308+
fieldName: 'test',
309+
});
310+
311+
expect(path?.prev).to.deep.include({
312+
key: 0,
313+
});
314+
315+
expect(path?.prev?.info).to.deep.include({
316+
parentType: testType,
317+
fieldName: 'test',
318+
});
319+
320+
expect(path?.prev?.prev).to.deep.include({
321+
key: 'l1',
322+
});
323+
324+
expect(path?.prev?.prev?.info).to.deep.include({
325+
parentType: testType,
326+
fieldName: 'test',
303327
});
304328
});
305329

src/execution/__tests__/stream-test.ts

Lines changed: 221 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { assert } from 'chai';
22
import { describe, it } from 'mocha';
33

44
import { expectJSON } from '../../__testUtils__/expectJSON.js';
5+
import { resolveOnNextTick } from '../../__testUtils__/resolveOnNextTick.js';
56

67
import type { PromiseOrValue } from '../../jsutils/PromiseOrValue.js';
78

@@ -1134,7 +1135,7 @@ describe('Execute: stream directive', () => {
11341135
},
11351136
]);
11361137
});
1137-
it('Handles async errors thrown by completeValue after initialCount is reached from async iterable for a non-nullable list', async () => {
1138+
it('Handles async errors thrown by completeValue after initialCount is reached from async generator for a non-nullable list', async () => {
11381139
const document = parse(`
11391140
query {
11401141
nonNullFriendList @stream(initialCount: 1) {
@@ -1181,6 +1182,158 @@ describe('Execute: stream directive', () => {
11811182
},
11821183
]);
11831184
});
1185+
it('Handles async errors thrown by completeValue after initialCount is reached from async iterable for a non-nullable list when the async iterable does not provide a return method) ', async () => {
1186+
const document = parse(`
1187+
query {
1188+
nonNullFriendList @stream(initialCount: 1) {
1189+
nonNullName
1190+
}
1191+
}
1192+
`);
1193+
let count = 0;
1194+
const result = await complete(document, {
1195+
nonNullFriendList: {
1196+
[Symbol.asyncIterator]: () => ({
1197+
next: async () => {
1198+
switch (count++) {
1199+
case 0:
1200+
return Promise.resolve({
1201+
done: false,
1202+
value: { nonNullName: friends[0].name },
1203+
});
1204+
case 1:
1205+
return Promise.resolve({
1206+
done: false,
1207+
value: {
1208+
nonNullName: () => Promise.reject(new Error('Oops')),
1209+
},
1210+
});
1211+
case 2:
1212+
return Promise.resolve({
1213+
done: false,
1214+
value: { nonNullName: friends[1].name },
1215+
});
1216+
// Not reached
1217+
/* c8 ignore next 5 */
1218+
case 3:
1219+
return Promise.resolve({
1220+
done: false,
1221+
value: { nonNullName: friends[2].name },
1222+
});
1223+
}
1224+
},
1225+
}),
1226+
},
1227+
});
1228+
expectJSON(result).toDeepEqual([
1229+
{
1230+
data: {
1231+
nonNullFriendList: [{ nonNullName: 'Luke' }],
1232+
},
1233+
hasNext: true,
1234+
},
1235+
{
1236+
incremental: [
1237+
{
1238+
items: null,
1239+
path: ['nonNullFriendList', 1],
1240+
errors: [
1241+
{
1242+
message: 'Oops',
1243+
locations: [{ line: 4, column: 11 }],
1244+
path: ['nonNullFriendList', 1, 'nonNullName'],
1245+
},
1246+
],
1247+
},
1248+
],
1249+
hasNext: true,
1250+
},
1251+
{
1252+
hasNext: false,
1253+
},
1254+
]);
1255+
});
1256+
it('Handles async errors thrown by completeValue after initialCount is reached from async iterable for a non-nullable list when the async iterable provides concurrent next/return methods and has a slow return ', async () => {
1257+
const document = parse(`
1258+
query {
1259+
nonNullFriendList @stream(initialCount: 1) {
1260+
nonNullName
1261+
}
1262+
}
1263+
`);
1264+
let count = 0;
1265+
let returned = false;
1266+
const result = await complete(document, {
1267+
nonNullFriendList: {
1268+
[Symbol.asyncIterator]: () => ({
1269+
next: async () => {
1270+
/* c8 ignore next 3 */
1271+
if (returned) {
1272+
return Promise.resolve({ done: true });
1273+
}
1274+
switch (count++) {
1275+
case 0:
1276+
return Promise.resolve({
1277+
done: false,
1278+
value: { nonNullName: friends[0].name },
1279+
});
1280+
case 1:
1281+
return Promise.resolve({
1282+
done: false,
1283+
value: {
1284+
nonNullName: () => Promise.reject(new Error('Oops')),
1285+
},
1286+
});
1287+
case 2:
1288+
return Promise.resolve({
1289+
done: false,
1290+
value: { nonNullName: friends[1].name },
1291+
});
1292+
// Not reached
1293+
/* c8 ignore next 5 */
1294+
case 3:
1295+
return Promise.resolve({
1296+
done: false,
1297+
value: { nonNullName: friends[2].name },
1298+
});
1299+
}
1300+
},
1301+
return: async () => {
1302+
await resolveOnNextTick();
1303+
returned = true;
1304+
return { done: true };
1305+
},
1306+
}),
1307+
},
1308+
});
1309+
expectJSON(result).toDeepEqual([
1310+
{
1311+
data: {
1312+
nonNullFriendList: [{ nonNullName: 'Luke' }],
1313+
},
1314+
hasNext: true,
1315+
},
1316+
{
1317+
incremental: [
1318+
{
1319+
items: null,
1320+
path: ['nonNullFriendList', 1],
1321+
errors: [
1322+
{
1323+
message: 'Oops',
1324+
locations: [{ line: 4, column: 11 }],
1325+
path: ['nonNullFriendList', 1, 'nonNullName'],
1326+
},
1327+
],
1328+
},
1329+
],
1330+
hasNext: true,
1331+
},
1332+
{
1333+
hasNext: false,
1334+
},
1335+
]);
1336+
});
11841337
it('Filters payloads that are nulled', async () => {
11851338
const document = parse(`
11861339
query {
@@ -1359,9 +1512,6 @@ describe('Execute: stream directive', () => {
13591512
],
13601513
},
13611514
],
1362-
hasNext: true,
1363-
},
1364-
{
13651515
hasNext: false,
13661516
},
13671517
]);
@@ -1421,10 +1571,11 @@ describe('Execute: stream directive', () => {
14211571
const iterable = {
14221572
[Symbol.asyncIterator]: () => ({
14231573
next: () => {
1574+
/* c8 ignore start */
14241575
if (requested) {
1425-
// Ignores further errors when filtered.
1576+
// stream is filtered, next is not called, and so this is not reached.
14261577
return Promise.reject(new Error('Oops'));
1427-
}
1578+
} /* c8 ignore stop */
14281579
requested = true;
14291580
const friend = friends[0];
14301581
return Promise.resolve({
@@ -1563,6 +1714,70 @@ describe('Execute: stream directive', () => {
15631714
},
15641715
]);
15651716
});
1717+
it('Handles overlapping deferred and non-deferred streams', async () => {
1718+
const document = parse(`
1719+
query {
1720+
nestedObject {
1721+
nestedFriendList @stream(initialCount: 0) {
1722+
id
1723+
}
1724+
}
1725+
nestedObject {
1726+
... @defer {
1727+
nestedFriendList @stream(initialCount: 0) {
1728+
id
1729+
name
1730+
}
1731+
}
1732+
}
1733+
}
1734+
`);
1735+
const result = await complete(document, {
1736+
nestedObject: {
1737+
async *nestedFriendList() {
1738+
yield await Promise.resolve(friends[0]);
1739+
yield await Promise.resolve(friends[1]);
1740+
},
1741+
},
1742+
});
1743+
expectJSON(result).toDeepEqual([
1744+
{
1745+
data: {
1746+
nestedObject: {
1747+
nestedFriendList: [],
1748+
},
1749+
},
1750+
hasNext: true,
1751+
},
1752+
{
1753+
incremental: [
1754+
{
1755+
data: {
1756+
nestedFriendList: [],
1757+
},
1758+
path: ['nestedObject'],
1759+
},
1760+
{
1761+
items: [{ id: '1', name: 'Luke' }],
1762+
path: ['nestedObject', 'nestedFriendList', 0],
1763+
},
1764+
],
1765+
hasNext: true,
1766+
},
1767+
{
1768+
incremental: [
1769+
{
1770+
items: [{ id: '2', name: 'Han' }],
1771+
path: ['nestedObject', 'nestedFriendList', 1],
1772+
},
1773+
],
1774+
hasNext: true,
1775+
},
1776+
{
1777+
hasNext: false,
1778+
},
1779+
]);
1780+
});
15661781
it('Returns payloads in correct order when parent deferred fragment resolves slower than stream', async () => {
15671782
const [slowFieldPromise, resolveSlowField] = createResolvablePromise();
15681783
const document = parse(`

0 commit comments

Comments
 (0)