/** * SSE Hub — manages connected clients and broadcasts events. * Product backends create an SSEHub instance and push events to it; * the hub fans out to all connected SSE clients. */ import type { ServerResponse } from 'node:http'; export interface SSEMessage { event?: string; data: string; id?: string; retry?: number; } interface ConnectedClient { id: string; userId?: string; res: ServerResponse; connectedAt: string; } export class SSEHub { private clients = new Map(); private clientCounter = 0; /** * Add a new SSE client connection. * Sets up the SSE headers and returns a client ID. */ addClient(res: ServerResponse, userId?: string): string { const id = `sse_${++this.clientCounter}_${Date.now()}`; res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive', 'X-Accel-Buffering': 'no', }); // Send initial connection event res.write(`event: connected\ndata: ${JSON.stringify({ clientId: id })}\n\n`); const client: ConnectedClient = { id, userId, res, connectedAt: new Date().toISOString(), }; this.clients.set(id, client); // Clean up on close res.on('close', () => { this.clients.delete(id); }); return id; } /** * Broadcast an SSE message to all connected clients. */ broadcast(message: SSEMessage): number { let sent = 0; const formatted = formatSSE(message); for (const [id, client] of this.clients) { try { client.res.write(formatted); sent++; } catch { this.clients.delete(id); } } return sent; } /** * Send an SSE message to a specific user's connections. */ sendToUser(userId: string, message: SSEMessage): number { let sent = 0; const formatted = formatSSE(message); for (const [id, client] of this.clients) { if (client.userId === userId) { try { client.res.write(formatted); sent++; } catch { this.clients.delete(id); } } } return sent; } /** * Send a heartbeat (comment) to all clients to keep connections alive. */ heartbeat(): void { for (const [id, client] of this.clients) { try { client.res.write(': heartbeat\n\n'); } catch { this.clients.delete(id); } } } /** * Get count of connected clients. */ get clientCount(): number { return this.clients.size; } /** * Disconnect all clients. */ disconnectAll(): void { for (const [, client] of this.clients) { try { client.res.end(); } catch { /* already closed */ } } this.clients.clear(); } } function formatSSE(message: SSEMessage): string { let output = ''; if (message.id) output += `id: ${message.id}\n`; if (message.event) output += `event: ${message.event}\n`; if (message.retry) output += `retry: ${message.retry}\n`; output += `data: ${message.data}\n\n`; return output; }