/** * @fileoverview Server-Sent Events (SSE) event bus. * * The bus maintains a Set of connected SSE clients (each is an * `http.ServerResponse` already configured for SSE). When the plugin * records a tool call or a task status change, it calls * `eventBus.broadcast(event)` and every connected client receives a * standard SSE `data:` frame. * * The bus also handles client disconnect cleanup and periodic keep-alive * pings (so reverse proxies / load balancers don't drop the connection). * * The implementation is intentionally tiny — no external dependencies, no * abstractions beyond what the plugin needs. Each subscriber is identified * only by a numeric id (assigned on `subscribe`) so `unsubscribe` is * idempotent and safe to call multiple times. */ import type * as http from "node:http"; import type { TaskEvent } from "./types.js"; import { SSE_HEARTBEAT_INTERVAL_MS as HEARTBEAT_MS } from "./types.js"; /** * Callback invoked by the bus on each broadcast — useful for tests that * don't want to spin up a real HTTP server + EventSource. */ export type EventListener = (event: TaskEvent) => void; /** * Connection handle returned by `subscribe()`. Pass to `unsubscribe()` * when the client disconnects. */ export interface SubscriberHandle { /** Numeric id assigned at subscribe time. */ readonly id: number; } export declare class EventBus { private clients; private listeners; private nextId; private heartbeatTimer; /** * Register an HTTP response as a new SSE subscriber. * * Caller is responsible for setting up SSE headers on `res` BEFORE * calling this method (use `SSEClient.writeHeaders`). The bus will: * - register a `close` listener that auto-removes the subscriber * - start a periodic keep-alive ping (if not already running) * * Returns a handle that can be passed to `unsubscribe()` for explicit * removal (the close listener handles cleanup automatically on socket * close). */ subscribe(res: http.ServerResponse, label?: string): SubscriberHandle; /** * Remove a subscriber (idempotent — safe to call multiple times). */ unsubscribe(handle: SubscriberHandle): void; /** * Broadcast an event to every connected client and registered listener. * * `event` is serialized with `JSON.stringify` and sent as a single SSE * `data:` frame. Clients with closed sockets are silently dropped from * the subscriber set on the next write attempt. * * Exceptions from individual client writes are caught and logged — one * broken subscriber must not break the rest. */ broadcast(event: TaskEvent): void; /** * Register an in-process event listener (test-only path). * Returns a function that unregisters the listener. */ addListener(fn: EventListener): () => void; /** Number of currently-connected SSE clients (for tests / health checks). */ size(): number; /** True if there are no subscribers. */ isEmpty(): boolean; /** Force-close every subscriber (called on dispose). */ closeAll(): void; private maybeStartHeartbeat; private maybeStopHeartbeat; } /** * Format a TaskEvent as an SSE frame. Uses `data: \n\n` per the SSE * spec (https://html.spec.whatwg.org/multipage/server-sent-events.html). * * Optionally a caller can supply an `id:` line so clients can resume from * the last event after a reconnect — currently we omit it (not needed * for the plugin's use case where the frontend re-fetches the full state * snapshot on reconnect). */ export declare function formatSSEFrame(event: TaskEvent): string; /** * Write the standard SSE response headers and the initial comment line. * Helper for `server.ts` to keep the handler readable. */ export declare function writeSSEHeaders(res: http.ServerResponse): void; /** Re-export heartbeat constant for convenience. */ export { HEARTBEAT_MS }; //# sourceMappingURL=event-bus.d.ts.map