import type { TraceEnvelope } from "../src/types.ts"; export interface ViewerSnapshot { readonly events: readonly TraceEnvelope[]; readonly turns: Readonly>; readonly spans: Readonly>; } export type ViewerEventSubscriber = (event: TraceEnvelope) => void; function groupEventIds(events: readonly TraceEnvelope[], field: "turnId" | "spanId"): Record { const mutable: Record = {}; for (const event of events) { const id = event[field]; if (id === undefined) continue; (mutable[id] ??= []).push(event.eventId); } return mutable; } export class ViewerEventStore { private readonly eventIds = new Set(); private readonly events: TraceEnvelope[] = []; private readonly subscribers = new Set(); ingest(event: TraceEnvelope): boolean { if (this.eventIds.has(event.eventId)) return false; this.eventIds.add(event.eventId); this.events.push(event); for (const subscriber of this.subscribers) { try { subscriber(event); } catch { // One browser subscriber must not stop ingestion for the others. } } return true; } snapshot(): ViewerSnapshot { const events = [...this.events].sort((left, right) => { const runOrder = left.runId.localeCompare(right.runId); if (runOrder !== 0) return runOrder; if (left.seq !== right.seq) return left.seq - right.seq; return left.eventId.localeCompare(right.eventId); }); return { events, turns: groupEventIds(events, "turnId"), spans: groupEventIds(events, "spanId"), }; } subscribe(subscriber: ViewerEventSubscriber): () => void { this.subscribers.add(subscriber); return () => { this.subscribers.delete(subscriber); }; } }