Skip to content

Commit

Permalink
Support returning async iterables from resolver functions
Browse files Browse the repository at this point in the history
  • Loading branch information
robrichard committed Aug 12, 2020
1 parent a9a095e commit 93bbece
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 19 deletions.
33 changes: 33 additions & 0 deletions src/execution/__tests__/lists-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,39 @@ describe('Execute: Accepts any iterable as list value', () => {
],
});
});

it('Accepts an AsyncGenerator function as a List value', async () => {
async function* yieldAsyncItems() {
yield await 'two';
yield await 4;
yield await false;
}
const listField = yieldAsyncItems();

expect(await complete({ listField })).to.deep.equal({
data: { listField: ['two', '4', 'false'] },
});
});

it('Handles an AsyncGenerator function that throws', async () => {
async function* yieldAsyncItemsError() {
yield await 'two';
yield await 4;
throw new Error('bad');
}
const listField = yieldAsyncItemsError();

expect(await complete({ listField })).to.deep.equal({
data: { listField: ['two', '4', null] },
errors: [
{
message: 'bad',
locations: [{ line: 1, column: 3 }],
path: ['listField', 2],
},
],
});
});
});

describe('Execute: Handles list nullability', () => {
Expand Down
62 changes: 61 additions & 1 deletion src/execution/execute.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import arrayFrom from '../polyfills/arrayFrom';
import { SYMBOL_ASYNC_ITERATOR } from '../polyfills/symbols';

import type { Path } from '../jsutils/Path';
import type { ObjMap } from '../jsutils/ObjMap';
Expand All @@ -8,6 +9,7 @@ import memoize3 from '../jsutils/memoize3';
import invariant from '../jsutils/invariant';
import devAssert from '../jsutils/devAssert';
import isPromise from '../jsutils/isPromise';
import isAsyncIterable from '../jsutils/isAsyncIterable';
import isObjectLike from '../jsutils/isObjectLike';
import isCollection from '../jsutils/isCollection';
import promiseReduce from '../jsutils/promiseReduce';
Expand Down Expand Up @@ -850,6 +852,48 @@ function completeValue(
);
}

/**
* Complete a async iterator value by completing the result and calling
* recursively until all the results are completed.
*/
function completeAsyncIteratorValue(
exeContext: ExecutionContext,
itemType: GraphQLOutputType,
fieldNodes: $ReadOnlyArray<FieldNode>,
info: GraphQLResolveInfo,
path: Path,
index: number,
completedResults: Array<mixed>,
iterator: AsyncIterator<mixed>,
): Promise<$ReadOnlyArray<mixed>> {
const fieldPath = addPath(path, index, undefined);
return iterator.next().then(
({ value, done }) => {
if (done) {
return completedResults;
}
completedResults.push(
completeValue(exeContext, itemType, fieldNodes, info, fieldPath, value),
);
return completeAsyncIteratorValue(
exeContext,
itemType,
fieldNodes,
info,
path,
index + 1,
completedResults,
iterator,
);
},
(error) => {
completedResults.push(null);
handleFieldError(error, fieldNodes, fieldPath, itemType, exeContext);
return completedResults;
},
);
}

/**
* Complete a list value by completing each item in the list with the
* inner type
Expand All @@ -862,6 +906,23 @@ function completeListValue(
path: Path,
result: mixed,
): PromiseOrValue<$ReadOnlyArray<mixed>> {
const itemType = returnType.ofType;

if (isAsyncIterable(result)) {
const iterator = result[SYMBOL_ASYNC_ITERATOR]();

return completeAsyncIteratorValue(
exeContext,
itemType,
fieldNodes,
info,
path,
0,
[],
iterator,
);
}

if (!isCollection(result)) {
throw new GraphQLError(
`Expected Iterable, but did not find one for field "${info.parentType.name}.${info.fieldName}".`,
Expand All @@ -870,7 +931,6 @@ function completeListValue(

// This is specified as a simple map, however we're optimizing the path
// where the list contains no Promises by avoiding creating another Promise.
const itemType = returnType.ofType;
let containsPromise = false;
const completedResults = arrayFrom(result, (item, index) => {
// No need to modify the info object containing the path,
Expand Down
19 changes: 19 additions & 0 deletions src/jsutils/isAsyncIterable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// @flow strict

import { SYMBOL_ASYNC_ITERATOR } from '../polyfills/symbols';

/**
* Returns true if the provided object implements the AsyncIterator protocol via
* either implementing a `Symbol.asyncIterator` or `"@@asyncIterator"` method.
*/
declare function isAsyncIterable(value: mixed): boolean %checks(value instanceof
AsyncIterable);

// eslint-disable-next-line no-redeclare
export default function isAsyncIterable(maybeAsyncIterable) {
if (maybeAsyncIterable == null || typeof maybeAsyncIterable !== 'object') {
return false;
}

return typeof maybeAsyncIterable[SYMBOL_ASYNC_ITERATOR] === 'function';
}
21 changes: 3 additions & 18 deletions src/subscription/subscribe.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { SYMBOL_ASYNC_ITERATOR } from '../polyfills/symbols';

import inspect from '../jsutils/inspect';
import isAsyncIterable from '../jsutils/isAsyncIterable';
import { addPath, pathToArray } from '../jsutils/Path';

import { GraphQLError } from '../error/GraphQLError';
Expand Down Expand Up @@ -158,7 +157,7 @@ function subscribeImpl(
// Note: Flow can't refine isAsyncIterable, so explicit casts are used.
isAsyncIterable(resultOrStream)
? mapAsyncIterator(
((resultOrStream: any): AsyncIterable<mixed>),
resultOrStream,
mapSourceToResponse,
reportGraphQLError,
)
Expand Down Expand Up @@ -289,24 +288,10 @@ function executeSubscription(
`Received: ${inspect(eventStream)}.`,
);
}

// Note: isAsyncIterable above ensures this will be correct.
return ((eventStream: any): AsyncIterable<mixed>);
return eventStream;
},
(error) => {
throw locatedError(error, fieldNodes, pathToArray(path));
},
);
}

/**
* Returns true if the provided object implements the AsyncIterator protocol via
* either implementing a `Symbol.asyncIterator` or `"@@asyncIterator"` method.
*/
function isAsyncIterable(maybeAsyncIterable: mixed): boolean {
if (maybeAsyncIterable == null || typeof maybeAsyncIterable !== 'object') {
return false;
}

return typeof maybeAsyncIterable[SYMBOL_ASYNC_ITERATOR] === 'function';
}

0 comments on commit 93bbece

Please sign in to comment.