import { Buffer } from "node:buffer"; import type { TraceEnvelope } from "./types.ts"; export interface IpcSink { write(line: string): boolean; destroy(): void; on(event: "drain", listener: () => void): this; on(event: "error", listener: (error: Error) => void): this; } interface QueueEntry { line: string; bytes: number; } export type IpcDisconnectHandler = (reason: string) => void; export class IpcQueue { private readonly sink: IpcSink; private readonly maxBytes: number; private readonly onDisconnect: IpcDisconnectHandler; private readonly pending: QueueEntry[] = []; private pendingBytes = 0; private blocked = false; private disconnected = false; constructor(sink: IpcSink, maxBytes: number, onDisconnect: IpcDisconnectHandler) { this.sink = sink; this.maxBytes = maxBytes; this.onDisconnect = onDisconnect; this.sink.on("drain", () => this.handleDrain()); this.sink.on("error", (error) => this.disconnect(`Viewer IPC error: ${error.message}`)); } enqueue(_event: TraceEnvelope, line: string): boolean { if (this.disconnected) return false; if (!this.blocked && this.pending.length === 0) return this.writeImmediately(line); const bytes = Buffer.byteLength(line); if (this.pendingBytes + bytes > this.maxBytes) { return this.disconnect("Viewer IPC queue exceeded its byte limit"); } this.pending.push({ line, bytes }); this.pendingBytes += bytes; return true; } private writeImmediately(line: string): boolean { try { this.blocked = !this.sink.write(line); return true; } catch (error) { return this.disconnect(`Viewer IPC write failed: ${error instanceof Error ? error.message : String(error)}`); } } private handleDrain(): void { if (this.disconnected) return; this.blocked = false; while (!this.blocked && this.pending.length > 0) { const entry = this.pending.shift(); if (entry === undefined) break; this.pendingBytes -= entry.bytes; if (!this.writeImmediately(entry.line)) return; } } private disconnect(reason: string): false { if (this.disconnected) return false; this.disconnected = true; this.pending.length = 0; this.pendingBytes = 0; try { this.sink.destroy(); } catch { // Viewer teardown is best effort. } try { this.onDisconnect(reason); } catch { // Status reporting is also observational. } return false; } }