// retoor export class PubSubClient { constructor() { this.socket = null; this.ready = false; this.handlers = new Map(); this.pending = []; this.backoff = 200; this.maxBackoff = 5000; } _url() { const protocol = location.protocol === "https:" ? "wss:" : "ws:"; return `${protocol}//${location.host}/pubsub/ws`; } _connect() { if (this.socket) return; const socket = new WebSocket(this._url()); this.socket = socket; socket.addEventListener("open", () => { this.ready = true; this.backoff = 200; for (const topic of this.handlers.keys()) { socket.send(JSON.stringify({ type: "subscribe", topic })); } const queued = this.pending; this.pending = []; queued.forEach((frame) => socket.send(JSON.stringify(frame))); }); socket.addEventListener("message", (event) => { let frame; try { frame = JSON.parse(event.data); } catch (error) { return; } if (frame.type === "message") this._dispatch(frame); }); socket.addEventListener("close", (event) => { this.ready = false; this.socket = null; const delay = event.code === 4013 ? 200 : this.backoff; this.backoff = Math.min(this.backoff * 2, this.maxBackoff); if (this.handlers.size || this.pending.length) { setTimeout(() => this._connect(), delay); } }); } _matches(pattern, topic) { if (pattern === topic || pattern === "*") return true; if (pattern.endsWith(".*")) { return topic === pattern.slice(0, -2) || topic.startsWith(pattern.slice(0, -1)); } return false; } _dispatch(frame) { for (const [pattern, callbacks] of this.handlers.entries()) { if (this._matches(pattern, frame.topic)) { callbacks.forEach((callback) => callback(frame.data, frame.topic)); } } } _send(frame) { if (this.ready && this.socket) { this.socket.send(JSON.stringify(frame)); } else { this.pending.push(frame); this._connect(); } } subscribe(topic, callback) { let callbacks = this.handlers.get(topic); if (!callbacks) { callbacks = new Set(); this.handlers.set(topic, callbacks); this._send({ type: "subscribe", topic }); } callbacks.add(callback); this._connect(); return () => this.unsubscribe(topic, callback); } unsubscribe(topic, callback) { const callbacks = this.handlers.get(topic); if (!callbacks) return; callbacks.delete(callback); if (!callbacks.size) { this.handlers.delete(topic); this._send({ type: "unsubscribe", topic }); } } publish(topic, data) { this._send({ type: "publish", topic, data }); } }