import type { WorkspaceEventPayload, WorkspaceEventType } from "../../../server/src/api-types" import { serverApi } from "./api-client" import { getClientIdentity } from "./client-identity" import { getLogger } from "./logger" const RETRY_BASE_DELAY = 1000 const RETRY_MAX_DELAY = 10000 const log = getLogger("sse") function logSse(message: string, context?: Record) { if (context) { log.info(message, context) return } log.info(message) } class ServerEvents { private handlers = new Map void>>() private openHandlers = new Set<() => void>() private source: EventSource | null = null private retryDelay = RETRY_BASE_DELAY constructor() { this.connect() } private connect() { if (this.source) { this.source.close() } logSse("Connecting to backend events stream") this.source = serverApi.connectEvents( (event) => this.dispatch(event), () => this.scheduleReconnect(), (payload) => { void serverApi .sendClientConnectionPong({ ...getClientIdentity(), pingTs: payload.ts, }) .catch((error) => { log.error("Failed to send client connection pong", error) }) }, ) this.source.onopen = () => { logSse("Events stream connected") this.retryDelay = RETRY_BASE_DELAY this.openHandlers.forEach((handler) => handler()) } } private scheduleReconnect() { if (this.source) { this.source.close() this.source = null } logSse("Events stream disconnected, scheduling reconnect", { delayMs: this.retryDelay }) setTimeout(() => { this.retryDelay = Math.min(this.retryDelay * 2, RETRY_MAX_DELAY) this.connect() }, this.retryDelay) } private dispatch(event: WorkspaceEventPayload) { logSse(`event ${event.type}`) this.handlers.get("*")?.forEach((handler) => handler(event)) this.handlers.get(event.type)?.forEach((handler) => handler(event)) } on(type: WorkspaceEventType | "*", handler: (event: WorkspaceEventPayload) => void): () => void { if (!this.handlers.has(type)) { this.handlers.set(type, new Set()) } const bucket = this.handlers.get(type)! bucket.add(handler) return () => bucket.delete(handler) } onOpen(handler: () => void): () => void { this.openHandlers.add(handler) return () => this.openHandlers.delete(handler) } } export const serverEvents = new ServerEvents()