diff --git a/.changeset/silent-lemons-learn.md b/.changeset/silent-lemons-learn.md
new file mode 100644
index 00000000000..6307b1f99a2
--- /dev/null
+++ b/.changeset/silent-lemons-learn.md
@@ -0,0 +1,5 @@
+---
+'@graphiql/toolkit': minor
+---
+
+Add GraphQL over SSE support to `createGraphiQLFetcher`.
diff --git a/packages/graphiql-toolkit/README.md b/packages/graphiql-toolkit/README.md
index 8446b1d8890..6eeb2cab5f4 100644
--- a/packages/graphiql-toolkit/README.md
+++ b/packages/graphiql-toolkit/README.md
@@ -14,5 +14,5 @@ that are useful when working with these packages.
- **[`createFetcher`](./docs/create-fetcher.md)** : a utility for creating a
`fetcher` prop implementation for HTTP GET, POST including multipart,
- websockets fetcher
+ GraphQL over SSE and websockets subscriptions
- more to come!
diff --git a/packages/graphiql-toolkit/docs/create-fetcher.md b/packages/graphiql-toolkit/docs/create-fetcher.md
index 939f6e9f872..8b9ca294c9a 100644
--- a/packages/graphiql-toolkit/docs/create-fetcher.md
+++ b/packages/graphiql-toolkit/docs/create-fetcher.md
@@ -2,10 +2,11 @@
A utility for generating a full-featured `fetcher` for GraphiQL including
`@stream`, `@defer` `IncrementalDelivery`and `multipart` and subscriptions using
-`graphql-ws` or the legacy websockets protocol.
+GraphQL over SSE, `graphql-ws` or the legacy websockets protocol.
-Under the hood, it uses [`graphql-ws`](https://www.npmjs.com/package/graphql-ws)
-client and [`meros`](https://www.npmjs.com/package/meros) which act as client
+Under the hood, it uses [`graphql-sse`](https://www.npmjs.com/package/graphql-sse),
+[`graphql-ws`](https://www.npmjs.com/package/graphql-ws) and
+[`meros`](https://www.npmjs.com/package/meros) which act as client
reference implementations of the
[GraphQL over HTTP Working Group Spec](https://github.com/graphql/graphql-over-http)
specification, and the most popular transport spec proposals.
@@ -45,6 +46,54 @@ const root = createRoot(document.getElementById('graphiql'));
root.render();
```
+### Adding GraphQL over SSE subscriptions
+
+First you'll need to install `graphql-sse` as a peer dependency:
+
+```bash
+npm install graphql-sse
+```
+
+When loading GraphiQL from an ESM CDN with an import map, make sure the optional
+`graphql-sse` peer dependency is also mapped if you use `sseUrl`:
+
+```html
+
+```
+
+Just by providing the `sseUrl`, you can generate a `graphql-sse` client. This
+client supports HTTP/Multipart Incremental Delivery for `@defer` and `@stream`,
+_and_ subscriptions over Server-Sent Events.
+
+```jsx
+import * as React from 'react';
+import { createRoot } from 'react-dom/client';
+import { GraphiQL } from 'graphiql';
+import { createGraphiQLFetcher } from '@graphiql/toolkit';
+
+const url = 'https://my-schema.com/graphql';
+
+const sseUrl = 'https://my-schema.com/graphql/stream';
+
+const fetcher = createGraphiQLFetcher({ url, sseUrl });
+
+export const App = () => ;
+
+const root = createRoot(document.getElementById('graphiql'));
+root.render();
+```
+
+You can further customize the `graphql-sse` implementation by creating a custom
+client instance and providing it as the `sseClient` parameter.
+
### Adding `graphql-ws` websockets subscriptions
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
must be compatible with the new `graphql-ws` subscriptions spec for this to
work.
+### `sseUrl`
+
+This generates a `graphql-sse` client using the provided url. Note that a server
+must be compatible with the GraphQL over SSE protocol for this to work. When
+`sseUrl` or `sseClient` is provided, GraphiQL uses SSE for subscriptions instead
+of websockets.
+
+### `sseClient`
+
+Provide your own GraphQL over SSE subscriptions client. Using this option
+bypasses `sseUrl`. In theory, this could be any client using any transport, as
+long as it matches the `graphql-sse` client signature.
+
+### `sseClientOptions`
+
+Provide additional options used when creating a `graphql-sse` client from
+`sseUrl`, for example `singleConnection`.
+
### `wsClient`
Provide your own subscriptions client. Using this option bypasses
@@ -129,6 +196,35 @@ Pass a custom fetch implementation such as `isomorphic-fetch`.
## Customization Examples
+### Custom `sseClient` Example using `graphql-sse`
+
+This example passes a `graphql-sse` client to the `sseClient` option:
+
+```jsx
+import * as React from 'react';
+import { createRoot } from 'react-dom/client';
+import { GraphiQL } from 'graphiql';
+import { createClient } from 'graphql-sse';
+import { createGraphiQLFetcher } from '@graphiql/toolkit';
+
+const url = 'https://my-schema.com/graphql';
+
+const sseUrl = 'https://my-schema.com/graphql/stream';
+
+const fetcher = createGraphiQLFetcher({
+ url,
+ sseClient: createClient({
+ url: sseUrl,
+ singleConnection: true,
+ }),
+});
+
+export const App = () => ;
+
+const root = createRoot(document.getElementById('graphiql'));
+root.render();
+```
+
### Custom `wsClient` Example using `graphql-ws`
This example passes a `graphql-ws` client to the `wsClient` option:
diff --git a/packages/graphiql-toolkit/package.json b/packages/graphiql-toolkit/package.json
index 8b456840bc9..f21feffb774 100644
--- a/packages/graphiql-toolkit/package.json
+++ b/packages/graphiql-toolkit/package.json
@@ -30,6 +30,7 @@
},
"devDependencies": {
"graphql": "^16.9.0",
+ "graphql-sse": "^2.6.0",
"graphql-ws": "^5.5.5",
"isomorphic-fetch": "^3.0.0",
"subscriptions-transport-ws": "0.11.0",
@@ -37,9 +38,13 @@
},
"peerDependencies": {
"graphql": "^15.5.0 || ^16.0.0 || ^17.0.0",
+ "graphql-sse": ">= 2.0.0",
"graphql-ws": ">= 4.5.0"
},
"peerDependenciesMeta": {
+ "graphql-sse": {
+ "optional": true
+ },
"graphql-ws": {
"optional": true
}
diff --git a/packages/graphiql-toolkit/src/create-fetcher/__tests__/buildFetcher.spec.ts b/packages/graphiql-toolkit/src/create-fetcher/__tests__/buildFetcher.spec.ts
index 35a270933c4..b52ec79dcfa 100644
--- a/packages/graphiql-toolkit/src/create-fetcher/__tests__/buildFetcher.spec.ts
+++ b/packages/graphiql-toolkit/src/create-fetcher/__tests__/buildFetcher.spec.ts
@@ -16,14 +16,22 @@ import {
createSimpleFetcher as _createSimpleFetcher,
createWebsocketsFetcherFromClient as _createWebsocketsFetcherFromClient,
createLegacyWebsocketsFetcher as _createLegacyWebsocketsFetcher,
+ getSubscriptionFetcher as _getSubscriptionFetcher,
+ isSubscriptionWithName as _isSubscriptionWithName,
} from '../lib';
import { createClient as _createClient } from 'graphql-ws';
import { SubscriptionClient } from 'subscriptions-transport-ws';
const serverURL = 'http://localhost:3000/graphql';
const wssURL = 'ws://localhost:3000/graphql';
+const sseURL = 'http://localhost:3000/graphql/stream';
const exampleIntrospectionDocument = parse(getIntrospectionQuery());
+const exampleSubscriptionDocument = parse(/* GraphQL */ `
+ subscription Example {
+ example
+ }
+`);
const createWebsocketsFetcherFromUrl = _createWebsocketsFetcherFromUrl as Mock<
typeof _createWebsocketsFetcherFromUrl
@@ -42,6 +50,12 @@ const createWebsocketsFetcherFromClient =
const createLegacyWebsocketsFetcher = _createLegacyWebsocketsFetcher as Mock<
typeof _createLegacyWebsocketsFetcher
>;
+const getSubscriptionFetcher = _getSubscriptionFetcher as Mock<
+ typeof _getSubscriptionFetcher
+>;
+const isSubscriptionWithName = _isSubscriptionWithName as Mock<
+ typeof _isSubscriptionWithName
+>;
describe('createGraphiQLFetcher', () => {
afterEach(() => {
@@ -138,4 +152,31 @@ describe('createGraphiQLFetcher', () => {
expect(createWebsocketsFetcherFromClient.mock.calls).toEqual([]);
expect(createLegacyWebsocketsFetcher.mock.calls).toEqual([]);
});
+
+ it('uses the subscription fetcher for subscription operations', async () => {
+ const subscriptionFetcher = vi.fn(() => ({ data: { example: true } }));
+ isSubscriptionWithName.mockReturnValue(true);
+ getSubscriptionFetcher.mockResolvedValue(subscriptionFetcher);
+
+ const args = {
+ url: serverURL,
+ sseUrl: sseURL,
+ enableIncrementalDelivery: true,
+ };
+ const graphQLParams = {
+ query: 'subscription Example { example }',
+ operationName: 'Example',
+ };
+ const fetcherOpts = {
+ documentAST: exampleSubscriptionDocument,
+ headers: { authorization: 'Bearer token' },
+ };
+
+ const fetcher = createGraphiQLFetcher(args);
+ const result = await fetcher(graphQLParams, fetcherOpts);
+
+ expect(getSubscriptionFetcher.mock.calls).toEqual([[args, fetcherOpts]]);
+ expect(subscriptionFetcher.mock.calls).toEqual([[graphQLParams]]);
+ expect(result).toEqual({ data: { example: true } });
+ });
});
diff --git a/packages/graphiql-toolkit/src/create-fetcher/__tests__/lib.spec.ts b/packages/graphiql-toolkit/src/create-fetcher/__tests__/lib.spec.ts
index c1b425469aa..b382e8d83ab 100644
--- a/packages/graphiql-toolkit/src/create-fetcher/__tests__/lib.spec.ts
+++ b/packages/graphiql-toolkit/src/create-fetcher/__tests__/lib.spec.ts
@@ -2,22 +2,30 @@ import { type Mock, describe, it, expect, vi, afterEach } from 'vitest';
import { parse } from 'graphql';
import {
isSubscriptionWithName,
+ createSubscriptionFetcherFromClient,
+ createSseFetcherFromClient,
+ createSseFetcherFromUrl,
createWebsocketsFetcherFromUrl,
+ getSubscriptionFetcher,
getWsFetcher,
} from '../lib';
import 'isomorphic-fetch';
vi.mock('graphql-ws');
+vi.mock('graphql-sse');
vi.mock('subscriptions-transport-ws');
import { createClient as _createClient } from 'graphql-ws';
+import { createClient as _createSseClient } from 'graphql-sse';
import { SubscriptionClient as _SubscriptionClient } from 'subscriptions-transport-ws';
const createClient = _createClient as Mock;
+const createSseClient = _createSseClient as Mock;
const SubscriptionClient = _SubscriptionClient as Mock;
+const sseURL = 'https://example.com/graphql/stream';
const exampleWithSubscription = parse(/* GraphQL */ `
subscription Example {
@@ -66,6 +74,123 @@ describe('createWebsocketsFetcherFromUrl', () => {
});
});
+describe('createSubscriptionFetcherFromClient', () => {
+ it('adapts subscription clients to async iterables and disposes them', async () => {
+ const unsubscribe = vi.fn();
+ const client = {
+ subscribe: vi.fn((_payload, sink) => {
+ sink.next({ data: { example: true } });
+ return unsubscribe;
+ }),
+ };
+ const fetcher = createSubscriptionFetcherFromClient(client);
+
+ const result = fetcher({
+ query: 'subscription Example { example }',
+ operationName: 'Example',
+ }) as AsyncIterableIterator;
+
+ await expect(result.next()).resolves.toEqual({
+ done: false,
+ value: { data: { example: true } },
+ });
+
+ await result.return?.();
+
+ expect(client.subscribe.mock.calls[0][0]).toEqual({
+ query: 'subscription Example { example }',
+ operationName: 'Example',
+ });
+ expect(unsubscribe).toHaveBeenCalled();
+ });
+
+ it('runs additional owner cleanup after disposing the subscription', async () => {
+ const unsubscribe = vi.fn();
+ const disposeClient = vi.fn();
+ const client = {
+ subscribe: vi.fn(() => unsubscribe),
+ };
+ const fetcher = createSubscriptionFetcherFromClient(
+ client,
+ undefined,
+ disposeClient,
+ );
+
+ const result = fetcher({
+ query: 'subscription Example { example }',
+ operationName: 'Example',
+ }) as AsyncIterableIterator;
+
+ await result.return?.();
+
+ expect(unsubscribe).toHaveBeenCalled();
+ expect(disposeClient).toHaveBeenCalled();
+ });
+});
+
+describe('createSseFetcherFromUrl', () => {
+ afterEach(() => {
+ vi.resetAllMocks();
+ });
+
+ it('creates an SSE client using provided url, headers and client options', async () => {
+ // @ts-expect-error
+ createSseClient.mockReturnValue(true);
+ await createSseFetcherFromUrl(
+ sseURL,
+ { authorization: 'Bearer token' },
+ { singleConnection: true },
+ );
+ expect(createSseClient.mock.calls[0][0]).toEqual({
+ singleConnection: true,
+ url: sseURL,
+ headers: { authorization: 'Bearer token' },
+ });
+ });
+
+ it('disposes internally created SSE clients when the subscription closes', async () => {
+ const unsubscribe = vi.fn();
+ const disposeClient = vi.fn();
+ createSseClient.mockReturnValue({
+ subscribe: vi.fn(() => unsubscribe),
+ iterate: vi.fn(),
+ dispose: disposeClient,
+ });
+ const fetcher = await createSseFetcherFromUrl(sseURL);
+
+ const result = fetcher?.({
+ query: 'subscription Example { example }',
+ operationName: 'Example',
+ }) as AsyncIterableIterator;
+
+ await result.return?.();
+
+ expect(unsubscribe).toHaveBeenCalled();
+ expect(disposeClient).toHaveBeenCalled();
+ });
+});
+
+describe('createSseFetcherFromClient', () => {
+ it('does not dispose caller-owned SSE clients', async () => {
+ const unsubscribe = vi.fn();
+ const disposeClient = vi.fn();
+ const fetcher = createSseFetcherFromClient({
+ subscribe: vi.fn(() => unsubscribe),
+ dispose: disposeClient,
+ });
+
+ const result = fetcher({
+ query: 'subscription Example { example }',
+ operationName: 'Example',
+ }) as AsyncIterableIterator;
+
+ await result.return?.();
+
+ expect(unsubscribe).toHaveBeenCalled();
+ expect(disposeClient).not.toHaveBeenCalled();
+ });
+});
+
describe('getWsFetcher', () => {
afterEach(() => {
vi.resetAllMocks();
@@ -96,6 +221,60 @@ describe('getWsFetcher', () => {
});
});
+describe('getSubscriptionFetcher', () => {
+ afterEach(() => {
+ vi.resetAllMocks();
+ });
+
+ it('prefers a custom sseClient option over websocket options', async () => {
+ // @ts-expect-error
+ createClient.mockReturnValue(true);
+ await getSubscriptionFetcher({
+ url: '',
+ // @ts-expect-error
+ sseClient: true,
+ // @ts-expect-error
+ wsClient: true,
+ subscriptionUrl: 'wss://example',
+ });
+ expect(createSseClient.mock.calls).toHaveLength(0);
+ expect(createClient.mock.calls).toHaveLength(0);
+ });
+
+ it('creates an SSE client when sseUrl is provided and merges headers', async () => {
+ // @ts-expect-error
+ createSseClient.mockReturnValue(true);
+ await getSubscriptionFetcher(
+ {
+ url: '',
+ sseUrl: sseURL,
+ subscriptionUrl: 'wss://example',
+ headers: { 'x-static': 'static' },
+ sseClientOptions: { singleConnection: true },
+ },
+ { headers: { 'x-request': 'request' } },
+ );
+ expect(createSseClient.mock.calls[0][0]).toEqual({
+ singleConnection: true,
+ url: sseURL,
+ headers: { 'x-static': 'static', 'x-request': 'request' },
+ });
+ expect(createClient.mock.calls).toHaveLength(0);
+ });
+
+ it('falls back to websocket options when SSE is not configured', async () => {
+ // @ts-expect-error
+ createClient.mockReturnValue(true);
+ await getSubscriptionFetcher({
+ url: '',
+ subscriptionUrl: 'wss://example',
+ });
+ expect(createClient.mock.calls[0]).toEqual([
+ { connectionParams: {}, url: 'wss://example' },
+ ]);
+ });
+});
+
describe('missing `graphql-ws` dependency', () => {
it('should throw a nice error', async () => {
vi.resetModules();
@@ -117,4 +296,40 @@ describe('missing `graphql-ws` dependency', () => {
/You need to install the 'graphql-ws' package to use websockets when passing a 'subscriptionUrl'/,
);
});
+
+ it('should throw a nice error when ESM import reports a missing module', async () => {
+ vi.resetModules();
+ vi.doMock('graphql-ws', () => {
+ return {
+ createClient: vi.fn().mockImplementation(() => {
+ // eslint-disable-next-line no-throw-literal
+ throw { code: 'ERR_MODULE_NOT_FOUND' };
+ }),
+ };
+ });
+
+ await expect(
+ createWebsocketsFetcherFromUrl('wss://example.com'),
+ ).rejects.toThrow(
+ /You need to install the 'graphql-ws' package to use websockets when passing a 'subscriptionUrl'/,
+ );
+ });
+});
+
+describe('missing `graphql-sse` dependency', () => {
+ it('should throw a nice error', async () => {
+ vi.resetModules();
+ vi.doMock('graphql-sse', () => {
+ return {
+ createClient: vi.fn().mockImplementation(() => {
+ // eslint-disable-next-line no-throw-literal
+ throw { code: 'MODULE_NOT_FOUND' };
+ }),
+ };
+ });
+
+ await expect(createSseFetcherFromUrl(sseURL)).rejects.toThrow(
+ /You need to install the 'graphql-sse' package to use GraphQL over SSE when passing an 'sseUrl'/,
+ );
+ });
});
diff --git a/packages/graphiql-toolkit/src/create-fetcher/createFetcher.ts b/packages/graphiql-toolkit/src/create-fetcher/createFetcher.ts
index b90c65ee83e..05de74cdeb9 100644
--- a/packages/graphiql-toolkit/src/create-fetcher/createFetcher.ts
+++ b/packages/graphiql-toolkit/src/create-fetcher/createFetcher.ts
@@ -4,13 +4,13 @@ import {
createMultipartFetcher,
createSimpleFetcher,
isSubscriptionWithName,
- getWsFetcher,
+ getSubscriptionFetcher,
} from './lib';
/**
* build a GraphiQL fetcher that is:
* - backwards compatible
- * - optionally supports graphql-ws or `
+ * - optionally supports GraphQL over SSE, graphql-ws, or legacy websockets
*/
export function createGraphiQLFetcher(options: CreateFetcherOptions): Fetcher {
const httpFetch =
@@ -41,18 +41,23 @@ export function createGraphiQLFetcher(options: CreateFetcherOptions): Fetcher {
)
: false;
if (isSubscription) {
- const wsFetcher = await getWsFetcher(options, fetcherOpts);
+ const subscriptionFetcher = await getSubscriptionFetcher(
+ options,
+ fetcherOpts,
+ );
- if (!wsFetcher) {
+ if (!subscriptionFetcher) {
throw new Error(
- `Your GraphiQL createFetcher is not properly configured for websocket subscriptions yet. ${
- options.subscriptionUrl
- ? `Provided URL ${options.subscriptionUrl} failed`
- : 'Please provide subscriptionUrl, wsClient or legacyClient option first.'
+ `Your GraphiQL createFetcher is not properly configured for subscriptions yet. ${
+ options.sseUrl
+ ? `Provided SSE URL ${options.sseUrl} failed`
+ : options.subscriptionUrl
+ ? `Provided websocket URL ${options.subscriptionUrl} failed`
+ : 'Please provide sseUrl, sseClient, subscriptionUrl, wsClient or legacyClient option first.'
}`,
);
}
- return wsFetcher(graphQLParams);
+ return subscriptionFetcher(graphQLParams);
}
return httpFetcher(graphQLParams, fetcherOpts);
};
diff --git a/packages/graphiql-toolkit/src/create-fetcher/lib.ts b/packages/graphiql-toolkit/src/create-fetcher/lib.ts
index 6f046d8f41b..1c552b62a97 100644
--- a/packages/graphiql-toolkit/src/create-fetcher/lib.ts
+++ b/packages/graphiql-toolkit/src/create-fetcher/lib.ts
@@ -18,12 +18,19 @@ import type {
FetcherOpts,
ExecutionResultPayload,
CreateFetcherOptions,
+ GraphQLSSEClient,
+ GraphQLSSEClientOptions,
+ SubscriptionClient,
} from './types';
const errorHasCode = (err: unknown): err is { code: string } => {
return typeof err === 'object' && err !== null && 'code' in err;
};
+type GraphQLSSEModule = {
+ createClient: (options: GraphQLSSEClientOptions) => GraphQLSSEClient;
+};
+
/**
* Returns true if the name matches a subscription in the AST
*
@@ -85,7 +92,10 @@ export async function createWebsocketsFetcherFromUrl(
wsClient = createClient({ url, connectionParams });
return createWebsocketsFetcherFromClient(wsClient);
} catch (err) {
- if (errorHasCode(err) && err.code === 'MODULE_NOT_FOUND') {
+ if (
+ errorHasCode(err) &&
+ (err.code === 'MODULE_NOT_FOUND' || err.code === 'ERR_MODULE_NOT_FOUND')
+ ) {
throw new Error(
"You need to install the 'graphql-ws' package to use websockets when passing a 'subscriptionUrl'",
);
@@ -96,29 +106,86 @@ export async function createWebsocketsFetcherFromUrl(
}
/**
- * Create ws/s fetcher using provided wsClient implementation
+ * Create a fetcher using any subscription client that accepts GraphQL params
+ * and pushes execution results into a sink.
*/
-export const createWebsocketsFetcherFromClient =
- (wsClient: Client): Fetcher =>
+export const createSubscriptionFetcherFromClient =
+ (
+ subscriptionClient: SubscriptionClient,
+ normalizeError: (err: any) => any = err => err,
+ onDispose?: () => void,
+ ): Fetcher =>
(graphQLParams: FetcherParams) =>
- makeAsyncIterableIteratorFromSink(sink =>
- wsClient.subscribe(graphQLParams, {
+ makeAsyncIterableIteratorFromSink(sink => {
+ const dispose = subscriptionClient.subscribe(graphQLParams, {
...sink,
error(err) {
- if (err instanceof CloseEvent) {
- sink.error(
- new Error(
- `Socket closed with event ${err.code} ${
- err.reason || ''
- }`.trim(),
- ),
- );
- } else {
- sink.error(err);
- }
+ sink.error(normalizeError(err));
},
- }),
- );
+ });
+ return () => {
+ try {
+ if (typeof dispose === 'function') {
+ dispose();
+ }
+ } finally {
+ onDispose?.();
+ }
+ };
+ });
+
+/**
+ * Create ws/s fetcher using provided wsClient implementation
+ */
+export const createWebsocketsFetcherFromClient = (wsClient: Client): Fetcher =>
+ createSubscriptionFetcherFromClient(wsClient, err => {
+ if (typeof CloseEvent !== 'undefined' && err instanceof CloseEvent) {
+ return new Error(
+ `Socket closed with event ${err.code} ${err.reason || ''}`.trim(),
+ );
+ }
+ return err;
+ });
+
+export async function createSseFetcherFromUrl(
+ url: string,
+ headers?: FetcherOpts['headers'],
+ sseClientOptions?: CreateFetcherOptions['sseClientOptions'],
+): Promise {
+ try {
+ const { createClient } =
+ process.env.USE_IMPORT === 'false'
+ ? (require('graphql-sse') as GraphQLSSEModule)
+ : ((await import('graphql-sse')) as GraphQLSSEModule);
+
+ const sseClient = createClient({
+ ...sseClientOptions,
+ url,
+ headers: headers || {},
+ });
+ return createSseFetcherFromClient(sseClient, () => sseClient.dispose?.());
+ } catch (err) {
+ if (
+ errorHasCode(err) &&
+ (err.code === 'MODULE_NOT_FOUND' || err.code === 'ERR_MODULE_NOT_FOUND')
+ ) {
+ throw new Error(
+ "You need to install the 'graphql-sse' package to use GraphQL over SSE when passing an 'sseUrl'",
+ );
+ }
+ // eslint-disable-next-line no-console
+ console.error(`Error creating SSE client for ${url}`, err);
+ }
+}
+
+/**
+ * Create GraphQL over SSE fetcher using provided sseClient implementation
+ */
+export const createSseFetcherFromClient = (
+ sseClient: GraphQLSSEClient,
+ onDispose?: () => void,
+): Fetcher =>
+ createSubscriptionFetcherFromClient(sseClient, undefined, onDispose);
/**
* Allow legacy websockets protocol client, but no definitions for it,
@@ -198,3 +265,26 @@ export async function getWsFetcher(
return createLegacyWebsocketsFetcher(legacyWebsocketsClient);
}
}
+
+/**
+ * If `sseClient` or `sseUrl` are provided, they take precedence over websocket clients.
+ */
+export async function getSubscriptionFetcher(
+ options: CreateFetcherOptions,
+ fetcherOpts?: FetcherOpts,
+): Promise {
+ if (options.sseClient) {
+ return createSseFetcherFromClient(options.sseClient);
+ }
+ if (options.sseUrl) {
+ return createSseFetcherFromUrl(
+ options.sseUrl,
+ {
+ ...options.headers,
+ ...fetcherOpts?.headers,
+ },
+ options.sseClientOptions,
+ );
+ }
+ return getWsFetcher(options, fetcherOpts);
+}
diff --git a/packages/graphiql-toolkit/src/create-fetcher/types.ts b/packages/graphiql-toolkit/src/create-fetcher/types.ts
index 7d3a9dac011..d49705aadc5 100644
--- a/packages/graphiql-toolkit/src/create-fetcher/types.ts
+++ b/packages/graphiql-toolkit/src/create-fetcher/types.ts
@@ -72,6 +72,26 @@ export type Fetcher = (
opts?: FetcherOpts,
) => FetcherReturnType;
+export type SubscriptionSink = {
+ next: (value: ExecutionResult) => void;
+ error: (error: any) => void;
+ complete: () => void;
+};
+
+export type SubscriptionClient = {
+ subscribe: (payload: any, sink: SubscriptionSink) => void | (() => void);
+};
+
+export type GraphQLSSEClient = SubscriptionClient & {
+ dispose?: () => void;
+};
+
+export type GraphQLSSEClientOptions = {
+ url: string;
+ headers?: HeadersInit | (() => MaybePromise);
+ [option: string]: unknown;
+};
+
/**
* Options for creating a simple, spec-compliant GraphiQL fetcher
*/
@@ -84,6 +104,19 @@ export interface CreateFetcherOptions {
* url for websocket subscription requests
*/
subscriptionUrl?: string;
+ /**
+ * url for GraphQL over SSE subscription requests
+ */
+ sseUrl?: string;
+ /**
+ * `sseClient` implementation that matches `graphql-sse` signature,
+ * whether via `createClient()` itself or another client.
+ */
+ sseClient?: GraphQLSSEClient;
+ /**
+ * Additional `graphql-sse` client options used when you provide `sseUrl`.
+ */
+ sseClientOptions?: Omit;
/**
* `wsClient` implementation that matches `ws-graphql` signature,
* whether via `createClient()` itself or another client.
diff --git a/yarn.lock b/yarn.lock
index c11958dddac..20bdf0decff 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4785,6 +4785,7 @@ __metadata:
dependencies:
"@n1ru4l/push-pull-async-iterable-iterator": "npm:^3.1.0"
graphql: "npm:^16.9.0"
+ graphql-sse: "npm:^2.6.0"
graphql-ws: "npm:^5.5.5"
isomorphic-fetch: "npm:^3.0.0"
meros: "npm:^1.1.4"
@@ -4792,8 +4793,11 @@ __metadata:
tsup: "npm:^8.2.4"
peerDependencies:
graphql: ^15.5.0 || ^16.0.0 || ^17.0.0
+ graphql-sse: ">= 2.0.0"
graphql-ws: ">= 4.5.0"
peerDependenciesMeta:
+ graphql-sse:
+ optional: true
graphql-ws:
optional: true
languageName: unknown
@@ -16592,6 +16596,15 @@ __metadata:
languageName: unknown
linkType: soft
+"graphql-sse@npm:^2.6.0":
+ version: 2.6.0
+ resolution: "graphql-sse@npm:2.6.0"
+ peerDependencies:
+ graphql: ">=0.11 <=16"
+ checksum: 10c0/e05f0b5c8539d61e5ce34af8e0bb418c02bf922d6a7f9232a9abd53c77df5654684ca14f674ac771645c8fba9c37ce666c5d06f46eef54ea07e63653468065b0
+ languageName: node
+ linkType: hard
+
"graphql-tag@npm:2.12.6":
version: 2.12.6
resolution: "graphql-tag@npm:2.12.6"