Files
mq/examples/minimal_admin/static/admin/js/socket.js
2025-07-30 09:04:02 +05:45

250 lines
8.8 KiB
JavaScript

class Socket {
events = {}
reconnectInterval
reconnectOpts = { enabled: true, replayOnConnect: true, intervalMS: 5000 }
reconnecting = false
connectedOnce = false
headerStartCharCode = 1
headerStartChar
dataStartCharCode = 2
dataStartChar
subProtocol = 'sac-sock'
ws
reconnected = false
maxAttempts = 3
totalAttempts = 0
kickedOut = false
userID
url
constructor(url, userID, opts = { reconnectOpts: {} }) {
opts = opts || { reconnectOpts: {} };
this.headerStartChar = String.fromCharCode(this.headerStartCharCode)
this.dataStartChar = String.fromCharCode(this.dataStartCharCode)
this.url = url
this.userID = userID
if (typeof opts.reconnectOpts == 'object') {
for (let i in opts.reconnectOpts) {
if (!opts.reconnectOpts.hasOwnProperty(i)) continue;
this.reconnectOpts[i] = opts.reconnectOpts[i];
}
}
}
noop() { }
async connect(timeout = 10000) {
try {
this.ws = new WebSocket(this.url, this.subProtocol);
this.ws.binaryType = 'arraybuffer';
this.handleEvents()
const isOpened = () => (this.ws.readyState === WebSocket.OPEN)
if (this.ws.readyState !== WebSocket.CONNECTING) {
return isOpened()
}
} catch (err) {
console.log("Error on reconnection", err)
}
}
handleEvents() {
let self = this
this.onConnect(this.noop)
this.onDisconnect(this.noop)
this.ws.onmessage = function (e) {
let msg = e.data,
headers = {},
eventName = '',
data = '',
chr = null,
i, msgLen;
if (typeof msg === 'string') {
let dataStarted = false,
headerStarted = false;
for (i = 0, msgLen = msg.length; i < msgLen; i++) {
chr = msg[i];
if (!dataStarted && !headerStarted && chr !== self.dataStartChar && chr !== self.headerStartChar) {
eventName += chr;
} else if (!headerStarted && chr === self.headerStartChar) {
headerStarted = true;
} else if (headerStarted && !dataStarted && chr !== self.dataStartChar) {
headers[chr] = true;
} else if (!dataStarted && chr === self.dataStartChar) {
dataStarted = true;
} else {
data += chr;
}
}
} else if (msg && msg instanceof ArrayBuffer && msg.byteLength !== undefined) {
let dv = new DataView(msg),
headersStarted = false;
for (i = 0, msgLen = dv.byteLength; i < msgLen; i++) {
chr = dv.getUint8(i);
if (chr !== self.dataStartCharCode && chr !== self.headerStartCharCode && !headersStarted) {
eventName += String.fromCharCode(chr);
} else if (chr === self.headerStartCharCode && !headersStarted) {
headersStarted = true;
} else if (headersStarted && chr !== self.dataStartCharCode) {
headers[String.fromCharCode(chr)] = true;
} else if (chr === self.dataStartCharCode) {
// @ts-ignore
data = dv.buffer.slice(i + 1);
break;
}
}
}
if (eventName.length === 0) return; //no event to dispatch
if (typeof self.events[eventName] === 'undefined') return;
// @ts-ignore
self.events[eventName].call(self, (headers.J) ? JSON.parse(data) : data);
}
}
startReconnect(timeout = 10000) {
let self = this
setTimeout(async function () {
try {
if (self.maxAttempts > self.totalAttempts) {
return
}
let newWS = new WebSocket(self.url, self.subProtocol);
self.totalAttempts += 1
console.log("attempt to reconnect...", self.totalAttempts)
newWS.onmessage = self.ws.onmessage;
newWS.onclose = self.ws.onclose;
newWS.binaryType = self.ws.binaryType;
self.handleEvents()
//we need to run the initially set onConnect function on the first successful connecting,
//even if replayOnConnect is disabled. The server might not be available on a first
//connection attempt.
if (self.reconnectOpts.replayOnConnect || !self.connectedOnce) {
newWS.onopen = self.ws.onopen;
}
self.ws = newWS;
if (!self.reconnectOpts.replayOnConnect && self.connectedOnce) {
self.onConnect(self.noop);
}
self.ws = newWS
const isOpened = () => (self.ws.readyState === WebSocket.OPEN)
if (self.ws.readyState !== WebSocket.CONNECTING) {
const opened = isOpened()
if (!opened) {
self.startReconnect(timeout)
} else {
console.log("connected with signal server")
}
return opened
}
else {
const intrasleep = 100
const ttl = timeout / intrasleep // time to loop
let loop = 0
while (self.ws.readyState === WebSocket.CONNECTING && loop < ttl) {
await new Promise(resolve => setTimeout(resolve, intrasleep))
loop++
}
const opened = isOpened()
if (!opened) {
self.startReconnect(timeout)
} else {
console.log("connected with signal server")
}
return opened
}
} catch (err) {
console.log("Error on reconnection", err)
}
}, self.reconnectOpts.intervalMS);
}
onConnect(callback) {
let self = this
this.ws.onopen = function () {
self.connectedOnce = true;
callback.apply(self, arguments);
if (self.reconnecting) {
self.reconnecting = false;
}
};
};
onDisconnect(callback) {
let self = this
this.ws.onclose = function () {
if (!self.reconnecting && self.connectedOnce) {
callback.apply(self, arguments);
}
if (self.reconnectOpts.enabled && !self.kickedOut) {
self.reconnecting = true;
self.startReconnect();
}
};
};
on(eventName, callback, override) {
override = override || false
if (!this.events.hasOwnProperty(eventName)) {
this.events[eventName] = callback;
} else if (override) {
this.off(eventName)
this.events[eventName] = callback;
}
}
off(eventName) {
if (this.events[eventName]) {
delete this.events[eventName];
}
}
emit(eventName, data) {
let rs = this.ws.readyState;
if (rs === 0) {
console.warn("websocket is not open yet");
return;
} else if (rs === 2 || rs === 3) {
console.error("websocket is closed");
return;
}
let msg;
if (data instanceof ArrayBuffer) {
let ab = new ArrayBuffer(data.byteLength + eventName.length + 1),
newBuf = new DataView(ab),
oldBuf = new DataView(data),
i = 0;
for (let evtLen = eventName.length; i < evtLen; i++) {
newBuf.setUint8(i, eventName.charCodeAt(i));
}
newBuf.setUint8(i, this.dataStartCharCode);
i++;
for (let x = 0, xLen = oldBuf.byteLength; x < xLen; x++, i++) {
newBuf.setUint8(i, oldBuf.getUint8(x));
}
msg = ab;
} else if (typeof data === 'object') {
msg = eventName + this.dataStartChar + JSON.stringify(data);
} else {
msg = eventName + this.dataStartChar + data;
}
this.ws.send(msg);
}
close() {
this.reconnectOpts.enabled = false;
return this.ws.close(1000);
}
}
// Initialize dashboard when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
window.Socket = Socket;
});