import type { WsEvent } from './types'; type Listener = (event: WsEvent) => void; export class VaultWebSocket { private ws: WebSocket | null = null; private listeners: Map> = new Map(); private globalListeners: Set = new Set(); private reconnectTimer: ReturnType | null = null; private url: string; constructor(url?: string) { const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; this.url = url || `${proto}//${window.location.host}/ws`; } connect() { if (this.ws?.readyState === WebSocket.OPEN) return; this.ws = new WebSocket(this.url); this.ws.onmessage = (msg) => { try { const event: WsEvent = JSON.parse(msg.data); this.globalListeners.forEach((fn) => fn(event)); this.listeners.get(event.type)?.forEach((fn) => fn(event)); } catch { // ignore malformed messages } }; this.ws.onclose = () => { this.scheduleReconnect(); }; this.ws.onerror = () => { this.ws?.close(); }; } disconnect() { if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } this.ws?.close(); this.ws = null; } /** Listen to a specific event type */ on(type: string, fn: Listener): () => void { if (!this.listeners.has(type)) { this.listeners.set(type, new Set()); } this.listeners.get(type)!.add(fn); return () => this.listeners.get(type)?.delete(fn); } /** Listen to all events */ onAny(fn: Listener): () => void { this.globalListeners.add(fn); return () => this.globalListeners.delete(fn); } send(action: Record) { if (this.ws?.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify(action)); } } private scheduleReconnect() { if (this.reconnectTimer) return; this.reconnectTimer = setTimeout(() => { this.reconnectTimer = null; this.connect(); }, 3000); } } export const vaultWs = new VaultWebSocket();