Bound live event stream queues (Refs #71)
This commit is contained in:
@@ -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'
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user