Skip to content

Commit b9cdcb4

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. This PR adds tests addressing these scenarios, demonstrating test failures.
1 parent 7a609a2 commit b9cdcb4

File tree

1 file changed

+153
-1
lines changed

1 file changed

+153
-1
lines changed

src/execution/__tests__/stream-test.ts

Lines changed: 153 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { assert, expect } 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,157 @@ 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+
case 3:
1217+
return Promise.resolve({
1218+
done: false,
1219+
value: { nonNullName: friends[2].name },
1220+
});
1221+
default:
1222+
return Promise.resolve({ done: true });
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+
if (returned) {
1271+
return Promise.resolve({ done: true });
1272+
}
1273+
switch (count++) {
1274+
case 0:
1275+
return Promise.resolve({
1276+
done: false,
1277+
value: { nonNullName: friends[0].name },
1278+
});
1279+
case 1:
1280+
return Promise.resolve({
1281+
done: false,
1282+
value: {
1283+
nonNullName: () => Promise.reject(new Error('Oops')),
1284+
},
1285+
});
1286+
case 2:
1287+
return Promise.resolve({
1288+
done: false,
1289+
value: { nonNullName: friends[1].name },
1290+
});
1291+
case 3:
1292+
return Promise.resolve({
1293+
done: false,
1294+
value: { nonNullName: friends[2].name },
1295+
});
1296+
default:
1297+
return Promise.resolve({ done: true });
1298+
}
1299+
},
1300+
return: async () => {
1301+
await resolveOnNextTick();
1302+
returned = true;
1303+
return { done: true };
1304+
},
1305+
}),
1306+
},
1307+
});
1308+
expectJSON(result).toDeepEqual([
1309+
{
1310+
data: {
1311+
nonNullFriendList: [{ nonNullName: 'Luke' }],
1312+
},
1313+
hasNext: true,
1314+
},
1315+
{
1316+
incremental: [
1317+
{
1318+
items: null,
1319+
path: ['nonNullFriendList', 1],
1320+
errors: [
1321+
{
1322+
message: 'Oops',
1323+
locations: [{ line: 4, column: 11 }],
1324+
path: ['nonNullFriendList', 1, 'nonNullName'],
1325+
},
1326+
],
1327+
},
1328+
],
1329+
hasNext: true,
1330+
},
1331+
{
1332+
hasNext: false,
1333+
},
1334+
]);
1335+
});
11841336
it('Filters payloads that are nulled', async () => {
11851337
const document = parse(`
11861338
query {

0 commit comments

Comments
 (0)