Getting started
Install
yarn add graphql-sse
Create a GraphQL schema
import { GraphQLSchema, GraphQLObjectType, GraphQLString } from 'graphql';
const schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
fields: {
hello: {
type: GraphQLString,
resolve: () => 'world',
},
},
}),
subscription: new GraphQLObjectType({
name: 'Subscription',
fields: {
greetings: {
type: GraphQLString,
subscribe: async function* () {
for (const hi of ['Hi', 'Bonjour', 'Hola', 'Ciao', 'Zdravo']) {
yield { greetings: hi };
}
},
},
},
}),
});
Start the server
import http from 'http';
import { createHandler } from 'graphql-sse/lib/use/http';
import { schema } from './previous-step';
const handler = createHandler({ schema });
const server = http.createServer((req, res) => {
if (req.url.startsWith('/graphql/stream')) {
return handler(req, res);
}
res.writeHead(404).end();
});
server.listen(4000);
console.log('Listening to port 4000');
Browsers might complain about self-signed SSL/TLS certificates. Help can be found on StackOverflow.
$ openssl req -x509 -newkey rsa:2048 -nodes -sha256 -subj '/CN=localhost' \
-keyout localhost-privkey.pem -out localhost-cert.pem
import fs from 'fs';
import http2 from 'http2';
import { createHandler } from 'graphql-sse/lib/use/http2';
import { schema } from './previous-step';
const handler = createHandler({ schema });
const server = http2.createServer((req, res) => {
if (req.url.startsWith('/graphql/stream')) {
return handler(req, res);
}
res.writeHead(404).end();
});
server.listen(4000);
console.log('Listening to port 4000');
import express from 'express';
import { createHandler } from 'graphql-sse/lib/use/express';
import { schema } from './previous-step';
const handler = createHandler({ schema });
const app = express();
app.use('/graphql/stream', handler);
server.listen(4000);
console.log('Listening to port 4000');
import Fastify from 'fastify';
import { createHandler } from 'graphql-sse/lib/use/fastify';
const handler = createHandler({ schema });
const fastify = Fastify();
fastify.all('/graphql/stream', handler);
fastify.listen({ port: 4000 });
console.log('Listening to port 4000');
import { serve } from 'https://deno.land/std/http/server.ts';
import { createHandler } from 'https://esm.sh/graphql-sse/lib/use/fetch';
import { schema } from './previous-step';
const handler = createHandler({ schema });
await serve(
(req: Request) => {
const [path, _search] = req.url.split('?');
if (path.endsWith('/graphql/stream')) {
return await handler(req);
}
return new Response(null, { status: 404 });
},
{
port: 4000,
},
);
import { createHandler } from 'graphql-sse/lib/use/fetch';
import { schema } from './previous-step';
const handler = createHandler({ schema });
export default {
port: 4000,
async fetch(req) {
const [path, _search] = req.url.split('?');
if (path.endsWith('/graphql/stream')) {
return await handler(req);
}
return new Response(null, { status: 404 });
},
};
Use the client
import { createClient } from 'graphql-sse';
const client = createClient({
url: 'http://localhost:4000/graphql/stream',
});
(async () => {
const result = await new Promise((resolve, reject) => {
let result;
client.subscribe(
{
query: '{ hello }',
},
{
next: (data) => (result = data),
error: reject,
complete: () => resolve(result),
},
);
});
expect(result).toEqual({ hello: 'world' });
})();
(async () => {
const onNext = () => {
};
let unsubscribe = () => {
};
await new Promise((resolve, reject) => {
unsubscribe = client.subscribe(
{
query: 'subscription { greetings }',
},
{
next: onNext,
error: reject,
complete: resolve,
},
);
});
expect(onNext).toBeCalledTimes(5);
})();
Recipes
🔗 Client usage with Promise
import { createClient, RequestParams } from 'graphql-sse';
const client = createClient({
url: 'http://hey.there:4000/graphql/stream',
});
export async function execute<T>(payload: RequestParams) {
return new Promise<T>((resolve, reject) => {
let result: T;
client.subscribe<T>(payload, {
next: (data) => (result = data),
error: reject,
complete: () => resolve(result),
});
});
}
(async () => {
try {
const result = await execute({
query: '{ hello }',
});
} catch (err) {
}
})();
🔗 Client usage with AsyncIterator
import { createClient, RequestParams } from 'graphql-sse';
const client = createClient({
url: 'http://iterators.ftw:4000/graphql/stream',
});
export function subscribe(payload) {
let deferred = null;
const pending = [];
let throwMe = null,
done = false;
const dispose = client.subscribe(payload, {
next: (data) => {
pending.push(data);
deferred?.resolve(false);
},
error: (err) => {
throwMe = err;
deferred?.reject(throwMe);
},
complete: () => {
done = true;
deferred?.resolve(true);
},
});
return {
[Symbol.asyncIterator]() {
return this;
},
async next() {
if (done) return { done: true, value: undefined };
if (throwMe) throw throwMe;
if (pending.length) return { value: pending.shift() };
return (await new Promise(
(resolve, reject) => (deferred = { resolve, reject }),
))
? { done: true, value: undefined }
: { value: pending.shift() };
},
async throw(err) {
throwMe = err;
deferred?.reject(throwMe);
return { done: true, value: undefined };
},
async return() {
done = true;
deferred?.resolve(true);
dispose();
return { done: true, value: undefined };
},
};
}
(async () => {
const subscription = subscribe({
query: 'subscription { greetings }',
});
for await (const result of subscription) {
}
})();
🔗 Client usage with Observable
import { Observable } from 'relay-runtime';
import { Observable } from '@apollo/client/core';
import { Observable } from 'rxjs';
import Observable from 'zen-observable';
const client = createClient({
url: 'http://graphql.loves:4000/observables',
});
export function toObservable(operation) {
return new Observable((observer) =>
client.subscribe(operation, {
next: (data) => observer.next(data),
error: (err) => observer.error(err),
complete: () => observer.complete(),
}),
);
}
const observable = toObservable({ query: `subscription { ping }` });
const subscription = observable.subscribe({
next: (data) => {
expect(data).toBe({ data: { ping: 'pong' } });
},
});
subscription.unsubscribe();
🔗 Client usage with Relay
import { GraphQLError } from 'graphql';
import {
Network,
Observable,
RequestParameters,
Variables,
} from 'relay-runtime';
import { createClient } from 'graphql-sse';
const subscriptionsClient = createClient({
url: 'http://i.love:4000/graphql/stream',
headers: () => {
const session = getSession();
if (!session) return {};
return {
Authorization: `Bearer ${session.token}`,
};
},
});
function fetchOrSubscribe(operation: RequestParameters, variables: Variables) {
return Observable.create((sink) => {
if (!operation.text) {
return sink.error(new Error('Operation text cannot be empty'));
}
return subscriptionsClient.subscribe(
{
operationName: operation.name,
query: operation.text,
variables,
},
sink,
);
});
}
export const network = Network.create(fetchOrSubscribe, fetchOrSubscribe);
🔗 Client usage with urql
import { createClient, defaultExchanges, subscriptionExchange } from 'urql';
import { createClient as createSSEClient } from 'graphql-sse';
const sseClient = createSSEClient({
url: 'http://its.urql:4000/graphql/stream',
});
export const client = createClient({
url: '/graphql/stream',
exchanges: [
...defaultExchanges,
subscriptionExchange({
forwardSubscription(operation) {
return {
subscribe: (sink) => {
const dispose = sseClient.subscribe(operation, sink);
return {
unsubscribe: dispose,
};
},
};
},
}),
],
});
🔗 Client usage with Apollo
import {
ApolloLink,
Operation,
FetchResult,
Observable,
} from '@apollo/client/core';
import { print, GraphQLError } from 'graphql';
import { createClient, ClientOptions, Client } from 'graphql-sse';
class SSELink extends ApolloLink {
private client: Client;
constructor(options: ClientOptions) {
super();
this.client = createClient(options);
}
public request(operation: Operation): Observable<FetchResult> {
return new Observable((sink) => {
return this.client.subscribe<FetchResult>(
{ ...operation, query: print(operation.query) },
{
next: sink.next.bind(sink),
complete: sink.complete.bind(sink),
error: sink.error.bind(sink),
},
);
});
}
}
export const link = new SSELink({
url: 'http://where.is:4000/graphql/stream',
headers: () => {
const session = getSession();
if (!session) return {};
return {
Authorization: `Bearer ${session.token}`,
};
},
});
🔗 Client usage for HTTP/1 (aka. single connection mode)
import { createClient } from 'graphql-sse';
export const client = createClient({
singleConnection: true,
url: 'http://use.single:4000/connection/graphql/stream',
});
🔗 Client usage with custom retry timeout strategy
import { createClient } from 'graphql-sse';
import { waitForHealthy } from './my-servers';
const url = 'http://i.want.retry:4000/control/graphql/stream';
export const client = createClient({
url,
retryWait: async function waitForServerHealthyBeforeRetry() {
await waitForHealthy(url);
await new Promise((resolve) =>
setTimeout(resolve, 1000 + Math.random() * 3000),
);
},
});
🔗 Client usage with logging of incoming messages (browsers don't show them in the DevTools)
import { createClient } from 'graphql-sse';
export const client = createClient({
url: 'http://let-me-see.messages:4000/graphql/stream',
onMessage: console.log,
});
🔗 Client usage in browser
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>GraphQL over Server-Sent Events</title>
<script
type="text/javascript"
src="https://unpkg.com/graphql-sse/umd/graphql-sse.min.js"
></script>
</head>
<body>
<script type="text/javascript">
const client = graphqlSse.createClient({
url: 'http://umdfor.the:4000/win/graphql/stream',
});
</script>
</body>
</html>
🔗 Client usage in Node
const ws = require('ws');
const fetch = require('node-fetch');
const { AbortController } = require('node-abort-controller');
const Crypto = require('crypto');
const { createClient } = require('graphql-sse');
export const client = createClient({
url: 'http://no.browser:4000/graphql/stream',
fetchFn: fetch,
abortControllerImpl: AbortController,
generateID: () =>
([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) =>
(c ^ (Crypto.randomBytes(1)[0] & (15 >> (c / 4)))).toString(16),
),
});
🔗 Server handler usage with custom authentication
import { createHandler } from 'graphql-sse';
import {
schema,
getOrCreateTokenFromCookies,
customAuthenticationTokenDiscovery,
processAuthorizationHeader,
} from './my-graphql';
export const handler = createHandler({
schema,
authenticate: async (req) => {
let token = req.headers.get('x-graphql-event-stream-token');
if (token) {
return Array.isArray(token) ? token.join('') : token;
}
token = getOrCreateTokenFromCookies(req);
token = processAuthorizationHeader(req.headers.get('authorization'));
token = await customAuthenticationTokenDiscovery(req);
if (!token) {
return [null, { status: 401, statusText: 'Unauthorized' }];
}
if (
req.method === 'POST' &&
req.headers.get('accept') === 'text/event-stream'
) {
return '';
}
return token;
},
});
🔗 Server handler usage with dynamic schema
import { createHandler } from 'graphql-sse';
import { schema, checkIsAdmin, getDebugSchema } from './my-graphql';
export const handler = createHandler({
schema: async (req, executionArgsWithoutSchema) => {
const isAdmin = await checkIsAdmin(req);
if (isAdmin) return getDebugSchema(req, executionArgsWithoutSchema);
return schema;
},
});
🔗 Server handler usage with custom context value
import { createHandler } from 'graphql-sse';
import { schema, getDynamicContext } from './my-graphql';
export const handler = createHandler({
schema,
context: (req, args) => {
return getDynamicContext(req, args);
},
});
🔗 Server handler usage with custom execution arguments
import { parse } from 'graphql';
import { createHandler } from 'graphql-sse';
import { getSchema, myValidationRules } from './my-graphql';
export const handler = createHandler({
onSubscribe: async (req, params) => {
const schema = await getSchema(req);
return {
schema,
operationName: params.operationName,
document:
typeof params.query === 'string' ? parse(params.query) : params.query,
variableValues: params.variables,
contextValue: undefined,
};
},
});
🔗 Server handler and client usage with persisted queries
import { parse, ExecutionArgs } from 'graphql';
import { createHandler } from 'graphql-sse';
import { schema } from './my-graphql';
type QueryID = string;
const queriesStore: Record<QueryID, ExecutionArgs> = {
iWantTheGreetings: {
schema,
document: parse('subscription Greetings { greetings }'),
},
};
export const handler = createHandler({
onSubscribe: (_req, params) => {
const persistedQuery =
queriesStore[String(params.extensions?.persistedQuery)];
if (persistedQuery) {
return {
...persistedQuery,
variableValues: params.variables,
contextValue: undefined,
};
}
return [null, { status: 404, statusText: 'Not Found' }];
},
});
import { createClient } from 'graphql-sse';
const client = createClient({
url: 'http://persisted.graphql:4000/queries',
});
(async () => {
const onNext = () => {
};
await new Promise((resolve, reject) => {
client.subscribe(
{
query: '',
extensions: {
persistedQuery: 'iWantTheGreetings',
},
},
{
next: onNext,
error: reject,
complete: resolve,
},
);
});
expect(onNext).toBeCalledTimes(5);
})();
Check the docs folder out for TypeDoc generated documentation.
Read about the exact transport intricacies used by the library in the GraphQL over Server-Sent Events Protocol document.
File a bug, contribute with code, or improve documentation? Read up on our guidelines for contributing and drive development with yarn test --watch
away!