Skip to content

Commit a3e52a0

Browse files
authored
Add GraphQLWs subscription transport option for GraphiQL (#1162)
1 parent b12f1c0 commit a3e52a0

File tree

6 files changed

+91
-5
lines changed

6 files changed

+91
-5
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -853,7 +853,9 @@ app.UseGraphQL("/graphql", options =>
853853
});
854854
```
855855

856-
Please note that the included UI packages are configured to use the `graphql-ws` sub-protocol.
856+
Please note that the included UI packages are configured to use the `graphql-ws` sub-protocol by
857+
default. You may use the `graphql-transport-ws` sub-protocol with the GraphiQL package by setting
858+
the `GraphQLWsSubscriptions` option to `true` when configuring the GraphiQL middleware.
857859

858860
### Customizing middleware behavior
859861

src/Ui.GraphiQL/GraphiQLOptions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,9 @@ public class GraphiQLOptions
5353
/// See <see href="https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials"/>.
5454
/// </remarks>
5555
public RequestCredentials RequestCredentials { get; set; } = RequestCredentials.SameOrigin;
56+
57+
/// <summary>
58+
/// Use the graphql-ws package instead of the subscription-transports-ws package for subscriptions.
59+
/// </summary>
60+
public bool GraphQLWsSubscriptions { get; set; }
5661
}

src/Ui.GraphiQL/Internal/GraphiQLPageModel.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ public string Render()
5050
.Replace("@Model.Headers", JsonSerialize(headers))
5151
.Replace("@Model.HeaderEditorEnabled", _options.HeaderEditorEnabled ? "true" : "false")
5252
.Replace("@Model.GraphiQLElement", "GraphiQL")
53-
.Replace("@Model.RequestCredentials", requestCredentials);
53+
.Replace("@Model.RequestCredentials", requestCredentials)
54+
.Replace("@Model.GraphQLWs", _options.GraphQLWsSubscriptions ? "true" : "false");
5455

5556
// Here, fully-qualified, absolute and relative URLs are supported for both the
5657
// GraphQLEndPoint and SubscriptionsEndPoint. Those paths can be passed unmodified

src/Ui.GraphiQL/Internal/graphiql.cshtml

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@
7777
integrity="sha384-ArTEHLNWIe9TuoDpFEtD/NeztNdWn3SdmWwMiAuZaSJeOaYypEGzeQoBxuPO+ORM"
7878
crossorigin="anonymous"
7979
></script>
80+
<script
81+
src="https://unpkg.com/[email protected]/umd/graphql-ws.min.js"
82+
integrity="sha384-oEPbisbEBMo7iCrbQcKx244HXUjGnF1jyS8hkVZ3oCwnw9c9oLfY70c1RKeKj3+i"
83+
crossorigin="anonymous"
84+
></script>
8085

8186
</head>
8287
<body>
@@ -188,22 +193,93 @@
188193
// if location is absolute (e.g. "/api") then prepend host only
189194
return (window.location.protocol === "http:" ? "ws://" : "wss://") + window.location.host + subscriptionsEndPoint;
190195
}
196+
const subscriptionEndPoint = getSubscriptionsEndPoint();
191197
192198
// Enable Subscriptions via WebSocket
193-
var subscriptionsClient = new window.SubscriptionsTransportWs.SubscriptionClient(getSubscriptionsEndPoint(), { reconnect: true });
194-
function subscriptionsFetcher(graphQLParams, fetcherOpts = { headers: {} }) {
199+
let subscriptionsClient = null;
200+
function subscriptionsTransportWsFetcher(graphQLParams, fetcherOpts = { headers: {} }) {
201+
if (!subscriptionsClient)
202+
subscriptionsClient = new window.SubscriptionsTransportWs.SubscriptionClient(subscriptionEndPoint, { reconnect: true });
195203
return window.GraphiQLSubscriptionsFetcher.graphQLFetcher(subscriptionsClient, function (_graphQLParams) {
196204
return graphQLFetcher(_graphQLParams, fetcherOpts);
197205
})(graphQLParams);
198206
}
207+
208+
function isSubscription(operationName, documentAST) {
209+
if (!documentAST.definitions || !documentAST.definitions.length || !documentAST.definitions.filter) return false;
210+
let definitions = documentAST.definitions.filter(function (def) { return def.kind === 'OperationDefinition'; });
211+
if (operationName) definitions = definitions.filter(function (def) { return def.name && def.name.value === operationName; });
212+
if (definitions.length === 0) return false;
213+
return definitions[0].operation === 'subscription';
214+
}
215+
216+
let wsClient = null;
217+
function graphQLWsFetcher(payload, fetcherOpts) {
218+
if (!fetcherOpts || !fetcherOpts.documentAST || !isSubscription(payload.operationName, fetcherOpts.documentAST))
219+
return graphQLFetcher(payload, fetcherOpts);
220+
if (!wsClient) {
221+
wsClient = graphqlWs.createClient({ url: subscriptionEndPoint });
222+
}
223+
let deferred = null;
224+
const pending = [];
225+
let throwMe = null,
226+
done = false;
227+
const dispose = wsClient.subscribe(payload, {
228+
next: (data) => {
229+
pending.push(data);
230+
if (deferred) deferred.resolve(false);
231+
},
232+
error: (err) => {
233+
if (err instanceof Error) {
234+
throwMe = err;
235+
} else if (err instanceof CloseEvent) {
236+
throwMe = new Error(`Socket closed with event ${err.code} ${err.reason || ""}`.trim());
237+
} else {
238+
// GraphQLError[]
239+
throwMe = new Error(err.map(({ message }) => message).join(", "));
240+
}
241+
if (deferred) deferred.reject(throwMe);
242+
},
243+
complete: () => {
244+
done = true;
245+
if (deferred) deferred.resolve(true);
246+
},
247+
});
248+
249+
return {
250+
[Symbol.asyncIterator]: function() {
251+
return this;
252+
},
253+
next: function() {
254+
if (done) return Promise.resolve({ done: true, value: undefined });
255+
if (throwMe) return Promise.reject(throwMe);
256+
if (pending.length) return Promise.resolve({ value: pending.shift() });
257+
return new Promise(function(resolve, reject) {
258+
deferred = { resolve, reject };
259+
}).then(function(result) {
260+
if (result) {
261+
return { done: true, value: undefined };
262+
} else {
263+
return { value: pending.shift() };
264+
}
265+
});
266+
},
267+
return: function() {
268+
dispose();
269+
return Promise.resolve({ done: true, value: undefined });
270+
}
271+
};
272+
}
273+
274+
const subscriptionFetcher = (@Model.GraphQLWs) ? graphQLWsFetcher : subscriptionsTransportWsFetcher;
199275
200276
// Render <GraphiQL /> into the body.
201277
// See the README in the top level of this module to learn more about
202278
// how you can customize GraphiQL by providing different values or
203279
// additional child elements.
204280
ReactDOM.render(
205281
React.createElement(@Model.GraphiQLElement, {
206-
fetcher: subscriptionsFetcher,
282+
fetcher: subscriptionFetcher,
207283
query: parameters.query,
208284
variables: parameters.variables,
209285
operationName: parameters.operationName,

tests/ApiApprovalTests/net80+netcoreapp31/GraphQL.Server.Ui.GraphiQL.approved.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ namespace GraphQL.Server.Ui.GraphiQL
1616
public GraphiQLOptions() { }
1717
public bool ExplorerExtensionEnabled { get; set; }
1818
public string GraphQLEndPoint { get; set; }
19+
public bool GraphQLWsSubscriptions { get; set; }
1920
public bool HeaderEditorEnabled { get; set; }
2021
public System.Collections.Generic.Dictionary<string, string>? Headers { get; set; }
2122
public System.Func<GraphQL.Server.Ui.GraphiQL.GraphiQLOptions, System.IO.Stream> IndexStream { get; set; }

tests/ApiApprovalTests/netstandard20/GraphQL.Server.Ui.GraphiQL.approved.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ namespace GraphQL.Server.Ui.GraphiQL
1616
public GraphiQLOptions() { }
1717
public bool ExplorerExtensionEnabled { get; set; }
1818
public string GraphQLEndPoint { get; set; }
19+
public bool GraphQLWsSubscriptions { get; set; }
1920
public bool HeaderEditorEnabled { get; set; }
2021
public System.Collections.Generic.Dictionary<string, string>? Headers { get; set; }
2122
public System.Func<GraphQL.Server.Ui.GraphiQL.GraphiQLOptions, System.IO.Stream> IndexStream { get; set; }

0 commit comments

Comments
 (0)