Bound live event stream queues (Refs #71)

This commit is contained in:
2026-10-09 12:41:23 -04:00
parent 0abe5ba820
commit deb2d21f29
4 changed files with 244 additions and 73 deletions
+60
View File
@@ -0,0 +1,60 @@
import { subscribe, type PipelineEvent } from './bus.js';
const encoder = new TextEncoder();
const hello = encoder.encode(': hello\n\n');
const ping = encoder.encode(': ping\n\n');
const HEARTBEAT_MS = 20_000;
// Allow snapshots and short bursts, but never retain an unbounded event history.
const MAX_QUEUED_FRAMES = 64;
interface EventStreamOptions {
accepts: (event: PipelineEvent) => boolean;
encode: (event: PipelineEvent) => Uint8Array;
initialFrames?: Uint8Array[];
}
export function eventStreamResponse({ accepts, encode, initialFrames = [] }: EventStreamOptions): Response {
let heartbeat: ReturnType<typeof setInterval> | undefined;
let unsubscribe: (() => void) | undefined;
let closed = false;
const cleanup = () => {
closed = true;
if (heartbeat) clearInterval(heartbeat);
unsubscribe?.();
heartbeat = undefined;
unsubscribe = undefined;
};
const stream = new ReadableStream<Uint8Array>({
start(controller) {
const send = (frame: () => Uint8Array) => {
if (closed) return;
if ((controller.desiredSize ?? 0) <= 0) {
// Reconnect for fresh state instead of silently losing deltas.
// close() does not invoke cancel(), so release resources here.
cleanup();
controller.close();
return;
}
try { controller.enqueue(frame()); }
catch (cause) { cleanup(); controller.error(cause); }
};
send(() => hello);
for (const frame of initialFrames) send(() => frame);
if (closed) return;
unsubscribe = subscribe(event => {
if (accepts(event)) send(() => encode(event));
});
heartbeat = setInterval(() => send(() => ping), HEARTBEAT_MS);
},
cancel: cleanup
}, { highWaterMark: MAX_QUEUED_FRAMES });
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache, no-transform',
Connection: 'keep-alive'
}
});
}