/** * In-process SSE hub. Clients subscribe via GET /events; handlers call emit() * after a successful mutation so other tabs/users can refetch. */ /** @typedef {{ res: import('express').Response, companyId: number, userId: number }} SseClient */ /** @type {Set} */ const clients = new Set(); const KEEP_ALIVE_MS = 25_000; /** * Register an open SSE response. Caller must write the initial headers. * @param {import('express').Response} res * @param {{ companyId: number, userId: number }} meta * @returns {() => void} unsubscribe */ export function subscribe(res, { companyId, userId }) { /** @type {SseClient} */ const client = { res, companyId, userId }; clients.add(client); const keepAlive = setInterval(() => { if (res.writableEnded) return; res.write(': keepalive\n\n'); }, KEEP_ALIVE_MS); const unsubscribe = () => { clearInterval(keepAlive); clients.delete(client); }; res.on('close', unsubscribe); return unsubscribe; } /** * Broadcast a named signal to subscribers of the given company. * @param {string} signal - SSE event name (e.g. `category`) * @param {{ companyId: number }} options */ export function emit(signal, { companyId }) { if (companyId == null) return; const frame = `event: ${signal}\ndata: {}\n\n`; for (const client of clients) { if (client.companyId !== companyId) continue; if (client.res.writableEnded) continue; client.res.write(frame); } }