Skip to content

Commit 0b7daed

Browse files
authored
fix(incrementalDelivery): fix null bubbling with async iterables (#3760)
1 parent a29d982 commit 0b7daed

File tree

2 files changed

+146
-47
lines changed

2 files changed

+146
-47
lines changed

src/execution/__tests__/stream-test.ts

Lines changed: 110 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { assert } from 'chai';
1+
import { assert, expect } from 'chai';
22
import { describe, it } from 'mocha';
33

44
import { expectJSON } from '../../__testUtils__/expectJSON.js';
@@ -851,6 +851,57 @@ describe('Execute: stream directive', () => {
851851
]);
852852
});
853853
it('Handles async errors thrown by completeValue after initialCount is reached', async () => {
854+
const document = parse(`
855+
query {
856+
friendList @stream(initialCount: 1) {
857+
nonNullName
858+
}
859+
}
860+
`);
861+
const result = await complete(document, {
862+
friendList: () => [
863+
Promise.resolve({ nonNullName: friends[0].name }),
864+
Promise.resolve({
865+
nonNullName: () => Promise.reject(new Error('Oops')),
866+
}),
867+
Promise.resolve({ nonNullName: friends[1].name }),
868+
],
869+
});
870+
expectJSON(result).toDeepEqual([
871+
{
872+
data: {
873+
friendList: [{ nonNullName: 'Luke' }],
874+
},
875+
hasNext: true,
876+
},
877+
{
878+
incremental: [
879+
{
880+
items: [null],
881+
path: ['friendList', 1],
882+
errors: [
883+
{
884+
message: 'Oops',
885+
locations: [{ line: 4, column: 11 }],
886+
path: ['friendList', 1, 'nonNullName'],
887+
},
888+
],
889+
},
890+
],
891+
hasNext: true,
892+
},
893+
{
894+
incremental: [
895+
{
896+
items: [{ nonNullName: 'Han' }],
897+
path: ['friendList', 2],
898+
},
899+
],
900+
hasNext: false,
901+
},
902+
]);
903+
});
904+
it('Handles async errors thrown by completeValue after initialCount is reached for a non-nullable list', async () => {
854905
const document = parse(`
855906
query {
856907
nonNullFriendList @stream(initialCount: 1) {
@@ -946,6 +997,50 @@ describe('Execute: stream directive', () => {
946997
},
947998
]);
948999
});
1000+
it('Handles async errors thrown by completeValue after initialCount is reached from async iterable for a non-nullable list', async () => {
1001+
const document = parse(`
1002+
query {
1003+
nonNullFriendList @stream(initialCount: 1) {
1004+
nonNullName
1005+
}
1006+
}
1007+
`);
1008+
const result = await complete(document, {
1009+
async *nonNullFriendList() {
1010+
yield await Promise.resolve({ nonNullName: friends[0].name });
1011+
yield await Promise.resolve({
1012+
nonNullName: () => Promise.reject(new Error('Oops')),
1013+
});
1014+
yield await Promise.resolve({
1015+
nonNullName: friends[1].name,
1016+
}); /* c8 ignore start */
1017+
} /* c8 ignore stop */,
1018+
});
1019+
expectJSON(result).toDeepEqual([
1020+
{
1021+
data: {
1022+
nonNullFriendList: [{ nonNullName: 'Luke' }],
1023+
},
1024+
hasNext: true,
1025+
},
1026+
{
1027+
incremental: [
1028+
{
1029+
items: null,
1030+
path: ['nonNullFriendList', 1],
1031+
errors: [
1032+
{
1033+
message: 'Oops',
1034+
locations: [{ line: 4, column: 11 }],
1035+
path: ['nonNullFriendList', 1, 'nonNullName'],
1036+
},
1037+
],
1038+
},
1039+
],
1040+
hasNext: false,
1041+
},
1042+
]);
1043+
});
9491044
it('Filters payloads that are nulled', async () => {
9501045
const document = parse(`
9511046
query {
@@ -961,8 +1056,8 @@ describe('Execute: stream directive', () => {
9611056
nestedObject: {
9621057
nonNullScalarField: () => Promise.resolve(null),
9631058
async *nestedFriendList() {
964-
yield await Promise.resolve(friends[0]);
965-
},
1059+
yield await Promise.resolve(friends[0]); /* c8 ignore start */
1060+
} /* c8 ignore stop */,
9661061
},
9671062
});
9681063
expectJSON(result).toDeepEqual({
@@ -1061,9 +1156,6 @@ describe('Execute: stream directive', () => {
10611156
path: ['nestedObject', 'nestedFriendList', 0],
10621157
},
10631158
],
1064-
hasNext: true,
1065-
},
1066-
{
10671159
hasNext: false,
10681160
},
10691161
]);
@@ -1088,8 +1180,8 @@ describe('Execute: stream directive', () => {
10881180
deeperNestedObject: {
10891181
nonNullScalarField: () => Promise.resolve(null),
10901182
async *deeperNestedFriendList() {
1091-
yield await Promise.resolve(friends[0]);
1092-
},
1183+
yield await Promise.resolve(friends[0]); /* c8 ignore start */
1184+
} /* c8 ignore stop */,
10931185
},
10941186
},
10951187
});
@@ -1176,14 +1268,17 @@ describe('Execute: stream directive', () => {
11761268

11771269
it('Returns iterator and ignores errors when stream payloads are filtered', async () => {
11781270
let returned = false;
1179-
let index = 0;
1271+
let requested = false;
11801272
const iterable = {
11811273
[Symbol.asyncIterator]: () => ({
11821274
next: () => {
1183-
const friend = friends[index++];
1184-
if (!friend) {
1185-
return Promise.resolve({ done: true, value: undefined });
1275+
if (requested) {
1276+
/* c8 ignore next 3 */
1277+
// Not reached, iterator should end immediately.
1278+
expect.fail('Not reached');
11861279
}
1280+
requested = true;
1281+
const friend = friends[0];
11871282
return Promise.resolve({
11881283
done: false,
11891284
value: {
@@ -1261,17 +1356,12 @@ describe('Execute: stream directive', () => {
12611356
],
12621357
},
12631358
],
1264-
hasNext: true,
1359+
hasNext: false,
12651360
},
12661361
});
1267-
const result3 = await iterator.next();
1268-
expectJSON(result3).toDeepEqual({
1269-
done: false,
1270-
value: { hasNext: false },
1271-
});
12721362

1273-
const result4 = await iterator.next();
1274-
expectJSON(result4).toDeepEqual({ done: true, value: undefined });
1363+
const result3 = await iterator.next();
1364+
expectJSON(result3).toDeepEqual({ done: true, value: undefined });
12751365

12761366
assert(returned);
12771367
});

src/execution/execute.ts

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2036,43 +2036,52 @@ async function executeStreamIterator(
20362036
exeContext,
20372037
});
20382038

2039-
const dataPromise = executeStreamIteratorItem(
2040-
iterator,
2041-
exeContext,
2042-
fieldNodes,
2043-
info,
2044-
itemType,
2045-
asyncPayloadRecord,
2046-
itemPath,
2047-
);
2048-
2049-
asyncPayloadRecord.addItems(
2050-
dataPromise
2051-
.then(({ value }) => value)
2052-
.then(
2053-
(value) => [value],
2054-
(err) => {
2055-
asyncPayloadRecord.errors.push(err);
2056-
return null;
2057-
},
2058-
),
2059-
);
2039+
let iteration;
20602040
try {
20612041
// eslint-disable-next-line no-await-in-loop
2062-
const { done } = await dataPromise;
2063-
if (done) {
2064-
break;
2065-
}
2066-
} catch (err) {
2067-
// entire stream has errored and bubbled upwards
2042+
iteration = await executeStreamIteratorItem(
2043+
iterator,
2044+
exeContext,
2045+
fieldNodes,
2046+
info,
2047+
itemType,
2048+
asyncPayloadRecord,
2049+
itemPath,
2050+
);
2051+
} catch (error) {
2052+
asyncPayloadRecord.errors.push(error);
20682053
filterSubsequentPayloads(exeContext, path, asyncPayloadRecord);
2054+
asyncPayloadRecord.addItems(null);
2055+
// entire stream has errored and bubbled upwards
20692056
if (iterator?.return) {
20702057
iterator.return().catch(() => {
20712058
// ignore errors
20722059
});
20732060
}
20742061
return;
20752062
}
2063+
2064+
const { done, value: completedItem } = iteration;
2065+
2066+
let completedItems: PromiseOrValue<Array<unknown> | null>;
2067+
if (isPromise(completedItem)) {
2068+
completedItems = completedItem.then(
2069+
(value) => [value],
2070+
(error) => {
2071+
asyncPayloadRecord.errors.push(error);
2072+
filterSubsequentPayloads(exeContext, path, asyncPayloadRecord);
2073+
return null;
2074+
},
2075+
);
2076+
} else {
2077+
completedItems = [completedItem];
2078+
}
2079+
2080+
asyncPayloadRecord.addItems(completedItems);
2081+
2082+
if (done) {
2083+
break;
2084+
}
20762085
previousAsyncPayloadRecord = asyncPayloadRecord;
20772086
index++;
20782087
}

0 commit comments

Comments
 (0)