From 9416f81c7dc18be50ea8c18c6e9675b26ff01b4e Mon Sep 17 00:00:00 2001 From: Lee Byron Date: Tue, 8 Mar 2016 16:52:51 -0800 Subject: [PATCH] [RFC] Support a fetcher that returns Observable This supports the fetcher returning an Observable, which will let us begin to experiment with support for GraphQL subscriptions and observable "live queries". To test this, I added the crappiest possible Observable implementation to example/index.html: ```js if (graphQLParams.query.indexOf('subscription') === 0) { return { subscribe(observer) { try { var i = 0; var interval = setInterval(() => { if (i > 10) { clearInterval(interval); observer.complete && observer.complete(); } else { observer.next && observer.next({ data: i++ }); } }, 500); } catch (e) { clearInterval(interval); observer.error && observer.error(e); } return { unsubscribe() { clearInterval(interval); } }; } }; } ``` --- README.md | 2 +- src/components/ExecuteButton.js | 8 ++- src/components/GraphiQL.js | 96 ++++++++++++++++++++++++++++----- 3 files changed, 89 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 35d734d737c..94a7f792b4c 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ and children. **Props:** - `fetcher`: a function which accepts GraphQL-HTTP parameters and returns - a Promise which resolves to the GraphQL parsed JSON response. + a Promise or Observable which resolves to the GraphQL parsed JSON response. - `schema`: a GraphQLSchema instance or `null` if one is not to be used. If `undefined` is provided, GraphiQL will send an introspection query diff --git a/src/components/ExecuteButton.js b/src/components/ExecuteButton.js index 380a0ac93db..3d7217a1fad 100644 --- a/src/components/ExecuteButton.js +++ b/src/components/ExecuteButton.js @@ -16,7 +16,8 @@ import React, { PropTypes } from 'react'; */ export class ExecuteButton extends React.Component { static propTypes = { - onClick: PropTypes.func + onClick: PropTypes.func, + isRunning: PropTypes.bool } render() { @@ -26,7 +27,10 @@ export class ExecuteButton extends React.Component { onClick={this.props.onClick} title="Execute Query (Ctrl-Enter)"> - + { this.props.isRunning ? + : + + } ); diff --git a/src/components/GraphiQL.js b/src/components/GraphiQL.js index e95b63e5004..7df6758c18e 100644 --- a/src/components/GraphiQL.js +++ b/src/components/GraphiQL.js @@ -40,7 +40,8 @@ import { * Props: * * - fetcher: a function which accepts GraphQL-HTTP parameters and returns - * a Promise which resolves to the GraphQL parsed JSON response. + * a Promise or Observable which resolves to the GraphQL parsed + * JSON response. * * - schema: a GraphQLSchema instance or `null` if one is not to be used. * If `undefined` is provided, GraphiQL will send an introspection query @@ -192,6 +193,7 @@ export class GraphiQL extends React.Component { docsOpen: false, docsWidth: this._storageGet('docExplorerWidth') || 350, isWaitingForResponse: false, + subscription: null, }; // Ensure only the last executed editor query is rendered. @@ -242,7 +244,13 @@ export class GraphiQL extends React.Component { // Try the stock introspection query first, falling back on the // sans-subscriptions query for services which do not yet support it. - fetcher({ query: introspectionQuery }) + const fetch = fetcher({ query: introspectionQuery }); + if (!isPromise(fetch)) { + console.error('Fetcher did not return a Promise for introspection.'); + return; + } + + fetch .catch(() => fetcher({ query: introspectionQuerySansSubscriptions })) .then(result => { // If a schema was provided while this fetch was underway, then @@ -315,7 +323,10 @@ export class GraphiQL extends React.Component {
{logo} - + { + const fetcher = this.props.fetcher; + const fetch = fetcher({ query, variables }); + + if (isPromise(fetch)) { + // If fetcher returned a Promise, then call the callback when the promise + // resolves, otherwise handle the error. + fetch.then(cb).catch(error => { + this.setState({ + isWaitingForResponse: false, + response: error && (error.stack || String(error)) + }); + }); + } else if (isObservable(fetch)) { + // If the fetcher returned an Observable, then subscribe to it, calling + // the callback on each next value, and handling both errors and the + // completion of the Observable. Returns a Subscription object. + const subscription = fetch.subscribe({ + next: cb, + error: error => { + this.setState({ + isWaitingForResponse: false, + response: error && (error.stack || String(error)), + subscription: null + }); + }, + complete: () => { + this.setState({ + isWaitingForResponse: false, + subscription: null + }); + } + }); + + return subscription; + } else { this.setState({ isWaitingForResponse: false, - response: error && (error.stack || String(error)) + response: 'Fetcher did not return Promise or Observable.' }); - }); + } } - _runEditorQuery = () => { - this.setState({ - isWaitingForResponse: true, - response: null, - }); + _runOrStopEditorQuery = () => { + // If there is a current subscription, unsubscribe from it. + if (this.state.subscription) { + this.setState({ + isWaitingForResponse: false, + subscription: null + }); + this.state.subscription.unsubscribe(); + return; + } this._editorQueryID++; - var queryID = this._editorQueryID; + const queryID = this._editorQueryID; // Use the edited query after autoCompleteLeafs() runs or, // in case autoCompletion fails (the function returns undefined), // the current query from the editor. - let editedQuery = this.autoCompleteLeafs() || this.state.query; + const editedQuery = this.autoCompleteLeafs() || this.state.query; + const variables = this.state.variables; - this._fetchQuery(editedQuery, this.state.variables, result => { + // _fetchQuery may return a subscription. + const subscription = this._fetchQuery(editedQuery, variables, result => { if (queryID === this._editorQueryID) { this.setState({ isWaitingForResponse: false, @@ -425,6 +477,12 @@ export class GraphiQL extends React.Component { }); } }); + + this.setState({ + isWaitingForResponse: true, + response: null, + subscription + }); } _prettifyQuery = () => { @@ -699,3 +757,13 @@ function getVariableToType(schema, query) { } } } + +// Duck-type promise detection. +function isPromise(value) { + return typeof value === 'object' && typeof value.then === 'function'; +} + +// Duck-type observable detection. +function isObservable(value) { + return typeof value === 'object' && typeof value.subscribe === 'function'; +}