@graphql-yoga/plugin-graphql-sse
Version:
GraphQL over Server-Sent Events Protocol plugin for GraphQL Yoga.
101 lines (100 loc) • 4.95 kB
JavaScript
import { createPubSub, isAsyncIterable, map, } from 'graphql-yoga';
/**
* Get [GraphQL over Server-Sent Events Protocol](https://github.com/enisdenjo/graphql-sse/blob/master/PROTOCOL.md) integration with GraphQL Yoga by simply installing this plugin!
*
* Note that the endpoint defaults to `/graphql/stream`, this is where your [graphql-sse](https://github.com/enisdenjo/graphql-sse) client should connect.
*/
export function useGraphQLSSE(options = {
pubsub: createPubSub(),
}) {
const { pubsub } = options;
const tokenByRequest = new WeakMap();
const operationIdByRequest = new WeakMap();
return {
onRequest({ request, url, fetchAPI, endResponse }) {
const method = request.method.toLowerCase();
const token = request.headers.get('X-GraphQL-Event-Stream-Token') ||
url.searchParams.get('token');
const acceptHeader = request.headers.get('Accept');
if (token != null) {
tokenByRequest.set(request, token);
if (acceptHeader?.includes('text/event-stream') && method === 'get') {
const encoder = new fetchAPI.TextEncoder();
endResponse(new fetchAPI.Response(map((str) => encoder.encode(str))(pubsub.subscribe('graphql-sse-subscribe', token)), {
status: 200,
headers: {
'Content-Type': 'text/event-stream',
},
}));
}
}
if (method === 'delete') {
const operationId = url.searchParams.get('operationId');
if (operationId) {
pubsub.publish('graphql-sse-unsubscribe', operationId, true);
}
endResponse(new fetchAPI.Response(null, {
status: 204,
}));
}
if (method === 'put') {
const token = fetchAPI.crypto.randomUUID();
endResponse(new fetchAPI.Response(token, {
status: 201,
statusText: 'Created',
}));
}
},
onParams({ request, params }) {
if (tokenByRequest.has(request) && params?.extensions?.operationId) {
operationIdByRequest.set(request, params.extensions.operationId);
}
},
onResultProcess({ request, result, fetchAPI, serverContext, endResponse }) {
const token = tokenByRequest.get(request);
if (token) {
const operationId = operationIdByRequest.get(request);
// Batching is not supported by GraphQL SSE yet
if (operationId && !Array.isArray(result)) {
serverContext.waitUntil(Promise.resolve().then(async () => {
if (isAsyncIterable(result)) {
const asyncIterator = result[Symbol.asyncIterator]();
pubsub
.subscribe('graphql-sse-unsubscribe', operationId)
.next()
.finally(() => {
asyncIterator.return?.();
});
let iteratorValue;
while (!(iteratorValue = await asyncIterator.next()).done) {
const chunk = iteratorValue.value;
const messageJson = {
id: operationId,
payload: chunk,
};
const messageStr = `event: next\nid: ${operationId}\ndata: ${JSON.stringify(messageJson)}\n\n`;
pubsub.publish('graphql-sse-subscribe', token, messageStr);
}
}
else {
const messageJson = {
id: operationId,
payload: result,
};
const messageStr = `event: next\nid: ${operationId}\ndata: ${JSON.stringify(messageJson)}\n\n`;
pubsub.publish('graphql-sse-subscribe', token, messageStr);
}
const completeMessageJson = {
id: operationId,
};
const completeMessageStr = `event: complete\ndata: ${JSON.stringify(completeMessageJson)}\n\n`;
pubsub.publish('graphql-sse-subscribe', token, completeMessageStr);
}));
endResponse(new fetchAPI.Response(null, {
status: 202,
}));
}
}
},
};
}