Skip to content

Commit 559975e

Browse files
committed
Return underlying AsyncIterators when execute result is returned (#2843)
# Conflicts: # src/execution/execute.ts
1 parent 6fac720 commit 559975e

File tree

2 files changed

+231
-6
lines changed

2 files changed

+231
-6
lines changed

src/execution/__tests__/stream-test.ts

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

33
import { expectJSON } from '../../__testUtils__/expectJSON';
44

5+
import { invariant } from '../../jsutils/invariant';
56
import { isAsyncIterable } from '../../jsutils/isAsyncIterable';
67

78
import type { DocumentNode } from '../../language/ast';
@@ -161,6 +162,37 @@ const query = new GraphQLObjectType({
161162
yield await Promise.resolve({ string: friends[1].name });
162163
},
163164
},
165+
asyncIterableListDelayed: {
166+
type: new GraphQLList(friendType),
167+
async *resolve() {
168+
for (const friend of friends) {
169+
// pause an additional ms before yielding to allow time
170+
// for tests to return or throw before next value is processed.
171+
// eslint-disable-next-line no-await-in-loop
172+
await new Promise((r) => setTimeout(r, 1));
173+
yield friend; /* c8 ignore start */
174+
// Not reachable, early return
175+
}
176+
} /* c8 ignore stop */,
177+
},
178+
asyncIterableListNoReturn: {
179+
type: new GraphQLList(friendType),
180+
resolve() {
181+
let i = 0;
182+
return {
183+
[Symbol.asyncIterator]: () => ({
184+
async next() {
185+
const friend = friends[i++];
186+
if (friend) {
187+
await new Promise((r) => setTimeout(r, 1));
188+
return { value: friend, done: false };
189+
}
190+
return { value: undefined, done: true };
191+
},
192+
}),
193+
};
194+
},
195+
},
164196
asyncIterableListDelayedClose: {
165197
type: new GraphQLList(friendType),
166198
async *resolve() {
@@ -1188,4 +1220,181 @@ describe('Execute: stream directive', () => {
11881220
},
11891221
]);
11901222
});
1223+
it('Returns underlying async iterables when dispatcher is returned', async () => {
1224+
const document = parse(`
1225+
query {
1226+
asyncIterableListDelayed @stream(initialCount: 1) {
1227+
name
1228+
id
1229+
}
1230+
}
1231+
`);
1232+
const schema = new GraphQLSchema({ query });
1233+
1234+
const executeResult = await execute({ schema, document, rootValue: {} });
1235+
invariant(isAsyncIterable(executeResult));
1236+
const iterator = executeResult[Symbol.asyncIterator]();
1237+
1238+
const result1 = await iterator.next();
1239+
expectJSON(result1).toDeepEqual({
1240+
done: false,
1241+
value: {
1242+
data: {
1243+
asyncIterableListDelayed: [
1244+
{
1245+
id: '1',
1246+
name: 'Luke',
1247+
},
1248+
],
1249+
},
1250+
hasNext: true,
1251+
},
1252+
});
1253+
1254+
const returnPromise = iterator.return();
1255+
1256+
// this result had started processing before return was called
1257+
const result2 = await iterator.next();
1258+
expectJSON(result2).toDeepEqual({
1259+
done: false,
1260+
value: {
1261+
data: [
1262+
{
1263+
id: '2',
1264+
name: 'Han',
1265+
},
1266+
],
1267+
hasNext: true,
1268+
path: ['asyncIterableListDelayed', 1],
1269+
},
1270+
});
1271+
1272+
// third result is not returned because async iterator has returned
1273+
const result3 = await iterator.next();
1274+
expectJSON(result3).toDeepEqual({
1275+
done: true,
1276+
value: undefined,
1277+
});
1278+
await returnPromise;
1279+
});
1280+
it('Can return async iterable when underlying iterable does not have a return method', async () => {
1281+
const document = parse(`
1282+
query {
1283+
asyncIterableListNoReturn @stream(initialCount: 1) {
1284+
name
1285+
id
1286+
}
1287+
}
1288+
`);
1289+
const schema = new GraphQLSchema({ query });
1290+
1291+
const executeResult = await execute({ schema, document, rootValue: {} });
1292+
invariant(isAsyncIterable(executeResult));
1293+
const iterator = executeResult[Symbol.asyncIterator]();
1294+
1295+
const result1 = await iterator.next();
1296+
expectJSON(result1).toDeepEqual({
1297+
done: false,
1298+
value: {
1299+
data: {
1300+
asyncIterableListNoReturn: [
1301+
{
1302+
id: '1',
1303+
name: 'Luke',
1304+
},
1305+
],
1306+
},
1307+
hasNext: true,
1308+
},
1309+
});
1310+
1311+
const returnPromise = iterator.return();
1312+
1313+
// this result had started processing before return was called
1314+
const result2 = await iterator.next();
1315+
expectJSON(result2).toDeepEqual({
1316+
done: false,
1317+
value: {
1318+
data: [
1319+
{
1320+
id: '2',
1321+
name: 'Han',
1322+
},
1323+
],
1324+
hasNext: true,
1325+
path: ['asyncIterableListNoReturn', 1],
1326+
},
1327+
});
1328+
1329+
// third result is not returned because async iterator has returned
1330+
const result3 = await iterator.next();
1331+
expectJSON(result3).toDeepEqual({
1332+
done: true,
1333+
value: undefined,
1334+
});
1335+
await returnPromise;
1336+
});
1337+
it('Returns underlying async iterables when dispatcher is thrown', async () => {
1338+
const document = parse(`
1339+
query {
1340+
asyncIterableListDelayed @stream(initialCount: 1) {
1341+
name
1342+
id
1343+
}
1344+
}
1345+
`);
1346+
const schema = new GraphQLSchema({ query });
1347+
1348+
const executeResult = await execute({ schema, document, rootValue: {} });
1349+
invariant(isAsyncIterable(executeResult));
1350+
const iterator = executeResult[Symbol.asyncIterator]();
1351+
1352+
const result1 = await iterator.next();
1353+
expectJSON(result1).toDeepEqual({
1354+
done: false,
1355+
value: {
1356+
data: {
1357+
asyncIterableListDelayed: [
1358+
{
1359+
id: '1',
1360+
name: 'Luke',
1361+
},
1362+
],
1363+
},
1364+
hasNext: true,
1365+
},
1366+
});
1367+
1368+
const throwPromise = iterator.throw(new Error('bad'));
1369+
1370+
// this result had started processing before return was called
1371+
const result2 = await iterator.next();
1372+
expectJSON(result2).toDeepEqual({
1373+
done: false,
1374+
value: {
1375+
data: [
1376+
{
1377+
id: '2',
1378+
name: 'Han',
1379+
},
1380+
],
1381+
hasNext: true,
1382+
path: ['asyncIterableListDelayed', 1],
1383+
},
1384+
});
1385+
1386+
// third result is not returned because async iterator has returned
1387+
const result3 = await iterator.next();
1388+
expectJSON(result3).toDeepEqual({
1389+
done: true,
1390+
value: undefined,
1391+
});
1392+
try {
1393+
await throwPromise; /* c8 ignore start */
1394+
// Not reachable, always throws
1395+
/* c8 ignore stop */
1396+
} catch (e) {
1397+
// ignore error
1398+
}
1399+
});
11911400
});

src/execution/execute.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1574,6 +1574,7 @@ async function executeStreamIterator(
15741574
label,
15751575
path: fieldPath,
15761576
parentContext,
1577+
iterator,
15771578
});
15781579

15791580
const dataPromise = executeStreamIteratorItem(
@@ -1616,6 +1617,7 @@ function yieldSubsequentPayloads(
16161617
initialResult: ExecutionResult,
16171618
): AsyncGenerator<AsyncExecutionResult, void, void> {
16181619
let _hasReturnedInitialResult = false;
1620+
let isDone = false;
16191621

16201622
async function race(): Promise<IteratorResult<AsyncExecutionResult>> {
16211623
if (exeContext.subsequentPayloads.length === 0) {
@@ -1686,17 +1688,31 @@ function yieldSubsequentPayloads(
16861688
},
16871689
done: false,
16881690
});
1689-
} else if (exeContext.subsequentPayloads.length === 0) {
1691+
} else if (exeContext.subsequentPayloads.length === 0 || isDone) {
16901692
return Promise.resolve({ value: undefined, done: true });
16911693
}
16921694
return race();
16931695
},
1694-
// TODO: implement return & throw
1695-
return: /* istanbul ignore next: will be covered in follow up */ () =>
1696-
Promise.resolve({ value: undefined, done: true }),
1697-
throw: /* istanbul ignore next: will be covered in follow up */ (
1696+
async return(): Promise<IteratorResult<AsyncExecutionResult, void>> {
1697+
await Promise.all(
1698+
exeContext.subsequentPayloads.map((asyncPayloadRecord) =>
1699+
asyncPayloadRecord.iterator?.return?.(),
1700+
),
1701+
);
1702+
isDone = true;
1703+
return { value: undefined, done: true };
1704+
},
1705+
async throw(
16981706
error?: unknown,
1699-
) => Promise.reject(error),
1707+
): Promise<IteratorResult<AsyncExecutionResult, void>> {
1708+
await Promise.all(
1709+
exeContext.subsequentPayloads.map((asyncPayloadRecord) =>
1710+
asyncPayloadRecord.iterator?.return?.(),
1711+
),
1712+
);
1713+
isDone = true;
1714+
return Promise.reject(error);
1715+
},
17001716
};
17011717
}
17021718

0 commit comments

Comments
 (0)