UNPKG

use-next-sse

Version:

A lightweight Server-Sent Events (SSE) library for Next.js, enabling real-time, unidirectional data streaming from server to client

110 lines (108 loc) 4.56 kB
/** * Creates a Server-Sent Events (SSE) handler for Next.js. * * @param callback - A function that handles the SSE connection. It takes two arguments: {@link SSECallback} * - `send`: A function to send data to the client. See {@link SendFunction}. * - `close`: A function to close the SSE connection. * - `context`: An object containing the last event ID received by the client. Not null if the client has been reconnected. * The callback can return a cleanup function that will be called when the connection is closed. * * **HINT:** * Be sure to **NOT** await long running operations in the callback, as this will block the response from being sent initially to the client. Instead wrap your long running operations in a async function to allow the response to be sent to the client first. * * @example ``` export const GET = createSSEHandler((send, close) => { const asyncStuff = async () => { // async }; asyncStuff(); }); ``` * * @param options - An optional object to configure the handler. {@link SSEOptions} * - `onClose`: A function that will be called when the connection is closed even if the SSECallback has not been called yet. * @returns A function that handles the SSE request. This function takes a `NextRequest` object as an argument. * * The returned function creates a `ReadableStream` to handle the SSE connection. It sets up the `send` and `close` functions, * and listens for the `abort` event on the request signal to close the connection. * * The response is returned with the appropriate headers for SSE: * - `Content-Type`: `text/event-stream` * - `Cache-Control`: `no-cache, no-transform` * - `Connection`: `keep-alive` */ export function createSSEHandler(callback, options) { return async function (request) { const encoder = new TextEncoder(); let isClosed = false; let cleanup = options === null || options === void 0 ? void 0 : options.onClose; let cleanupSetBy = (options === null || options === void 0 ? void 0 : options.onClose) ? '`onClose` through createSSEHandler options' : ''; let messageId = 0; let controller; function onClose(destructor) { if (cleanup != null) { if (!isClosed) { logAlreadySetWarning(cleanupSetBy); } } else { cleanup = destructor; cleanupSetBy = '`onClose` through SSECallback context'; } } function close() { if (!isClosed) { isClosed = true; controller === null || controller === void 0 ? void 0 : controller.close(); if (typeof cleanup === 'function') { cleanup(); } } } request.signal.addEventListener('abort', () => { close(); }); const stream = new ReadableStream({ async start(cntrl) { controller = cntrl; const send = (data, eventName) => { if (!isClosed) { let message = `id: ${messageId}\n`; if (eventName) { message += `event: ${eventName}\n`; } message += `data: ${JSON.stringify(data)}\n\n`; cntrl.enqueue(encoder.encode(message)); messageId++; } }; const result = await callback(send, close, { lastEventId: request.headers.get('Last-Event-ID'), onClose }); if (typeof result === 'function') { if (cleanup != null) { if (!isClosed) { logAlreadySetWarning(cleanupSetBy); } } else { cleanup = result; } } }, }); return new Response(stream, { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache, no-transform', Connection: 'keep-alive', }, }); }; } let warningLogged = false; function logAlreadySetWarning(cleanupSetBy) { if (!warningLogged) { console.warn(`[use-next-sse:createSSEHandler]:\tCleanup function already set by ${cleanupSetBy}. Ignoring new cleanup function.`); warningLogged = true; } }