Skip to content

Commit

Permalink
feat: add AsyncIterable support to fetcher function (#1724)
Browse files Browse the repository at this point in the history
  • Loading branch information
n1ru4l authored Dec 5, 2020
1 parent 8485651 commit a568af3
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 11 deletions.
2 changes: 1 addition & 1 deletion packages/graphiql/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ For more details on props, see the [API Docs](https://graphiql-test.netlify.app/

| Prop | Type | Description |
| ---------------------------- | --------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `fetcher` | [`Fetcher function`](https://graphiql-test.netlify.app/typedoc/modules/graphiql.html#fetcher) | **Required.** a function which accepts GraphQL-HTTP parameters and returns a Promise or Observable which resolves to the GraphQL parsed JSON response. | |
| `fetcher` | [`Fetcher function`](https://graphiql-test.netlify.app/typedoc/modules/graphiql.html#fetcher) | **Required.** a function which accepts GraphQL-HTTP parameters and returns a Promise, Observable or AsyncIterable which resolves to the GraphQL parsed JSON response. | |
| `schema` | [`GraphQLSchema`](https://graphql.org/graphql-js/type/#graphqlschema) | a GraphQLSchema instance or `null` if one is not to be used. If `undefined` is provided, GraphiQL will send an introspection query using the fetcher to produce a schema. |
| `query` | `string` (GraphQL) | initial displayed query, if `undefined` is provided, the stored query or `defaultQuery` will be used. You can also set this value at runtime to override the current operation editor state. |
| `validationRules` | `ValidationRule[]` | A array of validation rules that will be used for validating the GraphQL operations. If `undefined` is provided, the default rules (exported as `specifiedRules` from `graphql`) will be used. |
Expand Down
95 changes: 85 additions & 10 deletions packages/graphiql/src/components/GraphiQL.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,15 @@ export type FetcherResult =
| string
| { data: any };

export type FetcherReturnType =
| Promise<FetcherResult>
| Observable<FetcherResult>
| AsyncIterable<FetcherResult>;

export type Fetcher = (
graphQLParams: FetcherParams,
opts?: FetcherOpts,
) => Promise<FetcherResult> | Observable<FetcherResult>;
) => FetcherReturnType;

type OnMouseMoveFn = Maybe<
(moveEvent: MouseEvent | React.MouseEvent<Element>) => void
Expand Down Expand Up @@ -814,7 +819,7 @@ export class GraphiQL extends React.Component<GraphiQLProps, GraphiQLState> {
fetcherOpts.headers = JSON.parse(this.props.headers);
}

const fetch = observableToPromise(
const fetch = fetcherReturnToPromise(
fetcher(
{
query: introspectionQuery,
Expand All @@ -823,6 +828,7 @@ export class GraphiQL extends React.Component<GraphiQLProps, GraphiQLState> {
fetcherOpts,
),
);

if (!isPromise(fetch)) {
this.setState({
response: 'Fetcher did not return a Promise for introspection.',
Expand All @@ -838,7 +844,7 @@ export class GraphiQL extends React.Component<GraphiQLProps, GraphiQLState> {

// Try the stock introspection query first, falling back on the
// sans-subscriptions query for services which do not yet support it.
const fetch2 = observableToPromise(
const fetch2 = fetcherReturnToPromise(
fetcher(
{
query: introspectionQuerySansSubscriptions,
Expand Down Expand Up @@ -958,8 +964,32 @@ export class GraphiQL extends React.Component<GraphiQLProps, GraphiQLState> {
});

return subscription;
} else if (isAsyncIterable(fetch)) {
(async () => {
try {
for await (const result of fetch) {
cb(result);
}
this.safeSetState({
isWaitingForResponse: false,
subscription: null,
});
} catch (error) {
this.safeSetState({
isWaitingForResponse: false,
response: error ? GraphiQL.formatError(error) : undefined,
subscription: null,
});
}
})();

return {
unsubscribe: () => fetch[Symbol.asyncIterator]().return?.(),
};
} else {
throw new Error('Fetcher did not return Promise or Observable.');
throw new Error(
'Fetcher did not return Promise, Observable or AsyncIterable.',
);
}
}

Expand Down Expand Up @@ -1625,12 +1655,7 @@ type Observable<T> = {
};

// Duck-type Observable.take(1).toPromise()
function observableToPromise<T>(
observable: Observable<T> | Promise<T>,
): Promise<T> {
if (!isObservable<T>(observable)) {
return observable;
}
function observableToPromise<T>(observable: Observable<T>): Promise<T> {
return new Promise((resolve, reject) => {
const subscription = observable.subscribe(
v => {
Expand All @@ -1654,6 +1679,56 @@ function isObservable<T>(value: any): value is Observable<T> {
);
}

function isAsyncIterable(input: unknown): input is AsyncIterable<unknown> {
return (
typeof input === 'object' &&
input !== null &&
// Some browsers still don't have Symbol.asyncIterator implemented (iOS Safari)
// That means every custom AsyncIterable must be built using a AsyncGeneratorFunction (async function * () {})
((input as any)[Symbol.toStringTag] === 'AsyncGenerator' ||
Symbol.asyncIterator in input)
);
}

function asyncIterableToPromise<T>(
input: AsyncIterable<T> | AsyncIterableIterator<T>,
): Promise<T> {
return new Promise((resolve, reject) => {
// Also support AsyncGenerator on Safari iOS.
// As mentioned in the isAsyncIterable function there is no Symbol.asyncIterator available
// so every AsyncIterable must be implemented using AsyncGenerator.
const iteratorReturn = ('return' in input
? input
: input[Symbol.asyncIterator]()
).return?.bind(input);
const iteratorNext = ('next' in input
? input
: input[Symbol.asyncIterator]()
).next.bind(input);

iteratorNext()
.then(result => {
resolve(result.value);
// ensure cleanup
iteratorReturn?.();
})
.catch(err => {
reject(err);
});
});
}

function fetcherReturnToPromise(
fetcherResult: FetcherReturnType,
): Promise<FetcherResult> {
if (isAsyncIterable(fetcherResult)) {
return asyncIterableToPromise(fetcherResult);
} else if (isObservable(fetcherResult)) {
return observableToPromise(fetcherResult);
}
return fetcherResult;
}

// Determines if the React child is of the same type of the provided React component
function isChildComponentType<T extends ComponentType>(
child: any,
Expand Down

0 comments on commit a568af3

Please sign in to comment.