@beignet/core
Version:
Core framework primitives for Beignet
275 lines • 9.91 kB
JavaScript
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