import { Response } from 'express'; import { SSEEvent } from '../../types/index.js'; import { createRedisClient } from '../infra/redis.util.js'; const clients = new Map(); const eventBuffer = new Map(); let heartbeatInterval: NodeJS.Timeout | null = null; // a socket may live on another instance const RELAY_CHANNEL = 'sse:relay'; const publisher = createRedisClient('sse-pub'); const subscriber = createRedisClient('sse-sub'); let relayReady = false; function writeToClient( id: string, client: Response, event: SSEEvent | Record ): void { try { client.write(`data: ${JSON.stringify(event)}\n\n`); } catch (error) { console.error('[SSE] Write failed:', (error as Error).message); removeClient(id); } } function ensureRelay(): void { if (relayReady) return; relayReady = true; subscriber .subscribe(RELAY_CHANNEL) .catch((error) => console.error('[SSE] relay subscribe failed:', (error as Error).message) ); subscriber.on('message', (channel: string, payload: string) => { if (channel !== RELAY_CHANNEL) return; try { const { id, event } = JSON.parse(payload) as { id: string; event: SSEEvent | Record; }; const client = clients.get(id); if (client) writeToClient(id, client, event); } catch { // ignore } }); } ensureRelay(); function startHeartbeat() { if (heartbeatInterval) return; heartbeatInterval = setInterval(() => { clients.forEach((client, id) => { try { client.write(': heartbeat\n\n'); } catch (err) { console.debug( '[SSE] Heartbeat failed, removing client', id, (err as Error).message ); removeClient(id); } }); }, 10000); // 10s heartbeat interval // unref keep-alive heartbeatInterval.unref?.(); } export function addClient(id: string, res: Response) { res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); res.setHeader('X-Accel-Buffering', 'no'); // disable buffering // sse cors const origin = res.getHeader('Access-Control-Allow-Origin') as string; res.writeHead(200, { 'Access-Control-Allow-Origin': origin || '*', }); res.write('retry: 5000\n\n'); clients.set(id, res); startHeartbeat(); // flush buffer const buffer = eventBuffer.get(id); if (buffer) { buffer.forEach((event) => { sendEvent(id, event); }); eventBuffer.delete(id); } res.on('close', () => removeClient(id)); } export function removeClient(id: string) { clients.delete(id); if (clients.size === 0 && heartbeatInterval) { clearInterval(heartbeatInterval); heartbeatInterval = null; } } export function sendEvent( id: string, event: SSEEvent | Record ) { const client = clients.get(id); if (client) { writeToClient(id, client, event); return; } publisher.publish(RELAY_CHANNEL, JSON.stringify({ id, event })).catch(() => { // ignore }); const buffer = eventBuffer.get(id) || []; // bound buffer for never-connected clients if (buffer.length >= 100) buffer.shift(); buffer.push(event as SSEEvent); eventBuffer.set(id, buffer); const bufferTimer = setTimeout(() => { const currentBuffer = eventBuffer.get(id); if (currentBuffer) { const index = currentBuffer.indexOf(event as SSEEvent); if (index > -1) currentBuffer.splice(index, 1); if (currentBuffer.length === 0) eventBuffer.delete(id); } }, 30000); bufferTimer.unref?.(); } export function sendBufferedEvent(id: string, event: SSEEvent) { // throttled progress sendEvent(id, event); } export function resetSSE() { if (heartbeatInterval) { clearInterval(heartbeatInterval); heartbeatInterval = null; } clients.clear(); eventBuffer.clear(); }