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
JavaScript
/**
* 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;
}
}