SHiNE-server/SHiNE-browser-plugin-wallet/js/lib/ws-client.js

142 lines
3.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
}
}