From b4ac57adb305aa7acd65b86e08182d340a218a2c Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Thu, 31 Oct 2024 10:21:52 -0400 Subject: [PATCH 1/4] Enable GraphQLWs subscription transport for GraphiQL --- README.md | 4 +- src/Ui.GraphiQL/GraphiQLOptions.cs | 5 ++ src/Ui.GraphiQL/Internal/GraphiQLPageModel.cs | 3 +- src/Ui.GraphiQL/Internal/graphiql.cshtml | 87 ++++++++++++++++++- .../GraphQL.Server.Ui.GraphiQL.approved.txt | 1 + .../GraphQL.Server.Ui.GraphiQL.approved.txt | 1 + 6 files changed, 96 insertions(+), 5 deletions(-) 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..e50f2ec8 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,90 @@ // 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; + const 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) { + console.log('graphQLWsFetcher', payload, fetcherOpts, !fetcherOpts || !fetcherOpts.documentAST || !isSubscription(payload.operationName, fetcherOpts.documentAST)); + 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 +284,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; } From fc8d6cd5eb157240664db665e94463b9117ace79 Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Thu, 31 Oct 2024 10:23:35 -0400 Subject: [PATCH 2/4] remove console log --- src/Ui.GraphiQL/Internal/graphiql.cshtml | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Ui.GraphiQL/Internal/graphiql.cshtml b/src/Ui.GraphiQL/Internal/graphiql.cshtml index e50f2ec8..6ffdecc0 100644 --- a/src/Ui.GraphiQL/Internal/graphiql.cshtml +++ b/src/Ui.GraphiQL/Internal/graphiql.cshtml @@ -215,7 +215,6 @@ let wsClient = null; function graphQLWsFetcher(payload, fetcherOpts) { - console.log('graphQLWsFetcher', payload, fetcherOpts, !fetcherOpts || !fetcherOpts.documentAST || !isSubscription(payload.operationName, fetcherOpts.documentAST)); if (!fetcherOpts || !fetcherOpts.documentAST || !isSubscription(payload.operationName, fetcherOpts.documentAST)) return graphQLFetcher(payload, fetcherOpts); if (!wsClient) { From 3817ecff7301c770807da1879eaf21a93a515874 Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Thu, 31 Oct 2024 10:24:17 -0400 Subject: [PATCH 3/4] update --- src/Ui.GraphiQL/Internal/graphiql.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ui.GraphiQL/Internal/graphiql.cshtml b/src/Ui.GraphiQL/Internal/graphiql.cshtml index 6ffdecc0..3d83b2ca 100644 --- a/src/Ui.GraphiQL/Internal/graphiql.cshtml +++ b/src/Ui.GraphiQL/Internal/graphiql.cshtml @@ -207,7 +207,7 @@ function isSubscription(operationName, documentAST) { if (!documentAST.definitions || !documentAST.definitions.length || !documentAST.definitions.filter) return false; - const definitions = documentAST.definitions.filter(function (def) { return def.kind === 'OperationDefinition'; }); + 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'; From 28678a2fb2e496411503cae670ab5fa54336a5ce Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Thu, 31 Oct 2024 10:25:13 -0400 Subject: [PATCH 4/4] update --- src/Ui.GraphiQL/Internal/graphiql.cshtml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Ui.GraphiQL/Internal/graphiql.cshtml b/src/Ui.GraphiQL/Internal/graphiql.cshtml index 3d83b2ca..d3f7825a 100644 --- a/src/Ui.GraphiQL/Internal/graphiql.cshtml +++ b/src/Ui.GraphiQL/Internal/graphiql.cshtml @@ -233,11 +233,7 @@ if (err instanceof Error) { throwMe = err; } else if (err instanceof CloseEvent) { - throwMe = new Error( - `Socket closed with event ${err.code} ${ - err.reason || "" - }`.trim() - ); + throwMe = new Error(`Socket closed with event ${err.code} ${err.reason || ""}`.trim()); } else { // GraphQLError[] throwMe = new Error(err.map(({ message }) => message).join(", "));