// eslint-disable-next-line type Listener = (data: any) => void; type ErrorListener = (error: Event | string) => void; class SocketManager { private static instance: SocketManager; private sockets = new Map(); private listeners = new Map>(); private connected = new Map(); private errorListeners = new Map>(); private constructor() {} static getInstance(): SocketManager { if (!SocketManager.instance) { SocketManager.instance = new SocketManager(); } return SocketManager.instance; } connect(url: string): void { if (this.sockets.has(url)) return; const ws = new WebSocket(url); this.sockets.set(url, ws); this.connected.set(url, false); this.listeners.set(url, new Set()); this.errorListeners.set(url, new Set()); ws.addEventListener('open', () => { this.connected.set(url, true); console.info(`Connected: ${url}`); }); ws.addEventListener('message', (event) => { const handlers = this.listeners.get(url); if (handlers) { for (const handler of handlers) { try { handler(event.data); } catch (err) { console.error(`Handler error for ${url}:`, err); } } } }); ws.addEventListener('close', () => { this.connected.set(url, false); console.warn(`Disconnected: ${url}`); this.triggerError(url, 'Connection closed'); }); ws.addEventListener('error', (err) => { console.error(`Socket error for ${url}:`, err); this.triggerError(url, err); }); } subscribe(url: string, listener: Listener, onError?: ErrorListener): void { if (!this.sockets.has(url)) { this.connect(url); } this.listeners.get(url)?.add(listener); if (onError) { this.errorListeners.get(url)?.add(onError); } } unsubscribe(url: string, listener: Listener, onError?: ErrorListener): void { this.listeners.get(url)?.delete(listener); if (onError) { this.errorListeners.get(url)?.delete(onError); } } // eslint-disable-next-line send(url: string, data: any): void { const ws = this.sockets.get(url); if (ws && this.connected.get(url)) { ws.send(typeof data === 'string' ? data : JSON.stringify(data)); } else { console.warn(`Cannot send: not connected to ${url}`); this.triggerError(url, 'Not connected'); } } disconnect(url: string): void { const ws = this.sockets.get(url); if (ws) { ws.close(); this.sockets.delete(url); this.listeners.delete(url); this.errorListeners.delete(url); this.connected.delete(url); } } disconnectAll(): void { for (const url of this.sockets.keys()) { this.disconnect(url); } } private triggerError(url: string, err: Event | string): void { const handlers = this.errorListeners.get(url); if (handlers) { for (const cb of handlers) { try { cb(err); } catch (e) { console.error(`Error handler threw for ${url}:`, e); } } } } } export const socket = SocketManager.getInstance();