Skip to content

Commit 5cf4774

Browse files
committed
add failing tests
Currently: When list item completion fails for a stream with a non-nullable list: 1. the entire list must be nulled within the given AsyncPayloadRecord 2. any other pending AsyncPayloadRecords must be filtered 3. async iteration powering the stream must be cancelled Currently, the third objective is accomplished by way of the second; during AsyncPayloadRecord filtering, if a stream record is filtered and has an associated asyncIterator, its return() method is called, which _should_ end the stream. This can go wrong in a few ways: A: The return() method may not exist; by specification, the return() method exists for the caller to notify the callee that the caller no longer intends to call next(), allowing for early cleanup. The method is optional, however, and so should not be relied on. B: The return method, even if it exists, may not be set up to block any next() calls while it operates. Async generators have next and return methods that always settle in call order, but async iterables do not.
1 parent a6f75e6 commit 5cf4774

File tree

1 file changed

+145
-1
lines changed

1 file changed

+145
-1
lines changed

src/execution/__tests__/stream-test.ts

Lines changed: 145 additions & 1 deletion
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) {
@@ -1174,9 +1175,152 @@ describe('Execute: stream directive', () => {
11741175
],
11751176
},
11761177
],
1178+
hasNext: false,
1179+
},
1180+
]);
1181+
});
1182+
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 () => {
1183+
const document = parse(`
1184+
query {
1185+
nonNullFriendList @stream(initialCount: 1) {
1186+
nonNullName
1187+
}
1188+
}
1189+
`);
1190+
let count = 0;
1191+
const result = await complete(document, {
1192+
nonNullFriendList: {
1193+
[Symbol.asyncIterator]: () => ({
1194+
next: async () => {
1195+
switch (count++) {
1196+
case 0:
1197+
return Promise.resolve({
1198+
done: false,
1199+
value: { nonNullName: friends[0].name },
1200+
});
1201+
case 1:
1202+
return Promise.resolve({
1203+
done: false,
1204+
value: {
1205+
nonNullName: () => Promise.reject(new Error('Oops')),
1206+
},
1207+
});
1208+
case 2:
1209+
return Promise.resolve({
1210+
done: false,
1211+
value: { nonNullName: friends[1].name },
1212+
});
1213+
// Not reached
1214+
/* c8 ignore next 5 */
1215+
case 3:
1216+
return Promise.resolve({
1217+
done: false,
1218+
value: { nonNullName: friends[2].name },
1219+
});
1220+
}
1221+
},
1222+
}),
1223+
},
1224+
});
1225+
expectJSON(result).toDeepEqual([
1226+
{
1227+
data: {
1228+
nonNullFriendList: [{ nonNullName: 'Luke' }],
1229+
},
1230+
hasNext: true,
1231+
},
1232+
{
1233+
incremental: [
1234+
{
1235+
items: null,
1236+
path: ['nonNullFriendList', 1],
1237+
errors: [
1238+
{
1239+
message: 'Oops',
1240+
locations: [{ line: 4, column: 11 }],
1241+
path: ['nonNullFriendList', 1, 'nonNullName'],
1242+
},
1243+
],
1244+
},
1245+
],
1246+
hasNext: false,
1247+
},
1248+
]);
1249+
});
1250+
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 () => {
1251+
const document = parse(`
1252+
query {
1253+
nonNullFriendList @stream(initialCount: 1) {
1254+
nonNullName
1255+
}
1256+
}
1257+
`);
1258+
let count = 0;
1259+
let returned = false;
1260+
const result = await complete(document, {
1261+
nonNullFriendList: {
1262+
[Symbol.asyncIterator]: () => ({
1263+
next: async () => {
1264+
/* c8 ignore next 3 */
1265+
if (returned) {
1266+
return Promise.resolve({ done: true });
1267+
}
1268+
switch (count++) {
1269+
case 0:
1270+
return Promise.resolve({
1271+
done: false,
1272+
value: { nonNullName: friends[0].name },
1273+
});
1274+
case 1:
1275+
return Promise.resolve({
1276+
done: false,
1277+
value: {
1278+
nonNullName: () => Promise.reject(new Error('Oops')),
1279+
},
1280+
});
1281+
case 2:
1282+
return Promise.resolve({
1283+
done: false,
1284+
value: { nonNullName: friends[1].name },
1285+
});
1286+
// Not reached
1287+
/* c8 ignore next 5 */
1288+
case 3:
1289+
return Promise.resolve({
1290+
done: false,
1291+
value: { nonNullName: friends[2].name },
1292+
});
1293+
}
1294+
},
1295+
return: async () => {
1296+
await resolveOnNextTick();
1297+
returned = true;
1298+
return { done: true };
1299+
},
1300+
}),
1301+
},
1302+
});
1303+
expectJSON(result).toDeepEqual([
1304+
{
1305+
data: {
1306+
nonNullFriendList: [{ nonNullName: 'Luke' }],
1307+
},
11771308
hasNext: true,
11781309
},
11791310
{
1311+
incremental: [
1312+
{
1313+
items: null,
1314+
path: ['nonNullFriendList', 1],
1315+
errors: [
1316+
{
1317+
message: 'Oops',
1318+
locations: [{ line: 4, column: 11 }],
1319+
path: ['nonNullFriendList', 1, 'nonNullName'],
1320+
},
1321+
],
1322+
},
1323+
],
11801324
hasNext: false,
11811325
},
11821326
]);

0 commit comments

Comments
 (0)