Skip to content

Commit

Permalink
Enable GraphQLWs subscription transport for GraphiQL
Browse files Browse the repository at this point in the history
  • Loading branch information
Shane32 committed Oct 31, 2024
1 parent b12f1c0 commit fc4d8ea
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 9 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 4 additions & 3 deletions samples/Samples.Basic/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@
app.UseWebSockets();
// configure the graphql endpoint at "/graphql"
app.UseGraphQL("/graphql");
// configure Playground at "/"
app.UseGraphQLPlayground(
// configure GraphiQL at "/"
app.UseGraphQLGraphiQL(
"/",
new GraphQL.Server.Ui.Playground.PlaygroundOptions
new GraphQL.Server.Ui.GraphiQL.GraphiQLOptions
{
GraphQLEndPoint = "/graphql",
SubscriptionsEndPoint = "/graphql",
//GraphQLWsSubscriptions = true,
});

await app.RunAsync();
2 changes: 1 addition & 1 deletion samples/Samples.Basic/Samples.Basic.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<ItemGroup>
<ProjectReference Include="..\..\src\Transports.AspNetCore\Transports.AspNetCore.csproj" />
<ProjectReference Include="..\Samples.Schemas.Chat\Samples.Schemas.Chat.csproj" />
<ProjectReference Include="..\..\src\Ui.Playground\Ui.Playground.csproj" />
<ProjectReference Include="..\..\src\Ui.GraphiQL\Ui.GraphiQL.csproj" />
</ItemGroup>

</Project>
5 changes: 5 additions & 0 deletions src/Ui.GraphiQL/GraphiQLOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,9 @@ public class GraphiQLOptions
/// See <see href="https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials"/>.
/// </remarks>
public RequestCredentials RequestCredentials { get; set; } = RequestCredentials.SameOrigin;

/// <summary>
/// Use the graphql-ws package instead of the subscription-transports-ws package for subscriptions.
/// </summary>
public bool GraphQLWsSubscriptions { get; set; }
}
3 changes: 2 additions & 1 deletion src/Ui.GraphiQL/Internal/GraphiQLPageModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
87 changes: 84 additions & 3 deletions src/Ui.GraphiQL/Internal/graphiql.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@
integrity="sha384-ArTEHLNWIe9TuoDpFEtD/NeztNdWn3SdmWwMiAuZaSJeOaYypEGzeQoBxuPO+ORM"
crossorigin="anonymous"
></script>
<script
src="https://unpkg.com/graphql-ws@5.16.0/umd/graphql-ws.min.js"
integrity="sha384-oEPbisbEBMo7iCrbQcKx244HXUjGnF1jyS8hkVZ3oCwnw9c9oLfY70c1RKeKj3+i"
crossorigin="anonymous"
></script>

</head>
<body>
Expand Down Expand Up @@ -188,22 +193,98 @@
// if location is absolute (e.g. "/api") then prepend host only
return (window.location.protocol === "http:" ? "ws://" : "wss://") + window.location.host + subscriptionsEndPoint;
}
var 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 <GraphiQL /> into the body.
// See the README in the top level of this module to learn more about
// how you can customize GraphiQL by providing different values or
// additional child elements.
ReactDOM.render(
React.createElement(@Model.GraphiQLElement, {
fetcher: subscriptionsFetcher,
fetcher: subscriptionFetcher,
query: parameters.query,
variables: parameters.variables,
operationName: parameters.operationName,
Expand Down

0 comments on commit fc4d8ea

Please sign in to comment.