Skip to content

Commit 3bdb787

Browse files
committed
Return underlying AsyncIterators when execute result is returned (#2843)
# Conflicts: # src/execution/execute.ts
1 parent ccbf236 commit 3bdb787

File tree

2 files changed

+251
-9
lines changed

2 files changed

+251
-9
lines changed

src/execution/__tests__/stream-test.ts

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1215,4 +1215,226 @@ describe('Execute: stream directive', () => {
12151215
done: true,
12161216
});
12171217
});
1218+
it('Returns underlying async iterables when returned generator is returned', async () => {
1219+
let returned = false;
1220+
let index = 0;
1221+
const iterable = {
1222+
[Symbol.asyncIterator]: () => ({
1223+
next: () => {
1224+
const friend = friends[index++];
1225+
if (!friend) {
1226+
return Promise.resolve({ done: true, value: undefined });
1227+
}
1228+
return Promise.resolve({ done: false, value: friend });
1229+
},
1230+
return: () => {
1231+
returned = true;
1232+
},
1233+
}),
1234+
};
1235+
1236+
const document = parse(`
1237+
query {
1238+
friendList @stream(initialCount: 1) {
1239+
id
1240+
... @defer {
1241+
name
1242+
}
1243+
}
1244+
}
1245+
`);
1246+
1247+
const executeResult = await execute({
1248+
schema,
1249+
document,
1250+
rootValue: {
1251+
friendList: iterable,
1252+
},
1253+
});
1254+
assert(isAsyncIterable(executeResult));
1255+
const iterator = executeResult[Symbol.asyncIterator]();
1256+
1257+
const result1 = await iterator.next();
1258+
expectJSON(result1).toDeepEqual({
1259+
done: false,
1260+
value: {
1261+
data: {
1262+
friendList: [{ id: '1' }],
1263+
},
1264+
hasNext: true,
1265+
},
1266+
});
1267+
const returnPromise = iterator.return();
1268+
1269+
// these results had started processing before return was called
1270+
const result2 = await iterator.next();
1271+
expectJSON(result2).toDeepEqual({
1272+
done: false,
1273+
value: {
1274+
incremental: [
1275+
{
1276+
data: { name: 'Luke' },
1277+
path: ['friendList', 0],
1278+
},
1279+
],
1280+
hasNext: true,
1281+
},
1282+
});
1283+
const result3 = await iterator.next();
1284+
expectJSON(result3).toDeepEqual({
1285+
done: true,
1286+
value: undefined,
1287+
});
1288+
await returnPromise;
1289+
assert(returned);
1290+
});
1291+
it('Can return async iterable when underlying iterable does not have a return method', async () => {
1292+
let index = 0;
1293+
const iterable = {
1294+
[Symbol.asyncIterator]: () => ({
1295+
next: () => {
1296+
const friend = friends[index++];
1297+
if (!friend) {
1298+
return Promise.resolve({ done: true, value: undefined });
1299+
}
1300+
return Promise.resolve({ done: false, value: friend });
1301+
},
1302+
}),
1303+
};
1304+
1305+
const document = parse(`
1306+
query {
1307+
friendList @stream(initialCount: 1) {
1308+
name
1309+
id
1310+
}
1311+
}
1312+
`);
1313+
1314+
const executeResult = await execute({
1315+
schema,
1316+
document,
1317+
rootValue: {
1318+
friendList: iterable,
1319+
},
1320+
});
1321+
assert(isAsyncIterable(executeResult));
1322+
const iterator = executeResult[Symbol.asyncIterator]();
1323+
1324+
const result1 = await iterator.next();
1325+
expectJSON(result1).toDeepEqual({
1326+
done: false,
1327+
value: {
1328+
data: {
1329+
friendList: [{ id: '1', name: 'Luke' }],
1330+
},
1331+
hasNext: true,
1332+
},
1333+
});
1334+
1335+
const returnPromise = iterator.return();
1336+
1337+
// this result had started processing before return was called
1338+
const result2 = await iterator.next();
1339+
expectJSON(result2).toDeepEqual({
1340+
done: false,
1341+
value: {
1342+
incremental: [
1343+
{
1344+
items: [{ id: '2', name: 'Han' }],
1345+
path: ['friendList', 1],
1346+
},
1347+
],
1348+
hasNext: true,
1349+
},
1350+
});
1351+
1352+
// third result is not returned because async iterator has returned
1353+
const result3 = await iterator.next();
1354+
expectJSON(result3).toDeepEqual({
1355+
done: true,
1356+
value: undefined,
1357+
});
1358+
await returnPromise;
1359+
});
1360+
it('Returns underlying async iterables when returned generator is thrown', async () => {
1361+
let index = 0;
1362+
let returned = false;
1363+
const iterable = {
1364+
[Symbol.asyncIterator]: () => ({
1365+
next: () => {
1366+
const friend = friends[index++];
1367+
if (!friend) {
1368+
return Promise.resolve({ done: true, value: undefined });
1369+
}
1370+
return Promise.resolve({ done: false, value: friend });
1371+
},
1372+
return: () => {
1373+
returned = true;
1374+
},
1375+
}),
1376+
};
1377+
const document = parse(`
1378+
query {
1379+
friendList @stream(initialCount: 1) {
1380+
... @defer {
1381+
name
1382+
}
1383+
id
1384+
}
1385+
}
1386+
`);
1387+
const executeResult = await execute({
1388+
schema,
1389+
document,
1390+
rootValue: {
1391+
friendList: iterable,
1392+
},
1393+
});
1394+
assert(isAsyncIterable(executeResult));
1395+
const iterator = executeResult[Symbol.asyncIterator]();
1396+
1397+
const result1 = await iterator.next();
1398+
expectJSON(result1).toDeepEqual({
1399+
done: false,
1400+
value: {
1401+
data: {
1402+
friendList: [{ id: '1' }],
1403+
},
1404+
hasNext: true,
1405+
},
1406+
});
1407+
1408+
const throwPromise = iterator.throw(new Error('bad'));
1409+
1410+
// these results had started processing before return was called
1411+
const result2 = await iterator.next();
1412+
expectJSON(result2).toDeepEqual({
1413+
done: false,
1414+
value: {
1415+
incremental: [
1416+
{
1417+
data: { name: 'Luke' },
1418+
path: ['friendList', 0],
1419+
},
1420+
],
1421+
hasNext: true,
1422+
},
1423+
});
1424+
1425+
// this result is not returned because async iterator has returned
1426+
const result3 = await iterator.next();
1427+
expectJSON(result3).toDeepEqual({
1428+
done: true,
1429+
value: undefined,
1430+
});
1431+
try {
1432+
await throwPromise; /* c8 ignore start */
1433+
// Not reachable, always throws
1434+
/* c8 ignore stop */
1435+
} catch (e) {
1436+
// ignore error
1437+
}
1438+
assert(returned);
1439+
});
12181440
});

src/execution/execute.ts

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1750,6 +1750,7 @@ async function executeStreamIterator(
17501750
label,
17511751
path: fieldPath,
17521752
parentContext: previousAsyncPayloadRecord,
1753+
iterator,
17531754
});
17541755

17551756
const dataPromise = executeStreamIteratorItem(
@@ -1793,6 +1794,7 @@ function yieldSubsequentPayloads(
17931794
initialResult: ExecutionResult,
17941795
): AsyncGenerator<AsyncExecutionResult, void, void> {
17951796
let _hasReturnedInitialResult = false;
1797+
let isDone = false;
17961798

17971799
async function race(): Promise<IteratorResult<AsyncExecutionResult>> {
17981800
if (exeContext.subsequentPayloads.length === 0) {
@@ -1870,19 +1872,37 @@ function yieldSubsequentPayloads(
18701872
},
18711873
done: false,
18721874
});
1873-
} else if (exeContext.subsequentPayloads.length === 0) {
1875+
} else if (exeContext.subsequentPayloads.length === 0 || isDone) {
18741876
return Promise.resolve({ value: undefined, done: true });
18751877
}
18761878
return race();
18771879
},
1878-
// TODO: implement return & throw
1879-
// c8 ignore next 2
1880-
// will be covered in follow up
1881-
return: () => Promise.resolve({ value: undefined, done: true }),
1882-
1883-
// c8 ignore next 2
1884-
// will be covered in follow up
1885-
throw: (error?: unknown) => Promise.reject(error),
1880+
async return(): Promise<IteratorResult<AsyncExecutionResult, void>> {
1881+
await Promise.all(
1882+
exeContext.subsequentPayloads.map((asyncPayloadRecord) => {
1883+
if (isStreamPayload(asyncPayloadRecord)) {
1884+
return asyncPayloadRecord.iterator?.return?.();
1885+
}
1886+
return undefined;
1887+
}),
1888+
);
1889+
isDone = true;
1890+
return { value: undefined, done: true };
1891+
},
1892+
async throw(
1893+
error?: unknown,
1894+
): Promise<IteratorResult<AsyncExecutionResult, void>> {
1895+
await Promise.all(
1896+
exeContext.subsequentPayloads.map((asyncPayloadRecord) => {
1897+
if (isStreamPayload(asyncPayloadRecord)) {
1898+
return asyncPayloadRecord.iterator?.return?.();
1899+
}
1900+
return undefined;
1901+
}),
1902+
);
1903+
isDone = true;
1904+
return Promise.reject(error);
1905+
},
18861906
};
18871907
}
18881908

0 commit comments

Comments
 (0)