export default class ReconnectingWebSocket { constructor(url) { if (url instanceof URL) { this.url = url; } else { this.url = new URL(url); } this.connected = false; this._eventTarget = new EventTarget(); this._backoff = 250; // milliseconds, doubled before use this._lastConnect = 0; this._socket = null; this._unsent = []; this._closing = false; this._connect(true); } _connect(first) { if (this._socket) try { this._socket.close(); } catch (e) {} try { this._socket = new WebSocket(this.url.href); } catch (e) { this._reconnecting = false; return this._reconnect(); } this._socket.addEventListener("close", () => this._reconnect()); this._socket.addEventListener("error", () => this._reconnect()); this._socket.addEventListener("message", ({ data }) => { this._eventTarget.dispatchEvent(new MessageEvent("message", { data })); }); this._socket.addEventListener("open", (e) => { if (first) this._eventTarget.dispatchEvent(new Event("open")); if (this._reconnecting) this._eventTarget.dispatchEvent(new Event("reconnected")); this._reconnecting = false; this._backoff = 250; this.connected = true; while (this._unsent.length > 0) this._socket.send(this._unsent.shift()); }); } _reconnect() { if (this._closing) return; if (this._reconnecting) return; this._eventTarget.dispatchEvent(new Event("reconnecting")); this._reconnecting = true; this.connected = false; this._backoff *= 2; // exponential backoff setTimeout(() => { this._connect(); }, Math.floor(this._backoff + Math.random() * this._backoff * 0.25 - this._backoff * 0.125)); } send(message) { if (this.connected) { this._socket.send(message); } else { this._unsent.push(message); } } close() { this._closing = true; this._socket.close(); } addEventListener(...a) { return this._eventTarget.addEventListener(...a); } removeEventListener(...a) { return this._eventTarget.removeEventListener(...a); } }