import WebSocket from "@tauri-apps/plugin-websocket"; import { PeerInfo } from './ts/bindings/PeerInfo'; import { ClientWsMessage } from './ts/bindings/ClientWsMessage'; import { ServerWsMessage } from './ts/bindings/ServerWsMessage'; type RemoveListener = (() => void) | null; export class LocalSignallingClient { private socket: WebSocket | null = null; private removeListener: RemoveListener = null; private myPeerId: string | null = null; private peers: PeerInfo[] = []; constructor( private readonly displayName: string, private readonly onLog: (message: string) => void, private readonly onPeers: (peers: PeerInfo[]) => void, private readonly onChat: (line: string) => void, ) {} public async connect(): Promise { if (this.socket !== null) { this.onLog("signalling already connected"); return; } const url = "ws://127.0.0.1:3012"; this.onLog(`connecting to ${url}`); let socket: WebSocket; try { socket = await WebSocket.connect(url); } catch (error) { throw new Error(`websocket connection failed: ${String(error)}`); } this.socket = socket; const removeListener = socket.addListener((message) => { if (message.type === "Text") { this.handleServerMessage(message.data); return; } if (message.type === "Close") { this.onLog("signalling disconnected"); this.cleanupDisconnectedState(); return; } if (message.type === "Binary") { this.onLog("unexpected binary websocket message"); return; } }); this.removeListener = removeListener; this.onLog("signalling connected"); await this.send({ type: "hello", display_name: this.displayName, }); } public async disconnect(): Promise { if (this.socket === null) { return; } const socket = this.socket; if (this.removeListener !== null) { this.removeListener(); this.removeListener = null; } this.socket = null; try { await socket.disconnect(); } catch (error) { this.onLog(`disconnect error: ${String(error)}`); } this.cleanupDisconnectedState(); } public async sendChat(text: string): Promise { await this.send({ type: "chat_send", text, }); } public getMyPeerId(): string | null { return this.myPeerId; } public getPeers(): PeerInfo[] { return [...this.peers]; } private async send(message: ClientWsMessage): Promise { if (this.socket === null) { this.onLog("cannot send: signalling not connected"); return; } const payload = JSON.stringify(message); try { await this.socket.send(payload); } catch (error) { this.onLog(`send failed: ${String(error)}`); } } private cleanupDisconnectedState(): void { this.socket = null; this.myPeerId = null; this.peers = []; this.onPeers([]); } private handleServerMessage(raw: string): void { let message: ServerWsMessage; try { message = JSON.parse(raw) as ServerWsMessage; } catch (error) { this.onLog(`invalid server message: ${String(error)}`); return; } switch (message.type) { case "welcome": this.myPeerId = message.peer_id; this.onLog(`welcome peer_id=${message.peer_id}`); break; case "peer_list": this.peers = message.peers; this.onPeers([...this.peers]); this.onLog(`peer_list received (${message.peers.length})`); break; case "peer_joined": this.peers = mergePeer(this.peers, message.peer); this.onPeers([...this.peers]); this.onLog(`peer joined: ${message.peer.display_name}`); break; case "peer_left": this.peers = this.peers.filter((peer) => peer.peer_id !== message.peer_id); this.onPeers([...this.peers]); this.onLog(`peer left: ${message.peer_id}`); break; case "chat_receive": this.onChat(`${message.from_display_name}: ${message.text}`); break; case "offer": this.onLog(`offer received from ${message.from_peer_id}`); break; case "answer": this.onLog(`answer received from ${message.from_peer_id}`); break; case "ice_candidate": this.onLog(`ice candidate received from ${message.from_peer_id}`); break; case "pong": this.onLog("pong"); break; case "error": this.onLog(`server error: ${message.message}`); break; default: this.onLog(`unhandled server message: ${raw}`); break; } } } function mergePeer(peers: PeerInfo[], incoming: PeerInfo): PeerInfo[] { const next = peers.filter((peer) => peer.peer_id !== incoming.peer_id); next.push(incoming); next.sort((a, b) => a.display_name.localeCompare(b.display_name)); return next; }