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>
75 lines
2.4 KiB
TypeScript
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
|
|
}
|
|
}
|