tubearr/src/server/routes/websocket.ts
John Lightner 0541a5f1d1 WIP: in-progress WebSocket download progress & event bus
Snapshot of active development by separate Claude instance.
Includes: event bus, progress parser, WebSocket route,
download progress bar component, SSE contexts/hooks.
Not tested or validated — commit for migration to dev01.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 11:34:26 -05:00

75 lines
2.4 KiB
TypeScript

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<void> {
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<string, unknown>): void {
try {
if (socket.readyState === socket.OPEN) {
socket.send(JSON.stringify(data));
}
} catch {
// Client disconnected — swallow
}
}