type Handler = (payload: any) => void; class WsClient { private ws: WebSocket | null = null; private handlers = new Map(); private reconnectTimer: ReturnType | null = null; private token: string | null = null; connect(token: string) { this.token = token; const wsBase = import.meta.env.VITE_WS_URL || `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}`; this.ws = new WebSocket(`${wsBase}/ws?token=${token}`); this.ws.onopen = () => { if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } this.emit('connected', null); }; this.ws.onmessage = (event) => { try { const { type, payload } = JSON.parse(event.data); const hs = this.handlers.get(type) || []; hs.forEach(h => h(payload)); } catch {} }; this.ws.onclose = () => { this.emit('disconnected', null); this.reconnectTimer = setTimeout(() => { if (this.token) this.connect(this.token); }, 3000); }; this.ws.onerror = () => { this.ws?.close(); }; } on(type: string, handler: Handler) { if (!this.handlers.has(type)) this.handlers.set(type, []); this.handlers.get(type)!.push(handler); return () => { const hs = this.handlers.get(type) || []; this.handlers.set(type, hs.filter(h => h !== handler)); }; } private emit(type: string, payload: any) { const hs = this.handlers.get(type) || []; hs.forEach(h => h(payload)); } send(type: string, payload: any) { if (this.ws?.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify({ type, payload })); } } trySend(type: string, payload: any): boolean { if (this.ws?.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify({ type, payload })); return true; } return false; } disconnect() { this.token = null; if (this.reconnectTimer) clearTimeout(this.reconnectTimer); this.ws?.close(); this.ws = null; } } export const wsClient = new WsClient();