summaryrefslogtreecommitdiffhomepage
path: root/yt-music-dl-web/src/adapters/ws.ts
blob: 158443d4fc9ab937ec07880c4bdbd80fff1c0aea (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
 * src/adapters/ws.ts — injected browser effect: WebSocket client.
 *
 * Connects to `/ws` (same origin — the Vite proxy forwards to the backend).
 * Exposes a typed subscribe/unsubscribe API + an async iterable of server
 * messages. The pure download-status reducer consumes these; this holds no
 * business logic.
 */
import type { JobId, WsServerMessage } from "@yt-music/contract";

export interface WsClient {
	connect(): void;
	subscribe(jobId: JobId): void;
	unsubscribe(jobId: JobId): void;
	onMessage(cb: (msg: WsServerMessage) => void): () => void;
	close(): void;
}

export function createWsClient(wsUrl: string): WsClient {
	let ws: WebSocket | null = null;
	const listeners = new Set<(msg: WsServerMessage) => void>();

	return {
		connect() {
			ws = new WebSocket(wsUrl);
			ws.onmessage = (ev) => {
				try {
					const msg = JSON.parse(ev.data as string) as WsServerMessage;
					for (const cb of listeners) cb(msg);
				} catch {
					// ignore malformed
				}
			};
		},
		subscribe(jobId) {
			ws?.send(JSON.stringify({ type: "subscribe", jobId }));
		},
		unsubscribe(jobId) {
			ws?.send(JSON.stringify({ type: "unsubscribe", jobId }));
		},
		onMessage(cb) {
			listeners.add(cb);
			return () => listeners.delete(cb);
		},
		close() {
			ws?.close();
			ws = null;
		},
	};
}