UNPKG

@beignet/core

Version:

Core framework primitives for Beignet

275 lines 9.91 kB
const DEFAULT_HEARTBEAT_MS = 25_000; const DEFAULT_MAX_BUFFERED_BYTES = 1_048_576; const MAX_BUFFERED_BYTES = 2_147_483_647; const MAX_TIMER_MS = 2_147_483_647; function assertTimerOption(name, value) { if (value === false || value === undefined || (Number.isInteger(value) && value >= 1 && value <= MAX_TIMER_MS)) { return; } throw new RangeError(`${name} must be false or an integer between 1 and ${MAX_TIMER_MS} milliseconds.`); } function assertMaxBufferedBytes(value) { if (Number.isInteger(value) && value >= 1 && value <= MAX_BUFFERED_BYTES) { return; } throw new RangeError(`maxBufferedBytes must be an integer between 1 and ${MAX_BUFFERED_BYTES} bytes.`); } function assertSingleLine(name, value) { if (value.includes("\r") || value.includes("\n")) { throw new TypeError(`SSE ${name} must not contain line breaks.`); } if (name === "id" && value.includes("\0")) { throw new TypeError("SSE id must not contain null characters."); } } function isPromiseLike(value) { return (value !== null && (typeof value === "object" || typeof value === "function") && "then" in value && typeof value.then === "function"); } function isAbortError(error) { try { return (typeof error === "object" && error !== null && "name" in error && error.name === "AbortError"); } catch { return false; } } function encodeMessage(message) { if (message.event !== undefined) { assertSingleLine("event", message.event); } if (message.id !== undefined) { assertSingleLine("id", message.id); } if (message.retry !== undefined && (!Number.isInteger(message.retry) || message.retry < 0 || message.retry > MAX_TIMER_MS)) { throw new RangeError(`SSE retry must be an integer between 0 and ${MAX_TIMER_MS} milliseconds.`); } const data = JSON.stringify(message.data); if (data === undefined) { throw new TypeError("SSE data must be JSON-serializable."); } const fields = []; if (message.event !== undefined) fields.push(`event: ${message.event}`); if (message.id !== undefined) fields.push(`id: ${message.id}`); if (message.retry !== undefined) fields.push(`retry: ${message.retry}`); fields.push(`data: ${data}`); return `${fields.join("\n")}\n\n`; } function encodeComment(value) { return `${value .split(/\r\n|\r|\n/) .map((line) => `: ${line}`) .join("\n")}\n\n`; } function createResponseHeaders(init) { const headers = new Headers(init); headers.set("Content-Type", "text/event-stream; charset=utf-8"); headers.set("Cache-Control", "no-store, no-transform"); headers.set("X-Accel-Buffering", "no"); headers.delete("Connection"); headers.delete("Content-Length"); headers.delete("Transfer-Encoding"); return headers; } /** * Create a portable Fetch `Response` that safely manages a Server-Sent Events * stream. * * The helper owns SSE framing, JSON encoding, heartbeats, abort handling, * bounded unread buffering, maximum lifetime, and cleanup. Authentication, * replay, authorization, connection limits, and application reconciliation * remain the caller's responsibility. */ export function createServerSentEventResponse(options) { const heartbeatMs = options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS; const maxBufferedBytes = options.maxBufferedBytes ?? DEFAULT_MAX_BUFFERED_BYTES; assertTimerOption("heartbeatMs", heartbeatMs); assertTimerOption("maxLifetimeMs", options.maxLifetimeMs); assertMaxBufferedBytes(maxBufferedBytes); const headers = createResponseHeaders(options.headers); const encoder = new TextEncoder(); const lifecycleController = new AbortController(); let cleanup; let cleanupPromise; let closed = false; let heartbeatTimer; let lifetimeTimer; let abortListener; let closeStream; const reportError = (error) => { if (!options.onError) return; void Promise.resolve() .then(() => options.onError?.(error)) .catch(() => undefined); }; const runCleanup = () => { if (cleanupPromise) return cleanupPromise; if (!cleanup) return Promise.resolve(); const activeCleanup = cleanup; cleanupPromise = Promise.resolve() .then(() => typeof activeCleanup === "function" ? activeCleanup() : activeCleanup.close()) .catch(reportError); return cleanupPromise; }; const stream = new ReadableStream({ start(controller) { const clearLifecycle = () => { if (heartbeatTimer !== undefined) clearInterval(heartbeatTimer); if (lifetimeTimer !== undefined) clearTimeout(lifetimeTimer); if (abortListener) { options.signal?.removeEventListener("abort", abortListener); } }; const close = () => { if (closed) return; closed = true; clearLifecycle(); lifecycleController.abort(); void runCleanup(); try { controller.close(); } catch { // Cancellation may already have detached the stream controller. } }; closeStream = close; const enqueue = (value) => { if (closed) return false; try { const chunk = encoder.encode(value); const availableBytes = controller.desiredSize; if (availableBytes === null || chunk.byteLength > availableBytes) { reportError(new RangeError(`SSE buffer limit of ${maxBufferedBytes} bytes exceeded.`)); close(); return false; } controller.enqueue(chunk); return true; } catch (error) { reportError(error); close(); return false; } }; const controls = { signal: lifecycleController.signal, send(message) { if (closed) return false; try { return enqueue(encodeMessage(message)); } catch (error) { reportError(error); close(); return false; } }, comment(value = "") { if (closed) return false; try { return enqueue(encodeComment(value)); } catch (error) { reportError(error); close(); return false; } }, close, }; if (options.signal?.aborted) { close(); return; } abortListener = close; options.signal?.addEventListener("abort", abortListener, { once: true, }); if (heartbeatMs !== false) { heartbeatTimer = setInterval(() => { controls.comment("heartbeat"); }, heartbeatMs); } if (options.maxLifetimeMs !== undefined && options.maxLifetimeMs !== false) { lifetimeTimer = setTimeout(close, options.maxLifetimeMs); } const registerCleanup = (resolvedCleanup) => { try { if (typeof resolvedCleanup === "function" || (typeof resolvedCleanup === "object" && resolvedCleanup !== null && "close" in resolvedCleanup && typeof resolvedCleanup.close === "function")) { cleanup = resolvedCleanup; if (closed) void runCleanup(); } else if (resolvedCleanup !== undefined) { reportError(new TypeError("SSE start must return a cleanup function, a closeable subscription, or nothing.")); close(); } } catch (error) { reportError(error); close(); } }; const handleStartError = (error) => { if (!(closed && isAbortError(error))) reportError(error); close(); }; try { const result = options.start(controls); if (isPromiseLike(result)) { void Promise.resolve(result).then(registerCleanup, handleStartError); } else { registerCleanup(result); } } catch (error) { handleStartError(error); } }, cancel() { closeStream?.(); return runCleanup(); }, }, { highWaterMark: maxBufferedBytes, size: (chunk) => chunk?.byteLength ?? 0, }); return new Response(stream, { status: 200, headers, }); } //# sourceMappingURL=server-sent-events.js.map