forked from lavender/watch-party
72 lines
2.2 KiB
JavaScript
72 lines
2.2 KiB
JavaScript
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);
|
|
}
|
|
}
|