Skip to content

Commit 05f1903

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

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
@@ -1744,6 +1744,7 @@ async function executeStreamIterator(
17441744
label,
17451745
path: fieldPath,
17461746
parentContext,
1747+
iterator,
17471748
});
17481749

17491750
const dataPromise = executeStreamIteratorItem(
@@ -1786,6 +1787,7 @@ function yieldSubsequentPayloads(
17861787
initialResult: ExecutionResult,
17871788
): AsyncGenerator<AsyncExecutionResult, void, void> {
17881789
let _hasReturnedInitialResult = false;
1790+
let isDone = false;
17891791

17901792
async function race(): Promise<IteratorResult<AsyncExecutionResult>> {
17911793
if (exeContext.subsequentPayloads.length === 0) {
@@ -1856,19 +1858,31 @@ function yieldSubsequentPayloads(
18561858
},
18571859
done: false,
18581860
});
1859-
} else if (exeContext.subsequentPayloads.length === 0) {
1861+
} else if (exeContext.subsequentPayloads.length === 0 || isDone) {
18601862
return Promise.resolve({ value: undefined, done: true });
18611863
}
18621864
return race();
18631865
},
1864-
// TODO: implement return & throw
1865-
// c8 ignore next 2
1866-
// will be covered in follow up
1867-
return: () => Promise.resolve({ value: undefined, done: true }),
1868-
1869-
// c8 ignore next 2
1870-
// will be covered in follow up
1871-
throw: (error?: unknown) => Promise.reject(error),
1866+
async return(): Promise<IteratorResult<AsyncExecutionResult, void>> {
1867+
await Promise.all(
1868+
exeContext.subsequentPayloads.map((asyncPayloadRecord) =>
1869+
asyncPayloadRecord.iterator?.return?.(),
1870+
),
1871+
);
1872+
isDone = true;
1873+
return { value: undefined, done: true };
1874+
},
1875+
async throw(
1876+
error?: unknown,
1877+
): Promise<IteratorResult<AsyncExecutionResult, void>> {
1878+
await Promise.all(
1879+
exeContext.subsequentPayloads.map((asyncPayloadRecord) =>
1880+
asyncPayloadRecord.iterator?.return?.(),
1881+
),
1882+
);
1883+
isDone = true;
1884+
return Promise.reject(error);
1885+
},
18721886
};
18731887
}
18741888

0 commit comments

Comments
 (0)