Skip to content

Commit 1e58b67

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

File tree

2 files changed

+277
-9
lines changed

2 files changed

+277
-9
lines changed

src/execution/__tests__/stream-test.ts

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { assert } from 'chai';
12
import { describe, it } from 'mocha';
23

34
import { expectJSON } from '../../__testUtils__/expectJSON';
@@ -162,6 +163,37 @@ const query = new GraphQLObjectType({
162163
yield await Promise.resolve({ string: friends[1].name });
163164
},
164165
},
166+
asyncIterableListDelayed: {
167+
type: new GraphQLList(friendType),
168+
async *resolve() {
169+
for (const friend of friends) {
170+
// pause an additional ms before yielding to allow time
171+
// for tests to return or throw before next value is processed.
172+
// eslint-disable-next-line no-await-in-loop
173+
await resolveOnNextTick();
174+
yield friend; /* c8 ignore start */
175+
// Not reachable, early return
176+
}
177+
} /* c8 ignore stop */,
178+
},
179+
asyncIterableListNoReturn: {
180+
type: new GraphQLList(friendType),
181+
resolve() {
182+
let i = 0;
183+
return {
184+
[Symbol.asyncIterator]: () => ({
185+
async next() {
186+
const friend = friends[i++];
187+
if (friend) {
188+
await resolveOnNextTick();
189+
return { value: friend, done: false };
190+
}
191+
return { value: undefined, done: true };
192+
},
193+
}),
194+
};
195+
},
196+
},
165197
asyncIterableListDelayedClose: {
166198
type: new GraphQLList(friendType),
167199
async *resolve() {
@@ -1344,4 +1376,220 @@ describe('Execute: stream directive', () => {
13441376
},
13451377
]);
13461378
});
1379+
it('Returns underlying async iterables when returned generator is returned', async () => {
1380+
const document = parse(`
1381+
query {
1382+
asyncIterableListDelayed @stream(initialCount: 1) {
1383+
id
1384+
... @defer {
1385+
name
1386+
}
1387+
}
1388+
}
1389+
`);
1390+
const schema = new GraphQLSchema({ query });
1391+
1392+
const executeResult = await execute({ schema, document, rootValue: {} });
1393+
assert(isAsyncIterable(executeResult));
1394+
const iterator = executeResult[Symbol.asyncIterator]();
1395+
1396+
const result1 = await iterator.next();
1397+
expectJSON(result1).toDeepEqual({
1398+
done: false,
1399+
value: {
1400+
data: {
1401+
asyncIterableListDelayed: [
1402+
{
1403+
id: '1',
1404+
},
1405+
],
1406+
},
1407+
hasNext: true,
1408+
},
1409+
});
1410+
const returnPromise = iterator.return();
1411+
1412+
// these results had started processing before return was called
1413+
const result2 = await iterator.next();
1414+
expectJSON(result2).toDeepEqual({
1415+
done: false,
1416+
value: {
1417+
incremental: [
1418+
{
1419+
data: {
1420+
name: 'Luke',
1421+
},
1422+
path: ['asyncIterableListDelayed', 0],
1423+
},
1424+
],
1425+
hasNext: true,
1426+
},
1427+
});
1428+
const result3 = await iterator.next();
1429+
expectJSON(result3).toDeepEqual({
1430+
done: false,
1431+
value: {
1432+
incremental: [
1433+
{
1434+
items: [
1435+
{
1436+
id: '2',
1437+
},
1438+
],
1439+
path: ['asyncIterableListDelayed', 1],
1440+
},
1441+
],
1442+
hasNext: true,
1443+
},
1444+
});
1445+
const result4 = await iterator.next();
1446+
expectJSON(result4).toDeepEqual({
1447+
done: true,
1448+
value: undefined,
1449+
});
1450+
await returnPromise;
1451+
});
1452+
it('Can return async iterable when underlying iterable does not have a return method', async () => {
1453+
const document = parse(`
1454+
query {
1455+
asyncIterableListNoReturn @stream(initialCount: 1) {
1456+
name
1457+
id
1458+
}
1459+
}
1460+
`);
1461+
const schema = new GraphQLSchema({ query });
1462+
1463+
const executeResult = await execute({ schema, document, rootValue: {} });
1464+
assert(isAsyncIterable(executeResult));
1465+
const iterator = executeResult[Symbol.asyncIterator]();
1466+
1467+
const result1 = await iterator.next();
1468+
expectJSON(result1).toDeepEqual({
1469+
done: false,
1470+
value: {
1471+
data: {
1472+
asyncIterableListNoReturn: [
1473+
{
1474+
id: '1',
1475+
name: 'Luke',
1476+
},
1477+
],
1478+
},
1479+
hasNext: true,
1480+
},
1481+
});
1482+
1483+
const returnPromise = iterator.return();
1484+
1485+
// this result had started processing before return was called
1486+
const result2 = await iterator.next();
1487+
expectJSON(result2).toDeepEqual({
1488+
done: false,
1489+
value: {
1490+
incremental: [
1491+
{
1492+
items: [
1493+
{
1494+
id: '2',
1495+
name: 'Han',
1496+
},
1497+
],
1498+
path: ['asyncIterableListNoReturn', 1],
1499+
},
1500+
],
1501+
hasNext: true,
1502+
},
1503+
});
1504+
1505+
// third result is not returned because async iterator has returned
1506+
const result3 = await iterator.next();
1507+
expectJSON(result3).toDeepEqual({
1508+
done: true,
1509+
value: undefined,
1510+
});
1511+
await returnPromise;
1512+
});
1513+
it('Returns underlying async iterables when returned generator is thrown', async () => {
1514+
const document = parse(`
1515+
query {
1516+
asyncIterableListDelayed @stream(initialCount: 1) {
1517+
... @defer {
1518+
name
1519+
}
1520+
id
1521+
}
1522+
}
1523+
`);
1524+
const schema = new GraphQLSchema({ query });
1525+
1526+
const executeResult = await execute({ schema, document, rootValue: {} });
1527+
assert(isAsyncIterable(executeResult));
1528+
const iterator = executeResult[Symbol.asyncIterator]();
1529+
1530+
const result1 = await iterator.next();
1531+
expectJSON(result1).toDeepEqual({
1532+
done: false,
1533+
value: {
1534+
data: {
1535+
asyncIterableListDelayed: [
1536+
{
1537+
id: '1',
1538+
},
1539+
],
1540+
},
1541+
hasNext: true,
1542+
},
1543+
});
1544+
1545+
const throwPromise = iterator.throw(new Error('bad'));
1546+
1547+
// these results had started processing before return was called
1548+
const result2 = await iterator.next();
1549+
expectJSON(result2).toDeepEqual({
1550+
done: false,
1551+
value: {
1552+
incremental: [
1553+
{
1554+
data: {
1555+
name: 'Luke',
1556+
},
1557+
path: ['asyncIterableListDelayed', 0],
1558+
},
1559+
],
1560+
hasNext: true,
1561+
},
1562+
});
1563+
const result3 = await iterator.next();
1564+
expectJSON(result3).toDeepEqual({
1565+
done: false,
1566+
value: {
1567+
incremental: [
1568+
{
1569+
items: [
1570+
{
1571+
id: '2',
1572+
},
1573+
],
1574+
path: ['asyncIterableListDelayed', 1],
1575+
},
1576+
],
1577+
hasNext: true,
1578+
},
1579+
});
1580+
1581+
// this result is not returned because async iterator has returned
1582+
const result4 = await iterator.next();
1583+
expectJSON(result4).toDeepEqual({
1584+
done: true,
1585+
value: undefined,
1586+
});
1587+
try {
1588+
await throwPromise; /* c8 ignore start */
1589+
// Not reachable, always throws
1590+
/* c8 ignore stop */
1591+
} catch (e) {
1592+
// ignore error
1593+
}
1594+
});
13471595
});

src/execution/execute.ts

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1747,6 +1747,7 @@ async function executeStreamIterator(
17471747
label,
17481748
path: fieldPath,
17491749
parentContext,
1750+
iterator,
17501751
});
17511752

17521753
const dataPromise = executeStreamIteratorItem(
@@ -1789,6 +1790,7 @@ function yieldSubsequentPayloads(
17891790
initialResult: ExecutionResult,
17901791
): AsyncGenerator<AsyncExecutionResult, void, void> {
17911792
let _hasReturnedInitialResult = false;
1793+
let isDone = false;
17921794

17931795
async function race(): Promise<IteratorResult<AsyncExecutionResult>> {
17941796
if (exeContext.subsequentPayloads.length === 0) {
@@ -1866,19 +1868,37 @@ function yieldSubsequentPayloads(
18661868
},
18671869
done: false,
18681870
});
1869-
} else if (exeContext.subsequentPayloads.length === 0) {
1871+
} else if (exeContext.subsequentPayloads.length === 0 || isDone) {
18701872
return Promise.resolve({ value: undefined, done: true });
18711873
}
18721874
return race();
18731875
},
1874-
// TODO: implement return & throw
1875-
// c8 ignore next 2
1876-
// will be covered in follow up
1877-
return: () => Promise.resolve({ value: undefined, done: true }),
1878-
1879-
// c8 ignore next 2
1880-
// will be covered in follow up
1881-
throw: (error?: unknown) => Promise.reject(error),
1876+
async return(): Promise<IteratorResult<AsyncExecutionResult, void>> {
1877+
await Promise.all(
1878+
exeContext.subsequentPayloads.map((asyncPayloadRecord) => {
1879+
if (isStreamPayload(asyncPayloadRecord)) {
1880+
return asyncPayloadRecord.iterator?.return?.();
1881+
}
1882+
return undefined;
1883+
}),
1884+
);
1885+
isDone = true;
1886+
return { value: undefined, done: true };
1887+
},
1888+
async throw(
1889+
error?: unknown,
1890+
): Promise<IteratorResult<AsyncExecutionResult, void>> {
1891+
await Promise.all(
1892+
exeContext.subsequentPayloads.map((asyncPayloadRecord) => {
1893+
if (isStreamPayload(asyncPayloadRecord)) {
1894+
return asyncPayloadRecord.iterator?.return?.();
1895+
}
1896+
return undefined;
1897+
}),
1898+
);
1899+
isDone = true;
1900+
return Promise.reject(error);
1901+
},
18821902
};
18831903
}
18841904

0 commit comments

Comments
 (0)