Skip to content

Commit b062f1b

Browse files
committed
Return underlying AsyncIterators when execute result is returned (#2843)
1 parent df690d3 commit b062f1b

File tree

2 files changed

+167
-0
lines changed

2 files changed

+167
-0
lines changed

src/execution/__tests__/stream-test.js

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { expect } from 'chai';
22
import { describe, it } from 'mocha';
33

4+
import invariant from '../../jsutils/invariant';
45
import isAsyncIterable from '../../jsutils/isAsyncIterable';
56
import { parse } from '../../language/parser';
67

@@ -72,6 +73,36 @@ const query = new GraphQLObjectType({
7273
yield {};
7374
},
7475
},
76+
asyncIterableListDelayed: {
77+
type: new GraphQLList(friendType),
78+
async *resolve() {
79+
for (const friend of friends) {
80+
// pause an additional ms before yielding to allow time
81+
// for tests to return or throw before next value is processed.
82+
// eslint-disable-next-line no-await-in-loop
83+
await new Promise((r) => setTimeout(r, 1));
84+
yield friend;
85+
}
86+
},
87+
},
88+
asyncIterableListNoReturn: {
89+
type: new GraphQLList(friendType),
90+
resolve() {
91+
let i = 0;
92+
return {
93+
[Symbol.asyncIterator]: () => ({
94+
async next() {
95+
const friend = friends[i++];
96+
if (friend) {
97+
await new Promise((r) => setTimeout(r, 1));
98+
return { value: friend, done: false };
99+
}
100+
return { value: undefined, done: true };
101+
},
102+
}),
103+
};
104+
},
105+
},
75106
asyncIterableListDelayedClose: {
76107
type: new GraphQLList(friendType),
77108
async *resolve() {
@@ -649,4 +680,114 @@ describe('Execute: stream directive', () => {
649680
},
650681
]);
651682
});
683+
it('Returns underlying async iterables when dispatcher is returned', async () => {
684+
const document = parse(`
685+
query {
686+
asyncIterableListDelayed @stream(initialCount: 1) {
687+
name
688+
id
689+
}
690+
}
691+
`);
692+
const schema = new GraphQLSchema({ query });
693+
694+
const executeResult = await execute(schema, document, {});
695+
invariant(isAsyncIterable(executeResult));
696+
697+
const result1 = await executeResult.next();
698+
expect(result1).to.deep.equal({
699+
done: false,
700+
value: {
701+
data: {
702+
asyncIterableListDelayed: [
703+
{
704+
id: '1',
705+
name: 'Luke',
706+
},
707+
],
708+
},
709+
hasNext: true,
710+
},
711+
});
712+
713+
executeResult.return();
714+
715+
// this result had started processing before return was called
716+
const result2 = await executeResult.next();
717+
expect(result2).to.deep.equal({
718+
done: false,
719+
value: {
720+
data: {
721+
id: '2',
722+
name: 'Han',
723+
},
724+
hasNext: true,
725+
path: ['asyncIterableListDelayed', 1],
726+
},
727+
});
728+
729+
// third result is not returned because async iterator has returned
730+
const result3 = await executeResult.next();
731+
expect(result3).to.deep.equal({
732+
done: false,
733+
value: {
734+
hasNext: false,
735+
},
736+
});
737+
});
738+
it('Can return async iterable when underlying iterable does not have a return method', async () => {
739+
const document = parse(`
740+
query {
741+
asyncIterableListNoReturn @stream(initialCount: 1) {
742+
name
743+
id
744+
}
745+
}
746+
`);
747+
const schema = new GraphQLSchema({ query });
748+
749+
const executeResult = await execute(schema, document, {});
750+
invariant(isAsyncIterable(executeResult));
751+
752+
const result1 = await executeResult.next();
753+
expect(result1).to.deep.equal({
754+
done: false,
755+
value: {
756+
data: {
757+
asyncIterableListNoReturn: [
758+
{
759+
id: '1',
760+
name: 'Luke',
761+
},
762+
],
763+
},
764+
hasNext: true,
765+
},
766+
});
767+
768+
executeResult.return();
769+
770+
// this result had started processing before return was called
771+
const result2 = await executeResult.next();
772+
expect(result2).to.deep.equal({
773+
done: false,
774+
value: {
775+
data: {
776+
id: '2',
777+
name: 'Han',
778+
},
779+
hasNext: true,
780+
path: ['asyncIterableListNoReturn', 1],
781+
},
782+
});
783+
784+
// third result is not returned because async iterator has returned
785+
const result3 = await executeResult.next();
786+
expect(result3).to.deep.equal({
787+
done: false,
788+
value: {
789+
hasNext: false,
790+
},
791+
});
792+
});
652793
});

src/execution/execute.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1604,11 +1604,15 @@ type DispatcherResult = {|
16041604
*/
16051605
export class Dispatcher {
16061606
_subsequentPayloads: Array<Promise<IteratorResult<DispatcherResult, void>>>;
1607+
_iterators: Array<AsyncIterator<mixed>>;
1608+
_isDone: boolean;
16071609
_initialResult: ?ExecutionResult;
16081610
_hasReturnedInitialResult: boolean;
16091611

16101612
constructor() {
16111613
this._subsequentPayloads = [];
1614+
this._iterators = [];
1615+
this._isDone = false;
16121616
this._hasReturnedInitialResult = false;
16131617
}
16141618

@@ -1677,13 +1681,16 @@ export class Dispatcher {
16771681
itemType: GraphQLOutputType,
16781682
): void {
16791683
const subsequentPayloads = this._subsequentPayloads;
1684+
const iterators = this._iterators;
1685+
iterators.push(iterator);
16801686
function next(index) {
16811687
const fieldPath = addPath(path, index);
16821688
const patchErrors = [];
16831689
subsequentPayloads.push(
16841690
iterator.next().then(
16851691
({ value: data, done }) => {
16861692
if (done) {
1693+
iterators.splice(iterators.indexOf(iterator), 1);
16871694
return { value: undefined, done: true };
16881695
}
16891696

@@ -1754,6 +1761,14 @@ export class Dispatcher {
17541761
}
17551762

17561763
_race(): Promise<IteratorResult<ExecutionPatchResult, void>> {
1764+
if (this._isDone) {
1765+
return Promise.resolve({
1766+
value: {
1767+
hasNext: false,
1768+
},
1769+
done: false,
1770+
});
1771+
}
17571772
return new Promise((resolve) => {
17581773
this._subsequentPayloads.forEach((promise) => {
17591774
promise.then(() => {
@@ -1810,13 +1825,24 @@ export class Dispatcher {
18101825
return this._race();
18111826
}
18121827

1828+
_return(): Promise<IteratorResult<AsyncExecutionResult, void>> {
1829+
return Promise.all(
1830+
// $FlowFixMe[prop-missing]
1831+
this._iterators.map((iterator) => iterator.return?.()),
1832+
).then(() => {
1833+
this._isDone = true;
1834+
return { value: undefined, done: true };
1835+
});
1836+
}
1837+
18131838
get(initialResult: ExecutionResult): AsyncIterable<AsyncExecutionResult> {
18141839
this._initialResult = initialResult;
18151840
return ({
18161841
[SYMBOL_ASYNC_ITERATOR]() {
18171842
return this;
18181843
},
18191844
next: () => this._next(),
1845+
return: () => this._return(),
18201846
}: any);
18211847
}
18221848
}

0 commit comments

Comments
 (0)