Skip to content

Commit 1a76618

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

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
@@ -1568,6 +1568,7 @@ async function executeStreamIterator(
15681568
label,
15691569
path: fieldPath,
15701570
parentContext,
1571+
iterator,
15711572
});
15721573

15731574
const dataPromise = executeStreamIteratorItem(
@@ -1610,6 +1611,7 @@ function yieldSubsequentPayloads(
16101611
initialResult: ExecutionResult,
16111612
): AsyncGenerator<AsyncExecutionResult, void, void> {
16121613
let _hasReturnedInitialResult = false;
1614+
let isDone = false;
16131615

16141616
async function race(): Promise<IteratorResult<AsyncExecutionResult>> {
16151617
if (exeContext.subsequentPayloads.length === 0) {
@@ -1680,19 +1682,31 @@ function yieldSubsequentPayloads(
16801682
},
16811683
done: false,
16821684
});
1683-
} else if (exeContext.subsequentPayloads.length === 0) {
1685+
} else if (exeContext.subsequentPayloads.length === 0 || isDone) {
16841686
return Promise.resolve({ value: undefined, done: true });
16851687
}
16861688
return race();
16871689
},
1688-
// TODO: implement return & throw
1689-
// c8 ignore next 2
1690-
// will be covered in follow up
1691-
return: () => Promise.resolve({ value: undefined, done: true }),
1692-
1693-
// c8 ignore next 2
1694-
// will be covered in follow up
1695-
throw: (error?: unknown) => Promise.reject(error),
1690+
async return(): Promise<IteratorResult<AsyncExecutionResult, void>> {
1691+
await Promise.all(
1692+
exeContext.subsequentPayloads.map((asyncPayloadRecord) =>
1693+
asyncPayloadRecord.iterator?.return?.(),
1694+
),
1695+
);
1696+
isDone = true;
1697+
return { value: undefined, done: true };
1698+
},
1699+
async throw(
1700+
error?: unknown,
1701+
): Promise<IteratorResult<AsyncExecutionResult, void>> {
1702+
await Promise.all(
1703+
exeContext.subsequentPayloads.map((asyncPayloadRecord) =>
1704+
asyncPayloadRecord.iterator?.return?.(),
1705+
),
1706+
);
1707+
isDone = true;
1708+
return Promise.reject(error);
1709+
},
16961710
};
16971711
}
16981712

0 commit comments

Comments
 (0)