Skip to content

Commit b4ac57a

Browse files
committed
Enable GraphQLWs subscription transport for GraphiQL
1 parent b12f1c0 commit b4ac57a

File tree

6 files changed

+96
-5
lines changed

6 files changed

+96
-5
lines changed

README.md

+3-1
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

+5
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

+2-1
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

+84-3
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,98 @@
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+
const 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+
console.log('graphQLWsFetcher', payload, fetcherOpts, !fetcherOpts || !fetcherOpts.documentAST || !isSubscription(payload.operationName, fetcherOpts.documentAST));
219+
if (!fetcherOpts || !fetcherOpts.documentAST || !isSubscription(payload.operationName, fetcherOpts.documentAST))
220+
return graphQLFetcher(payload, fetcherOpts);
221+
if (!wsClient) {
222+
wsClient = graphqlWs.createClient({ url: subscriptionEndPoint });
223+
}
224+
let deferred = null;
225+
const pending = [];
226+
let throwMe = null,
227+
done = false;
228+
const dispose = wsClient.subscribe(payload, {
229+
next: (data) => {
230+
pending.push(data);
231+
if (deferred) deferred.resolve(false);
232+
},
233+
error: (err) => {
234+
if (err instanceof Error) {
235+
throwMe = err;
236+
} else if (err instanceof CloseEvent) {
237+
throwMe = new Error(
238+
`Socket closed with event ${err.code} ${
239+
err.reason || ""
240+
}`.trim()
241+
);
242+
} else {
243+
// GraphQLError[]
244+
throwMe = new Error(err.map(({ message }) => message).join(", "));
245+
}
246+
if (deferred) deferred.reject(throwMe);
247+
},
248+
complete: () => {
249+
done = true;
250+
if (deferred) deferred.resolve(true);
251+
},
252+
});
253+
254+
return {
255+
[Symbol.asyncIterator]: function() {
256+
return this;
257+
},
258+
next: function() {
259+
if (done) return Promise.resolve({ done: true, value: undefined });
260+
if (throwMe) return Promise.reject(throwMe);
261+
if (pending.length) return Promise.resolve({ value: pending.shift() });
262+
return new Promise(function(resolve, reject) {
263+
deferred = { resolve, reject };
264+
}).then(function(result) {
265+
if (result) {
266+
return { done: true, value: undefined };
267+
} else {
268+
return { value: pending.shift() };
269+
}
270+
});
271+
},
272+
return: function() {
273+
dispose();
274+
return Promise.resolve({ done: true, value: undefined });
275+
}
276+
};
277+
}
278+
279+
const subscriptionFetcher = (@Model.GraphQLWs) ? graphQLWsFetcher : subscriptionsTransportWsFetcher;
199280
200281
// Render <GraphiQL /> into the body.
201282
// See the README in the top level of this module to learn more about
202283
// how you can customize GraphiQL by providing different values or
203284
// additional child elements.
204285
ReactDOM.render(
205286
React.createElement(@Model.GraphiQLElement, {
206-
fetcher: subscriptionsFetcher,
287+
fetcher: subscriptionFetcher,
207288
query: parameters.query,
208289
variables: parameters.variables,
209290
operationName: parameters.operationName,

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

+1
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

+1
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)