Skip to content

Commit 0df949a

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

File tree

2 files changed

+232
-9
lines changed

2 files changed

+232
-9
lines changed

src/execution/__tests__/stream-test.ts

Lines changed: 209 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() {
@@ -1189,4 +1221,181 @@ describe('Execute: stream directive', () => {
11891221
},
11901222
]);
11911223
});
1224+
it('Returns underlying async iterables when dispatcher is returned', async () => {
1225+
const document = parse(`
1226+
query {
1227+
asyncIterableListDelayed @stream(initialCount: 1) {
1228+
name
1229+
id
1230+
}
1231+
}
1232+
`);
1233+
const schema = new GraphQLSchema({ query });
1234+
1235+
const executeResult = await execute({ schema, document, rootValue: {} });
1236+
assert(isAsyncIterable(executeResult));
1237+
const iterator = executeResult[Symbol.asyncIterator]();
1238+
1239+
const result1 = await iterator.next();
1240+
expectJSON(result1).toDeepEqual({
1241+
done: false,
1242+
value: {
1243+
data: {
1244+
asyncIterableListDelayed: [
1245+
{
1246+
id: '1',
1247+
name: 'Luke',
1248+
},
1249+
],
1250+
},
1251+
hasNext: true,
1252+
},
1253+
});
1254+
1255+
const returnPromise = iterator.return();
1256+
1257+
// this result had started processing before return was called
1258+
const result2 = await iterator.next();
1259+
expectJSON(result2).toDeepEqual({
1260+
done: false,
1261+
value: {
1262+
data: [
1263+
{
1264+
id: '2',
1265+
name: 'Han',
1266+
},
1267+
],
1268+
hasNext: true,
1269+
path: ['asyncIterableListDelayed', 1],
1270+
},
1271+
});
1272+
1273+
// third result is not returned because async iterator has returned
1274+
const result3 = await iterator.next();
1275+
expectJSON(result3).toDeepEqual({
1276+
done: true,
1277+
value: undefined,
1278+
});
1279+
await returnPromise;
1280+
});
1281+
it('Can return async iterable when underlying iterable does not have a return method', async () => {
1282+
const document = parse(`
1283+
query {
1284+
asyncIterableListNoReturn @stream(initialCount: 1) {
1285+
name
1286+
id
1287+
}
1288+
}
1289+
`);
1290+
const schema = new GraphQLSchema({ query });
1291+
1292+
const executeResult = await execute({ schema, document, rootValue: {} });
1293+
assert(isAsyncIterable(executeResult));
1294+
const iterator = executeResult[Symbol.asyncIterator]();
1295+
1296+
const result1 = await iterator.next();
1297+
expectJSON(result1).toDeepEqual({
1298+
done: false,
1299+
value: {
1300+
data: {
1301+
asyncIterableListNoReturn: [
1302+
{
1303+
id: '1',
1304+
name: 'Luke',
1305+
},
1306+
],
1307+
},
1308+
hasNext: true,
1309+
},
1310+
});
1311+
1312+
const returnPromise = iterator.return();
1313+
1314+
// this result had started processing before return was called
1315+
const result2 = await iterator.next();
1316+
expectJSON(result2).toDeepEqual({
1317+
done: false,
1318+
value: {
1319+
data: [
1320+
{
1321+
id: '2',
1322+
name: 'Han',
1323+
},
1324+
],
1325+
hasNext: true,
1326+
path: ['asyncIterableListNoReturn', 1],
1327+
},
1328+
});
1329+
1330+
// third result is not returned because async iterator has returned
1331+
const result3 = await iterator.next();
1332+
expectJSON(result3).toDeepEqual({
1333+
done: true,
1334+
value: undefined,
1335+
});
1336+
await returnPromise;
1337+
});
1338+
it('Returns underlying async iterables when dispatcher is thrown', async () => {
1339+
const document = parse(`
1340+
query {
1341+
asyncIterableListDelayed @stream(initialCount: 1) {
1342+
name
1343+
id
1344+
}
1345+
}
1346+
`);
1347+
const schema = new GraphQLSchema({ query });
1348+
1349+
const executeResult = await execute({ schema, document, rootValue: {} });
1350+
assert(isAsyncIterable(executeResult));
1351+
const iterator = executeResult[Symbol.asyncIterator]();
1352+
1353+
const result1 = await iterator.next();
1354+
expectJSON(result1).toDeepEqual({
1355+
done: false,
1356+
value: {
1357+
data: {
1358+
asyncIterableListDelayed: [
1359+
{
1360+
id: '1',
1361+
name: 'Luke',
1362+
},
1363+
],
1364+
},
1365+
hasNext: true,
1366+
},
1367+
});
1368+
1369+
const throwPromise = iterator.throw(new Error('bad'));
1370+
1371+
// this result had started processing before return was called
1372+
const result2 = await iterator.next();
1373+
expectJSON(result2).toDeepEqual({
1374+
done: false,
1375+
value: {
1376+
data: [
1377+
{
1378+
id: '2',
1379+
name: 'Han',
1380+
},
1381+
],
1382+
hasNext: true,
1383+
path: ['asyncIterableListDelayed', 1],
1384+
},
1385+
});
1386+
1387+
// third result is not returned because async iterator has returned
1388+
const result3 = await iterator.next();
1389+
expectJSON(result3).toDeepEqual({
1390+
done: true,
1391+
value: undefined,
1392+
});
1393+
try {
1394+
await throwPromise; /* c8 ignore start */
1395+
// Not reachable, always throws
1396+
/* c8 ignore stop */
1397+
} catch (e) {
1398+
// ignore error
1399+
}
1400+
});
11921401
});

src/execution/execute.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1710,6 +1710,7 @@ async function executeStreamIterator(
17101710
label,
17111711
path: fieldPath,
17121712
parentContext,
1713+
iterator,
17131714
});
17141715

17151716
const dataPromise = executeStreamIteratorItem(
@@ -1752,6 +1753,7 @@ function yieldSubsequentPayloads(
17521753
initialResult: ExecutionResult,
17531754
): AsyncGenerator<AsyncExecutionResult, void, void> {
17541755
let _hasReturnedInitialResult = false;
1756+
let isDone = false;
17551757

17561758
async function race(): Promise<IteratorResult<AsyncExecutionResult>> {
17571759
if (exeContext.subsequentPayloads.length === 0) {
@@ -1822,19 +1824,31 @@ function yieldSubsequentPayloads(
18221824
},
18231825
done: false,
18241826
});
1825-
} else if (exeContext.subsequentPayloads.length === 0) {
1827+
} else if (exeContext.subsequentPayloads.length === 0 || isDone) {
18261828
return Promise.resolve({ value: undefined, done: true });
18271829
}
18281830
return race();
18291831
},
1830-
// TODO: implement return & throw
1831-
// c8 ignore next 2
1832-
// will be covered in follow up
1833-
return: () => Promise.resolve({ value: undefined, done: true }),
1834-
1835-
// c8 ignore next 2
1836-
// will be covered in follow up
1837-
throw: (error?: unknown) => Promise.reject(error),
1832+
async return(): Promise<IteratorResult<AsyncExecutionResult, void>> {
1833+
await Promise.all(
1834+
exeContext.subsequentPayloads.map((asyncPayloadRecord) =>
1835+
asyncPayloadRecord.iterator?.return?.(),
1836+
),
1837+
);
1838+
isDone = true;
1839+
return { value: undefined, done: true };
1840+
},
1841+
async throw(
1842+
error?: unknown,
1843+
): Promise<IteratorResult<AsyncExecutionResult, void>> {
1844+
await Promise.all(
1845+
exeContext.subsequentPayloads.map((asyncPayloadRecord) =>
1846+
asyncPayloadRecord.iterator?.return?.(),
1847+
),
1848+
);
1849+
isDone = true;
1850+
return Promise.reject(error);
1851+
},
18381852
};
18391853
}
18401854

0 commit comments

Comments
 (0)