Skip to content

Commit 65450d3

Browse files
committed
Support returning async iterables from resolver functions
1 parent b83a9fe commit 65450d3

File tree

4 files changed

+114
-19
lines changed

4 files changed

+114
-19
lines changed

src/execution/__tests__/lists-test.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,39 @@ describe('Execute: Accepts any iterable as list value', () => {
6363
],
6464
});
6565
});
66+
67+
it('Accepts an AsyncGenerator function as a List value', async () => {
68+
async function* yieldAsyncItems() {
69+
yield await 'two';
70+
yield await 4;
71+
yield await false;
72+
}
73+
const listField = yieldAsyncItems();
74+
75+
expect(await complete({ listField })).to.deep.equal({
76+
data: { listField: ['two', '4', 'false'] },
77+
});
78+
});
79+
80+
it('Handles an AsyncGenerator function that throws', async () => {
81+
async function* yieldAsyncItemsError() {
82+
yield await 'two';
83+
yield await 4;
84+
throw new Error('bad');
85+
}
86+
const listField = yieldAsyncItemsError();
87+
88+
expect(await complete({ listField })).to.deep.equal({
89+
data: { listField: ['two', '4', null] },
90+
errors: [
91+
{
92+
message: 'bad',
93+
locations: [{ line: 1, column: 3 }],
94+
path: ['listField', 2],
95+
},
96+
],
97+
});
98+
});
6699
});
67100

68101
describe('Execute: Handles list nullability', () => {

src/execution/execute.js

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import arrayFrom from '../polyfills/arrayFrom';
2+
import { SYMBOL_ASYNC_ITERATOR } from '../polyfills/symbols';
23

34
import type { Path } from '../jsutils/Path';
45
import type { ObjMap } from '../jsutils/ObjMap';
@@ -8,6 +9,7 @@ import memoize3 from '../jsutils/memoize3';
89
import invariant from '../jsutils/invariant';
910
import devAssert from '../jsutils/devAssert';
1011
import isPromise from '../jsutils/isPromise';
12+
import isAsyncIterable from '../jsutils/isAsyncIterable';
1113
import isObjectLike from '../jsutils/isObjectLike';
1214
import isCollection from '../jsutils/isCollection';
1315
import promiseReduce from '../jsutils/promiseReduce';
@@ -850,6 +852,48 @@ function completeValue(
850852
);
851853
}
852854

855+
/**
856+
* Complete a async iterator value by completing the result and calling
857+
* recursively until all the results are completed.
858+
*/
859+
function completeAsyncIteratorValue(
860+
exeContext: ExecutionContext,
861+
itemType: GraphQLOutputType,
862+
fieldNodes: $ReadOnlyArray<FieldNode>,
863+
info: GraphQLResolveInfo,
864+
path: Path,
865+
index: number,
866+
completedResults: Array<mixed>,
867+
iterator: AsyncIterator<mixed>,
868+
): Promise<$ReadOnlyArray<mixed>> {
869+
const fieldPath = addPath(path, index, undefined);
870+
return iterator.next().then(
871+
({ value, done }) => {
872+
if (done) {
873+
return completedResults;
874+
}
875+
completedResults.push(
876+
completeValue(exeContext, itemType, fieldNodes, info, fieldPath, value),
877+
);
878+
return completeAsyncIteratorValue(
879+
exeContext,
880+
itemType,
881+
fieldNodes,
882+
info,
883+
path,
884+
index + 1,
885+
completedResults,
886+
iterator,
887+
);
888+
},
889+
(error) => {
890+
completedResults.push(null);
891+
handleFieldError(error, fieldNodes, fieldPath, itemType, exeContext);
892+
return completedResults;
893+
},
894+
);
895+
}
896+
853897
/**
854898
* Complete a list value by completing each item in the list with the
855899
* inner type
@@ -862,6 +906,23 @@ function completeListValue(
862906
path: Path,
863907
result: mixed,
864908
): PromiseOrValue<$ReadOnlyArray<mixed>> {
909+
const itemType = returnType.ofType;
910+
911+
if (isAsyncIterable(result)) {
912+
const iterator = result[SYMBOL_ASYNC_ITERATOR]();
913+
914+
return completeAsyncIteratorValue(
915+
exeContext,
916+
itemType,
917+
fieldNodes,
918+
info,
919+
path,
920+
0,
921+
[],
922+
iterator,
923+
);
924+
}
925+
865926
if (!isCollection(result)) {
866927
throw new GraphQLError(
867928
`Expected Iterable, but did not find one for field "${info.parentType.name}.${info.fieldName}".`,
@@ -870,7 +931,6 @@ function completeListValue(
870931

871932
// This is specified as a simple map, however we're optimizing the path
872933
// where the list contains no Promises by avoiding creating another Promise.
873-
const itemType = returnType.ofType;
874934
let containsPromise = false;
875935
const completedResults = arrayFrom(result, (item, index) => {
876936
// No need to modify the info object containing the path,

src/jsutils/isAsyncIterable.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { SYMBOL_ASYNC_ITERATOR } from '../polyfills/symbols';
2+
3+
/**
4+
* Returns true if the provided object implements the AsyncIterator protocol via
5+
* either implementing a `Symbol.asyncIterator` or `"@@asyncIterator"` method.
6+
*/
7+
declare function isAsyncIterable(value: mixed): boolean %checks(value instanceof
8+
AsyncIterable);
9+
10+
// eslint-disable-next-line no-redeclare
11+
export default function isAsyncIterable(maybeAsyncIterable) {
12+
if (maybeAsyncIterable == null || typeof maybeAsyncIterable !== 'object') {
13+
return false;
14+
}
15+
16+
return typeof maybeAsyncIterable[SYMBOL_ASYNC_ITERATOR] === 'function';
17+
}

src/subscription/subscribe.js

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { SYMBOL_ASYNC_ITERATOR } from '../polyfills/symbols';
2-
31
import inspect from '../jsutils/inspect';
2+
import isAsyncIterable from '../jsutils/isAsyncIterable';
43
import { addPath, pathToArray } from '../jsutils/Path';
54

65
import { GraphQLError } from '../error/GraphQLError';
@@ -158,7 +157,7 @@ function subscribeImpl(
158157
// Note: Flow can't refine isAsyncIterable, so explicit casts are used.
159158
isAsyncIterable(resultOrStream)
160159
? mapAsyncIterator(
161-
((resultOrStream: any): AsyncIterable<mixed>),
160+
resultOrStream,
162161
mapSourceToResponse,
163162
reportGraphQLError,
164163
)
@@ -289,24 +288,10 @@ function executeSubscription(
289288
`Received: ${inspect(eventStream)}.`,
290289
);
291290
}
292-
293-
// Note: isAsyncIterable above ensures this will be correct.
294-
return ((eventStream: any): AsyncIterable<mixed>);
291+
return eventStream;
295292
},
296293
(error) => {
297294
throw locatedError(error, fieldNodes, pathToArray(path));
298295
},
299296
);
300297
}
301-
302-
/**
303-
* Returns true if the provided object implements the AsyncIterator protocol via
304-
* either implementing a `Symbol.asyncIterator` or `"@@asyncIterator"` method.
305-
*/
306-
function isAsyncIterable(maybeAsyncIterable: mixed): boolean {
307-
if (maybeAsyncIterable == null || typeof maybeAsyncIterable !== 'object') {
308-
return false;
309-
}
310-
311-
return typeof maybeAsyncIterable[SYMBOL_ASYNC_ITERATOR] === 'function';
312-
}

0 commit comments

Comments
 (0)