profileShare

rasmusjy / roundtable

Read-only snapshot

No repository description.

main default branch 181 files Expires Sep 13, 2026, 9:06 AM
sse.ts 1,327 bytes
1 /**
2 * Server-Sent Events plumbing for the debate stream.
3 *
4 * The orchestrator emits a `DebateEvent` union; we serialize each as a named SSE
5 * message so the browser's EventSource-style reader can dispatch on `event:`.
6 * Long-lived (minutes) - heartbeat comments keep intermediaries from closing the
7 * idle connection between slow model calls.
8 */
9 import type { DebateEvent } from '@/core/events';
10
11 export const SSE_HEADERS: Record<string, string> = {
12 'Content-Type': 'text/event-stream; charset=utf-8',
13 'Cache-Control': 'no-cache, no-transform',
14 Connection: 'keep-alive',
15 // Disable proxy buffering (nginx) so events flush immediately.
16 'X-Accel-Buffering': 'no',
17 };
18
19 const encoder = new TextEncoder();
20
21 /** Encode a debate event as an SSE `event:`/`data:` frame. */
22 export function encodeEvent(event: DebateEvent): Uint8Array {
23 return encoder.encode(`event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`);
24 }
25
26 /** A generic named SSE frame (used for `ready`, `error`, `ping`). */
27 export function encodeNamed(name: string, data: unknown): Uint8Array {
28 return encoder.encode(`event: ${name}\ndata: ${JSON.stringify(data)}\n\n`);
29 }
30
31 /** Heartbeat comment - ignored by clients, keeps the socket warm. */
32 export function encodeHeartbeat(): Uint8Array {
33 return encoder.encode(`: ping ${Date.now()}\n\n`);
34 }
35