mirror of
https://github.com/oarkflow/mq.git
synced 2025-09-27 20:32:15 +08:00
250 lines
8.8 KiB
JavaScript
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;
|
|
});
|