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)">
);
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';
+}