Skip to content

Commit 05a62ea

Browse files
committed
Add SSE subscription support to GraphiQL fetcher
1 parent e6f37d8 commit 05a62ea

10 files changed

Lines changed: 456 additions & 32 deletions

File tree

.changeset/silent-lemons-learn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@graphiql/toolkit': minor
3+
---
4+
5+
Add GraphQL over SSE support to `createGraphiQLFetcher`.

packages/graphiql-toolkit/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,5 @@ that are useful when working with these packages.
1414

1515
- **[`createFetcher`](./docs/create-fetcher.md)** : a utility for creating a
1616
`fetcher` prop implementation for HTTP GET, POST including multipart,
17-
websockets fetcher
17+
GraphQL over SSE and websockets subscriptions
1818
- more to come!

packages/graphiql-toolkit/docs/create-fetcher.md

Lines changed: 99 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

33
A utility for generating a full-featured `fetcher` for GraphiQL including
44
`@stream`, `@defer` `IncrementalDelivery`and `multipart` and subscriptions using
5-
`graphql-ws` or the legacy websockets protocol.
5+
GraphQL over SSE, `graphql-ws` or the legacy websockets protocol.
66

7-
Under the hood, it uses [`graphql-ws`](https://www.npmjs.com/package/graphql-ws)
8-
client and [`meros`](https://www.npmjs.com/package/meros) which act as client
7+
Under the hood, it uses [`graphql-sse`](https://www.npmjs.com/package/graphql-sse),
8+
[`graphql-ws`](https://www.npmjs.com/package/graphql-ws) and
9+
[`meros`](https://www.npmjs.com/package/meros) which act as client
910
reference implementations of the
1011
[GraphQL over HTTP Working Group Spec](https://github.com/graphql/graphql-over-http)
1112
specification, and the most popular transport spec proposals.
@@ -45,6 +46,54 @@ const root = createRoot(document.getElementById('graphiql'));
4546
root.render(<App />);
4647
```
4748

49+
### Adding GraphQL over SSE subscriptions
50+
51+
First you'll need to install `graphql-sse` as a peer dependency:
52+
53+
```bash
54+
npm install graphql-sse
55+
```
56+
57+
When loading GraphiQL from an ESM CDN with an import map, make sure the optional
58+
`graphql-sse` peer dependency is also mapped if you use `sseUrl`:
59+
60+
```html
61+
<script type="importmap">
62+
{
63+
"imports": {
64+
"@graphiql/toolkit": "https://esm.sh/@graphiql/toolkit",
65+
"graphql": "https://esm.sh/graphql",
66+
"graphql-sse": "https://esm.sh/graphql-sse?external=graphql"
67+
}
68+
}
69+
</script>
70+
```
71+
72+
Just by providing the `sseUrl`, you can generate a `graphql-sse` client. This
73+
client supports HTTP/Multipart Incremental Delivery for `@defer` and `@stream`,
74+
_and_ subscriptions over Server-Sent Events.
75+
76+
```jsx
77+
import * as React from 'react';
78+
import { createRoot } from 'react-dom/client';
79+
import { GraphiQL } from 'graphiql';
80+
import { createGraphiQLFetcher } from '@graphiql/toolkit';
81+
82+
const url = 'https://my-schema.com/graphql';
83+
84+
const sseUrl = 'https://my-schema.com/graphql/stream';
85+
86+
const fetcher = createGraphiQLFetcher({ url, sseUrl });
87+
88+
export const App = () => <GraphiQL fetcher={fetcher} />;
89+
90+
const root = createRoot(document.getElementById('graphiql'));
91+
root.render(<App />);
92+
```
93+
94+
You can further customize the `graphql-sse` implementation by creating a custom
95+
client instance and providing it as the `sseClient` parameter.
96+
4897
### Adding `graphql-ws` websockets subscriptions
4998

5099
First you'll need to install `graphql-ws` as a peer dependency:
@@ -90,6 +139,24 @@ This generates a `graphql-ws` client using the provided url. Note that a server
90139
must be compatible with the new `graphql-ws` subscriptions spec for this to
91140
work.
92141

142+
### `sseUrl`
143+
144+
This generates a `graphql-sse` client using the provided url. Note that a server
145+
must be compatible with the GraphQL over SSE protocol for this to work. When
146+
`sseUrl` or `sseClient` is provided, GraphiQL uses SSE for subscriptions instead
147+
of websockets.
148+
149+
### `sseClient`
150+
151+
Provide your own GraphQL over SSE subscriptions client. Using this option
152+
bypasses `sseUrl`. In theory, this could be any client using any transport, as
153+
long as it matches the `graphql-sse` client signature.
154+
155+
### `sseClientOptions`
156+
157+
Provide additional options used when creating a `graphql-sse` client from
158+
`sseUrl`, for example `singleConnection`.
159+
93160
### `wsClient`
94161

95162
Provide your own subscriptions client. Using this option bypasses
@@ -129,6 +196,35 @@ Pass a custom fetch implementation such as `isomorphic-fetch`.
129196

130197
## Customization Examples
131198

199+
### Custom `sseClient` Example using `graphql-sse`
200+
201+
This example passes a `graphql-sse` client to the `sseClient` option:
202+
203+
```jsx
204+
import * as React from 'react';
205+
import { createRoot } from 'react-dom/client';
206+
import { GraphiQL } from 'graphiql';
207+
import { createClient } from 'graphql-sse';
208+
import { createGraphiQLFetcher } from '@graphiql/toolkit';
209+
210+
const url = 'https://my-schema.com/graphql';
211+
212+
const sseUrl = 'https://my-schema.com/graphql/stream';
213+
214+
const fetcher = createGraphiQLFetcher({
215+
url,
216+
sseClient: createClient({
217+
url: sseUrl,
218+
singleConnection: true,
219+
}),
220+
});
221+
222+
export const App = () => <GraphiQL fetcher={fetcher} />;
223+
224+
const root = createRoot(document.getElementById('graphiql'));
225+
root.render(<App />);
226+
```
227+
132228
### Custom `wsClient` Example using `graphql-ws`
133229

134230
This example passes a `graphql-ws` client to the `wsClient` option:

packages/graphiql-toolkit/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,21 @@
3030
},
3131
"devDependencies": {
3232
"graphql": "^16.9.0",
33+
"graphql-sse": "^2.6.0",
3334
"graphql-ws": "^5.5.5",
3435
"isomorphic-fetch": "^3.0.0",
3536
"subscriptions-transport-ws": "0.11.0",
3637
"tsup": "^8.2.4"
3738
},
3839
"peerDependencies": {
3940
"graphql": "^15.5.0 || ^16.0.0 || ^17.0.0",
41+
"graphql-sse": ">= 2.0.0",
4042
"graphql-ws": ">= 4.5.0"
4143
},
4244
"peerDependenciesMeta": {
45+
"graphql-sse": {
46+
"optional": true
47+
},
4348
"graphql-ws": {
4449
"optional": true
4550
}

packages/graphiql-toolkit/src/create-fetcher/__tests__/buildFetcher.spec.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,22 @@ import {
1616
createSimpleFetcher as _createSimpleFetcher,
1717
createWebsocketsFetcherFromClient as _createWebsocketsFetcherFromClient,
1818
createLegacyWebsocketsFetcher as _createLegacyWebsocketsFetcher,
19+
getSubscriptionFetcher as _getSubscriptionFetcher,
20+
isSubscriptionWithName as _isSubscriptionWithName,
1921
} from '../lib';
2022
import { createClient as _createClient } from 'graphql-ws';
2123
import { SubscriptionClient } from 'subscriptions-transport-ws';
2224

2325
const serverURL = 'http://localhost:3000/graphql';
2426
const wssURL = 'ws://localhost:3000/graphql';
27+
const sseURL = 'http://localhost:3000/graphql/stream';
2528

2629
const exampleIntrospectionDocument = parse(getIntrospectionQuery());
30+
const exampleSubscriptionDocument = parse(/* GraphQL */ `
31+
subscription Example {
32+
example
33+
}
34+
`);
2735

2836
const createWebsocketsFetcherFromUrl = _createWebsocketsFetcherFromUrl as Mock<
2937
typeof _createWebsocketsFetcherFromUrl
@@ -42,6 +50,12 @@ const createWebsocketsFetcherFromClient =
4250
const createLegacyWebsocketsFetcher = _createLegacyWebsocketsFetcher as Mock<
4351
typeof _createLegacyWebsocketsFetcher
4452
>;
53+
const getSubscriptionFetcher = _getSubscriptionFetcher as Mock<
54+
typeof _getSubscriptionFetcher
55+
>;
56+
const isSubscriptionWithName = _isSubscriptionWithName as Mock<
57+
typeof _isSubscriptionWithName
58+
>;
4559

4660
describe('createGraphiQLFetcher', () => {
4761
afterEach(() => {
@@ -138,4 +152,31 @@ describe('createGraphiQLFetcher', () => {
138152
expect(createWebsocketsFetcherFromClient.mock.calls).toEqual([]);
139153
expect(createLegacyWebsocketsFetcher.mock.calls).toEqual([]);
140154
});
155+
156+
it('uses the subscription fetcher for subscription operations', async () => {
157+
const subscriptionFetcher = vi.fn(() => ({ data: { example: true } }));
158+
isSubscriptionWithName.mockReturnValue(true);
159+
getSubscriptionFetcher.mockResolvedValue(subscriptionFetcher);
160+
161+
const args = {
162+
url: serverURL,
163+
sseUrl: sseURL,
164+
enableIncrementalDelivery: true,
165+
};
166+
const graphQLParams = {
167+
query: 'subscription Example { example }',
168+
operationName: 'Example',
169+
};
170+
const fetcherOpts = {
171+
documentAST: exampleSubscriptionDocument,
172+
headers: { authorization: 'Bearer token' },
173+
};
174+
175+
const fetcher = createGraphiQLFetcher(args);
176+
const result = await fetcher(graphQLParams, fetcherOpts);
177+
178+
expect(getSubscriptionFetcher.mock.calls).toEqual([[args, fetcherOpts]]);
179+
expect(subscriptionFetcher.mock.calls).toEqual([[graphQLParams]]);
180+
expect(result).toEqual({ data: { example: true } });
181+
});
141182
});

0 commit comments

Comments
 (0)