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 | |