class ReconnectingWebSocket {
constructor(url, protocols, options = {}) {
this.onopen = null;
this.onclose = null;
this.onmessage = null;
this.onerror = null;
this.onreconnect = (details) => {
var reconnecting_el = document.getElementById("faucet-reconnecting-msg");
if (!reconnecting_el) {
const el = document.createElement("div");
el.style.position = "fixed";
el.style.bottom = "0";
el.style.left = "5px";
el.style.padding = "5px";
el.style.backgroundColor = "rgba(220, 220, 220, 0.4)";
el.style.color = "black";
el.style.borderRadius = "5px 5px 0 0";
el.style.zIndex = "10001";
el.style.fontFamily = "sans-serif";
el.id = "faucet-reconnecting-msg";
el.textContent = "Reconnecting...";
document.body.appendChild(el);
};
}
this.onreconnected = () => {
var reconnecting_el = document.getElementById("faucet-reconnecting-msg");
if (reconnecting_el) {
reconnecting_el.remove()
}
}
this._protocols = protocols;
this._ws = null;
this._reconnectAttempts = 0;
this._totalReconnectAttempts = 0;
this._forcedClose = false;
if (window.crypto && typeof window.crypto.randomUUID === "function") {
this._sessionId = window.crypto.randomUUID();
} else {
this._sessionId = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(
/[xy]/g,
function (c) {
const r = (Math.random() * 16) | 0;
const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
},
);
}
const sessionQueryParam =
options.sessionQueryParam != null
? options.sessionQueryParam
: "sessionId";
const separator = url.includes("?") ? "&" : "?";
this._url = `${url}${separator}${sessionQueryParam}=${this._sessionId}`;
this._maxReconnectAttempts =
options.maxReconnectAttempts != null ? options.maxReconnectAttempts : 50;
this._reconnectDelay =
options.reconnectDelay != null ? options.reconnectDelay : 500;
this._maxReconnectTime =
options.maxReconnectTime != null ? options.maxReconnectTime : 10000; this._lastDisconnectTime = null;
this.connect();
}
connect() {
console.log(`ReconnectingWebSocket: Connecting to ${this._url}...`);
this._ws = new WebSocket(`${this._url}&attempt=${this._totalReconnectAttempts}`, this._protocols);
this._ws.onopen = (event) => {
console.log(
`ReconnectingWebSocket: Connection opened with Session ID: ${this._sessionId}`,
);
if (this.onopen) {
this.onreconnected();
this.onopen(event);
}
};
this._ws.onmessage = (event) => {
if (this.onmessage) {
this.onmessage(event);
}
};
this._ws.onerror = (event) => {
console.error("ReconnectingWebSocket: Error:", event);
if (this.onerror) {
this.onerror(event);
}
};
this._ws.onclose = (event) => {
if (event.code === 1000 || event.code === 1001 || this._forcedClose) {
console.log(`ReconnectingWebSocket: Connection closed normally. Code: ${event.code}, Reason: ${event.reason}`);
if (this.onclose) {
this.onclose(event);
}
return;
}
if (this._reconnectAttempts == 0) {
this._lastDisconnectTime = Date.now();
}
this._handleReconnect(event);
};
}
_handleReconnect(event) {
var more_than_max_time_passed = (Date.now() - this._lastDisconnectTime) < this._maxReconnectTime;
if (this._reconnectAttempts < this._maxReconnectAttempts && more_than_max_time_passed) {
this._reconnectAttempts++;
this._totalReconnectAttempts++;
console.log(
`ReconnectingWebSocket: Connection lost. Reconnecting with same session... (${this._reconnectAttempts}/${this._maxReconnectAttempts})`,
);
if (this.onreconnect) {
this.onreconnect({
attempts: this._reconnectAttempts,
maxAttempts: this._maxReconnectAttempts,
delay: this._reconnectDelay,
});
}
setTimeout(() => this.connect(), this._reconnectDelay);
} else {
console.error(`ReconnectingWebSocket: Failed to reconnect after ${this._maxReconnectAttempts} attempts.`);
if (this.onclose) {
this.onclose(event);
}
}
}
send(data) {
if (this.readyState === WebSocket.OPEN) {
this._ws.send(data);
this._reconnectAttempts = 0;
} else {
throw new Error("WebSocket is not open. readyState: " + this.readyState);
}
}
close(code, reason) {
this._forcedClose = true;
if (this._ws) {
this._ws.close(code, reason);
}
}
get sessionId() {
return this._sessionId;
}
get readyState() {
return this._ws ? this._ws.readyState : WebSocket.CONNECTING;
}
get url() {
return this._ws ? this._ws.url : this._url;
}
get bufferedAmount() {
return this._ws ? this._ws.bufferedAmount : 0;
}
get extensions() {
return this._ws ? this._ws.extensions : "";
}
get protocol() {
return this._ws ? this._ws.protocol : "";
}
get binaryType() {
return this._ws ? this._ws.binaryType : "blob";
}
set binaryType(type) {
if (this._ws) {
this._ws.binaryType = type;
}
}
}
ReconnectingWebSocket.CONNECTING = 0;
ReconnectingWebSocket.OPEN = 1;
ReconnectingWebSocket.CLOSING = 2;
ReconnectingWebSocket.CLOSED = 3;
Shiny.createSocket = function () {
const url = "websocket";
return new ReconnectingWebSocket(url);
};
function getShinySocket() {
return Shiny.shinyapp.$socket;
}