import type { LiveEvent } from '@atm/shared/api'; import type { FastifyInstance } from 'fastify'; import type { LiveHub } from '../live/hub.ts'; export const HEARTBEAT_MS = 25_000; const MAX_CLIENTS = 500; const frame = (e: LiveEvent) => `id: ${e.id}\nevent: ${e.kind}\ndata: ${JSON.stringify(e.data)}\n\n`; export async function liveRoutes(app: FastifyInstance, { hub }: { hub: LiveHub }) { let clients = 0; const open = new Set<{ end: () => void }>(); // Hijacked streams are not tracked by Fastify; end them so shutdown does not hang. app.addHook('onClose', async () => { for (const res of open) res.end(); }); app.get<{ Headers: { 'last-event-id'?: string } }>('/api/live', async (req, reply) => { if (clients >= MAX_CLIENTS) return reply.code(503).send({ error: 'too_many_clients' }); clients++; reply.hijack(); const res = reply.raw; res.writeHead(200, { 'content-type': 'text/event-stream; charset=utf-8', 'cache-control': 'no-cache, no-transform', connection: 'keep-alive', 'x-accel-buffering': 'no', }); res.write(`retry: 5000\n: connected\n\n`); const lastId = Number(req.headers['last-event-id']); if (Number.isFinite(lastId) && lastId > 0) for (const e of hub.since(lastId)) res.write(frame(e)); const unsubscribe = hub.subscribe((e) => res.write(frame(e))); const heartbeat = setInterval(() => res.write(': ping\n\n'), HEARTBEAT_MS); open.add(res); const close = () => { clearInterval(heartbeat); unsubscribe(); open.delete(res); clients--; }; req.raw.on('close', close); }); }