diff --git a/README.md b/README.md
index 3210b30f..e484444b 100644
--- a/README.md
+++ b/README.md
@@ -853,7 +853,9 @@ app.UseGraphQL("/graphql", options =>
});
```
-Please note that the included UI packages are configured to use the `graphql-ws` sub-protocol.
+Please note that the included UI packages are configured to use the `graphql-ws` sub-protocol by
+default. You may use the `graphql-transport-ws` sub-protocol with the GraphiQL package by setting
+the `GraphQLWsSubscriptions` option to `true` when configuring the GraphiQL middleware.
### Customizing middleware behavior
diff --git a/src/Ui.GraphiQL/GraphiQLOptions.cs b/src/Ui.GraphiQL/GraphiQLOptions.cs
index 868bc4b9..e09e3b47 100644
--- a/src/Ui.GraphiQL/GraphiQLOptions.cs
+++ b/src/Ui.GraphiQL/GraphiQLOptions.cs
@@ -53,4 +53,9 @@ public class GraphiQLOptions
/// See .
///
public RequestCredentials RequestCredentials { get; set; } = RequestCredentials.SameOrigin;
+
+ ///
+ /// Use the graphql-ws package instead of the subscription-transports-ws package for subscriptions.
+ ///
+ public bool GraphQLWsSubscriptions { get; set; }
}
diff --git a/src/Ui.GraphiQL/Internal/GraphiQLPageModel.cs b/src/Ui.GraphiQL/Internal/GraphiQLPageModel.cs
index 02471628..99f33d6a 100644
--- a/src/Ui.GraphiQL/Internal/GraphiQLPageModel.cs
+++ b/src/Ui.GraphiQL/Internal/GraphiQLPageModel.cs
@@ -50,7 +50,8 @@ public string Render()
.Replace("@Model.Headers", JsonSerialize(headers))
.Replace("@Model.HeaderEditorEnabled", _options.HeaderEditorEnabled ? "true" : "false")
.Replace("@Model.GraphiQLElement", "GraphiQL")
- .Replace("@Model.RequestCredentials", requestCredentials);
+ .Replace("@Model.RequestCredentials", requestCredentials)
+ .Replace("@Model.GraphQLWs", _options.GraphQLWsSubscriptions ? "true" : "false");
// Here, fully-qualified, absolute and relative URLs are supported for both the
// GraphQLEndPoint and SubscriptionsEndPoint. Those paths can be passed unmodified
diff --git a/src/Ui.GraphiQL/Internal/graphiql.cshtml b/src/Ui.GraphiQL/Internal/graphiql.cshtml
index 3881134a..d3f7825a 100644
--- a/src/Ui.GraphiQL/Internal/graphiql.cshtml
+++ b/src/Ui.GraphiQL/Internal/graphiql.cshtml
@@ -77,6 +77,11 @@
integrity="sha384-ArTEHLNWIe9TuoDpFEtD/NeztNdWn3SdmWwMiAuZaSJeOaYypEGzeQoBxuPO+ORM"
crossorigin="anonymous"
>
+
@@ -188,14 +193,85 @@
// if location is absolute (e.g. "/api") then prepend host only
return (window.location.protocol === "http:" ? "ws://" : "wss://") + window.location.host + subscriptionsEndPoint;
}
+ const subscriptionEndPoint = getSubscriptionsEndPoint();
// Enable Subscriptions via WebSocket
- var subscriptionsClient = new window.SubscriptionsTransportWs.SubscriptionClient(getSubscriptionsEndPoint(), { reconnect: true });
- function subscriptionsFetcher(graphQLParams, fetcherOpts = { headers: {} }) {
+ let subscriptionsClient = null;
+ function subscriptionsTransportWsFetcher(graphQLParams, fetcherOpts = { headers: {} }) {
+ if (!subscriptionsClient)
+ subscriptionsClient = new window.SubscriptionsTransportWs.SubscriptionClient(subscriptionEndPoint, { reconnect: true });
return window.GraphiQLSubscriptionsFetcher.graphQLFetcher(subscriptionsClient, function (_graphQLParams) {
return graphQLFetcher(_graphQLParams, fetcherOpts);
})(graphQLParams);
}
+
+ function isSubscription(operationName, documentAST) {
+ if (!documentAST.definitions || !documentAST.definitions.length || !documentAST.definitions.filter) return false;
+ let definitions = documentAST.definitions.filter(function (def) { return def.kind === 'OperationDefinition'; });
+ if (operationName) definitions = definitions.filter(function (def) { return def.name && def.name.value === operationName; });
+ if (definitions.length === 0) return false;
+ return definitions[0].operation === 'subscription';
+ }
+
+ let wsClient = null;
+ function graphQLWsFetcher(payload, fetcherOpts) {
+ if (!fetcherOpts || !fetcherOpts.documentAST || !isSubscription(payload.operationName, fetcherOpts.documentAST))
+ return graphQLFetcher(payload, fetcherOpts);
+ if (!wsClient) {
+ wsClient = graphqlWs.createClient({ url: subscriptionEndPoint });
+ }
+ let deferred = null;
+ const pending = [];
+ let throwMe = null,
+ done = false;
+ const dispose = wsClient.subscribe(payload, {
+ next: (data) => {
+ pending.push(data);
+ if (deferred) deferred.resolve(false);
+ },
+ error: (err) => {
+ if (err instanceof Error) {
+ throwMe = err;
+ } else if (err instanceof CloseEvent) {
+ throwMe = new Error(`Socket closed with event ${err.code} ${err.reason || ""}`.trim());
+ } else {
+ // GraphQLError[]
+ throwMe = new Error(err.map(({ message }) => message).join(", "));
+ }
+ if (deferred) deferred.reject(throwMe);
+ },
+ complete: () => {
+ done = true;
+ if (deferred) deferred.resolve(true);
+ },
+ });
+
+ return {
+ [Symbol.asyncIterator]: function() {
+ return this;
+ },
+ next: function() {
+ if (done) return Promise.resolve({ done: true, value: undefined });
+ if (throwMe) return Promise.reject(throwMe);
+ if (pending.length) return Promise.resolve({ value: pending.shift() });
+ return new Promise(function(resolve, reject) {
+ deferred = { resolve, reject };
+ }).then(function(result) {
+ if (result) {
+ return { done: true, value: undefined };
+ } else {
+ return { value: pending.shift() };
+ }
+ });
+ },
+ return: function() {
+ dispose();
+ return Promise.resolve({ done: true, value: undefined });
+ }
+ };
+ }
+
+ const subscriptionFetcher = (@Model.GraphQLWs) ? graphQLWsFetcher : subscriptionsTransportWsFetcher;
// Render into the body.
// See the README in the top level of this module to learn more about
@@ -203,7 +279,7 @@
// additional child elements.
ReactDOM.render(
React.createElement(@Model.GraphiQLElement, {
- fetcher: subscriptionsFetcher,
+ fetcher: subscriptionFetcher,
query: parameters.query,
variables: parameters.variables,
operationName: parameters.operationName,
diff --git a/tests/ApiApprovalTests/net80+netcoreapp31/GraphQL.Server.Ui.GraphiQL.approved.txt b/tests/ApiApprovalTests/net80+netcoreapp31/GraphQL.Server.Ui.GraphiQL.approved.txt
index 4068d776..fe90f195 100644
--- a/tests/ApiApprovalTests/net80+netcoreapp31/GraphQL.Server.Ui.GraphiQL.approved.txt
+++ b/tests/ApiApprovalTests/net80+netcoreapp31/GraphQL.Server.Ui.GraphiQL.approved.txt
@@ -16,6 +16,7 @@ namespace GraphQL.Server.Ui.GraphiQL
public GraphiQLOptions() { }
public bool ExplorerExtensionEnabled { get; set; }
public string GraphQLEndPoint { get; set; }
+ public bool GraphQLWsSubscriptions { get; set; }
public bool HeaderEditorEnabled { get; set; }
public System.Collections.Generic.Dictionary? Headers { get; set; }
public System.Func IndexStream { get; set; }
diff --git a/tests/ApiApprovalTests/netstandard20/GraphQL.Server.Ui.GraphiQL.approved.txt b/tests/ApiApprovalTests/netstandard20/GraphQL.Server.Ui.GraphiQL.approved.txt
index 5eecf315..c9cfdec2 100644
--- a/tests/ApiApprovalTests/netstandard20/GraphQL.Server.Ui.GraphiQL.approved.txt
+++ b/tests/ApiApprovalTests/netstandard20/GraphQL.Server.Ui.GraphiQL.approved.txt
@@ -16,6 +16,7 @@ namespace GraphQL.Server.Ui.GraphiQL
public GraphiQLOptions() { }
public bool ExplorerExtensionEnabled { get; set; }
public string GraphQLEndPoint { get; set; }
+ public bool GraphQLWsSubscriptions { get; set; }
public bool HeaderEditorEnabled { get; set; }
public System.Collections.Generic.Dictionary? Headers { get; set; }
public System.Func IndexStream { get; set; }