const DEFAULT_TIMEOUT_MS = 12000; const runtimeTimers = globalThis; function buildWsUrl(raw) { const value = String(raw || '').trim(); if (!value) return 'wss://shineup.me/ws'; if (value.startsWith('ws://') || value.startsWith('wss://')) return value; if (value.startsWith('http://') || value.startsWith('https://')) { const parsed = new URL(value); parsed.protocol = parsed.protocol === 'https:' ? 'wss:' : 'ws:'; if (!parsed.pathname || parsed.pathname === '/') parsed.pathname = '/ws'; return parsed.toString(); } return value; } function createRequestId(op) { return `${op}-${Date.now()}-${Math.random().toString(16).slice(2)}`; } export class WsJsonClient { constructor(url) { this.url = buildWsUrl(url); this.ws = null; this.openPromise = null; this.pending = new Map(); this.listeners = new Map(); } async open() { if (this.ws && this.ws.readyState === WebSocket.OPEN) return; if (this.openPromise) return this.openPromise; this.openPromise = new Promise((resolve, reject) => { const ws = new WebSocket(this.url); this.ws = ws; ws.addEventListener('open', () => resolve(), { once: true }); ws.addEventListener('error', () => reject(new Error(`Не удалось подключиться к ${this.url}`)), { once: true }); ws.addEventListener('close', () => this.failPending('WebSocket соединение закрыто')); ws.addEventListener('message', (event) => this.handleMessage(event.data)); }).finally(() => { this.openPromise = null; }); return this.openPromise; } async request(op, payload = {}, timeoutMs = DEFAULT_TIMEOUT_MS) { await this.open(); const requestId = createRequestId(op); const body = { op, requestId, payload }; const response = new Promise((resolve, reject) => { const timer = runtimeTimers.setTimeout(() => { this.pending.delete(requestId); reject(new Error(`Таймаут ответа для операции ${op}`)); }, timeoutMs); this.pending.set(requestId, { resolve: (value) => { runtimeTimers.clearTimeout(timer); resolve(value); }, reject: (error) => { runtimeTimers.clearTimeout(timer); reject(error); }, }); }); this.ws.send(JSON.stringify(body)); return response; } handleMessage(raw) { let data; try { data = JSON.parse(raw); } catch { return; } if (data?.event) { this.emit(String(data?.op || ''), data); return; } const requestId = data?.requestId; if (!requestId) { this.emit(String(data?.op || ''), data); return; } const slot = this.pending.get(requestId); if (!slot) { this.emit(String(data?.op || ''), data); return; } this.pending.delete(requestId); slot.resolve(data); } on(op, handler) { const key = String(op || '').trim(); if (!key || typeof handler !== 'function') { return () => {}; } if (!this.listeners.has(key)) { this.listeners.set(key, new Set()); } this.listeners.get(key).add(handler); return () => { const bucket = this.listeners.get(key); if (!bucket) return; bucket.delete(handler); if (!bucket.size) { this.listeners.delete(key); } }; } emit(op, payload) { const bucket = this.listeners.get(String(op || '').trim()); if (!bucket?.size) return; for (const handler of [...bucket]) { try { handler(payload); } catch {} } } failPending(message) { const error = new Error(message); for (const slot of this.pending.values()) slot.reject(error); this.pending.clear(); } close() { if (this.ws) { this.ws.close(); this.ws = null; } } }