import websocket from '@fastify/websocket'; import type { FastifyInstance } from 'fastify'; import type { WebSocket } from 'ws'; import type { DownloadEventBus, DownloadProgressPayload, DownloadCompletePayload, DownloadFailedPayload } from '../../services/event-bus'; /** * WebSocket route plugin. * * Registers @fastify/websocket and a GET /ws route that broadcasts * download events (progress, complete, failed) to all connected clients. * * The event bus is passed via plugin options. Each connected client * gets its own set of event bus listeners, which are cleaned up on disconnect. * * Auth: The /ws route bypasses API key auth (not under /api/*). * This is intentional — WebSocket only broadcasts read-only progress data. */ export async function websocketRoutes( fastify: FastifyInstance, opts: { eventBus: DownloadEventBus } ): Promise { const { eventBus } = opts; await fastify.register(websocket); fastify.get('/ws', { websocket: true }, (socket: WebSocket) => { console.log('[websocket] client connected'); // Create listeners for each event type const onProgress = (data: DownloadProgressPayload) => { sendJson(socket, { type: 'download:progress', ...data }); }; const onComplete = (data: DownloadCompletePayload) => { sendJson(socket, { type: 'download:complete', ...data }); }; const onFailed = (data: DownloadFailedPayload) => { sendJson(socket, { type: 'download:failed', ...data }); }; // Subscribe to event bus eventBus.onDownload('download:progress', onProgress); eventBus.onDownload('download:complete', onComplete); eventBus.onDownload('download:failed', onFailed); // Cleanup on disconnect const cleanup = () => { eventBus.offDownload('download:progress', onProgress); eventBus.offDownload('download:complete', onComplete); eventBus.offDownload('download:failed', onFailed); console.log('[websocket] client disconnected'); }; socket.on('close', cleanup); socket.on('error', (err) => { console.log(`[websocket] client error: ${err.message}`); cleanup(); }); }); } /** * Send a JSON message to a WebSocket client. * Silently catches send errors (client may have disconnected). */ function sendJson(socket: WebSocket, data: Record): void { try { if (socket.readyState === socket.OPEN) { socket.send(JSON.stringify(data)); } } catch { // Client disconnected — swallow } }