import { inject, injectable, CoreTokens } from "@codemation/core"; import type { RunEvent, RunEventBus, RunEventSubscription } from "@codemation/core"; import type { TelemetrySpanUpsert } from "../../../domain/telemetry/TelemetryContracts"; import type { TelemetrySpanSseFanout } from "../../../application/telemetry/TelemetrySpanSseFanout"; import type { SseSpanSink } from "../../../application/telemetry/SseSpanSink.types"; const PING_FRAME = ": ping\n\n"; export interface RunSseStreamOptions { belongsToStream: (e: RunEvent) => boolean; isTerminal: (e: RunEvent) => boolean; runId?: string; telemetryFanout?: TelemetrySpanSseFanout; heartbeatMs?: number; } export interface SseStreamController { start(): Promise; bind(preamble: unknown, options: RunSseStreamOptions): void; readonly stream: ReadableStream; } @injectable() export class RunSseStreamHelper { constructor( @inject(CoreTokens.RunEventBus) private readonly runEventBus: RunEventBus, ) {} createStream(): SseStreamController { const bus = this.runEventBus; // eslint-disable-next-line codemation/no-manual-di-new -- TextEncoder is a Web API, not a DI-managed class const encoder = new TextEncoder(); let streamController: ReadableStreamDefaultController | null = null; let subscription: RunEventSubscription | null = null; let heartbeatHandle: ReturnType | null = null; let closed = false; const preBindBuffer: RunEvent[] = []; let liveOptions: RunSseStreamOptions | null = null; let spanSink: SseSpanSink | null = null; function send(text: string): void { if (closed || !streamController) return; try { streamController.enqueue(encoder.encode(text)); } catch {} } function encodeFrame(envelope: unknown): string { return `data: ${JSON.stringify(envelope)}\n\n`; } function cleanup(): void { if (closed) return; closed = true; if (heartbeatHandle !== null) { clearInterval(heartbeatHandle); heartbeatHandle = null; } if (spanSink !== null && liveOptions?.runId && liveOptions.telemetryFanout) { liveOptions.telemetryFanout.unregister(liveOptions.runId, spanSink); spanSink = null; } if (subscription !== null) { subscription.close().catch(() => {}); subscription = null; } } function handleEvent(event: RunEvent): void { if (liveOptions === null) { preBindBuffer.push(event); return; } if (!liveOptions.belongsToStream(event)) return; send(encodeFrame({ kind: "event", event })); if (liveOptions.isTerminal(event)) { send(encodeFrame({ kind: "done" })); cleanup(); if (streamController) { try { streamController.close(); } catch {} } } } // eslint-disable-next-line codemation/no-manual-di-new -- ReadableStream is a Web API, not a DI-managed class const stream = new ReadableStream({ start(c) { streamController = c; }, cancel() { cleanup(); }, }); const ctrl: SseStreamController = { stream, async start(): Promise { subscription = await bus.subscribe(handleEvent); }, bind(preamble: unknown, options: RunSseStreamOptions): void { liveOptions = options; if (options.runId && options.telemetryFanout) { spanSink = { send(span: TelemetrySpanUpsert): void { send(encodeFrame({ kind: "telemetryEvent", runId: options.runId, span })); }, }; options.telemetryFanout.register(options.runId, spanSink); } send(encodeFrame({ kind: "preamble", result: preamble })); for (const event of preBindBuffer) { handleEvent(event); if (closed) break; } preBindBuffer.length = 0; if (closed) return; const intervalMs = options.heartbeatMs ?? 15_000; heartbeatHandle = setInterval(() => { if (closed) { clearInterval(heartbeatHandle!); return; } send(PING_FRAME); }, intervalMs); if (typeof heartbeatHandle === "object" && heartbeatHandle !== null && "unref" in heartbeatHandle) { (heartbeatHandle as { unref(): void }).unref(); } }, }; return ctrl; } }