@iobroker/ws
Version:
ioBroker client side of web sockets
183 lines (182 loc) • 11.7 kB
JavaScript
"use strict";
var __defProp = Object.defineProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
/*!
* ioBroker WebSockets
* Copyright 2020-2026, bluefox <dogafox@gmail.com>
* Released under the MIT License.
* v 3.0.5 (2026_02_25)
*/
void 0 !== globalThis.process && (globalThis.location ||= { href: "http://localhost:8081/", protocol: "http:", host: "localhost:8081", pathname: "/", hostname: "localhost", reload: /* @__PURE__ */ __name(() => {
}, "reload") });
const MESSAGE_TYPES = { MESSAGE: 0, PING: 1, PONG: 2, CALLBACK: 3 }, DEBUG = true, ERRORS = { 1e3: "CLOSE_NORMAL", 1001: "CLOSE_GOING_AWAY", 1002: "CLOSE_PROTOCOL_ERROR", 1003: "CLOSE_UNSUPPORTED", 1005: "CLOSED_NO_STATUS", 1006: "CLOSE_ABNORMAL", 1007: "Unsupported payload", 1008: "Policy violation", 1009: "CLOSE_TOO_LARGE", 1010: "Mandatory extension", 1011: "Server error", 1012: "Service restart", 1013: "Try again later", 1014: "Bad gateway Server", 1015: "TLS handshake fail" };
class SocketClient {
static {
__name(this, "SocketClient");
}
connectHandlers = [];
reconnectHandlers = [];
disconnectHandlers = [];
errorHandlers = [];
handlers = {};
wasConnected = false;
connectTimer = null;
connectingTimer = null;
connectionCount = 0;
callbacks = [];
pending = [];
id = 0;
lastPong = 0;
socket = null;
url = "";
options = null;
pingInterval = null;
sessionID = 0;
authTimeout = null;
connected = false;
log;
constructor() {
this.log = { debug: /* @__PURE__ */ __name((text) => {
console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${text}`);
}, "debug"), warn: /* @__PURE__ */ __name((text) => console.warn(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${text}`), "warn"), error: /* @__PURE__ */ __name((text) => console.error(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${text}`), "error") };
}
static getQuery(_url) {
const parts = (_url.split("?")[1] || "").split("&"), result = {};
for (let p = 0; p < parts.length; p++) {
result[parts[p].split("=")[0]] = decodeURIComponent(parts[1]);
}
return result;
}
connect(url, options) {
if (this.log.debug("Try to connect"), url && (url = url.split("#")[0]), this.id = 0, this.connectTimer && (clearInterval(this.connectTimer), this.connectTimer = null), this.url ||= url || globalThis.location.href, this.options ||= JSON.parse(JSON.stringify(options || {})), !this.options) throw new Error("No options provided!");
options?.WebSocket && (this.options.WebSocket = options?.WebSocket), this.options.pongTimeout = parseInt(this.options.pongTimeout, 10) || 6e4, this.options.pingInterval = parseInt(this.options.pingInterval, 10) || 5e3, this.options.connectTimeout = parseInt(this.options.connectTimeout, 10) || 3e3, this.options.authTimeout = parseInt(this.options.authTimeout, 10) || 3e3, this.options.connectInterval = parseInt(this.options.connectInterval, 10) || 1e3, this.options.connectMaxAttempt = parseInt(this.options.connectMaxAttempt, 10) || 5, this.sessionID = Date.now();
try {
if ("/" === this.url) {
const parts = globalThis.location.pathname.split("/");
(globalThis.location.pathname.endsWith(".html") || globalThis.location.pathname.endsWith(".htm")) && parts.pop(), this.url = `${globalThis.location.protocol || "ws:"}//${globalThis.location.host || "localhost"}/${parts.join("/")}`;
}
const query = SocketClient.getQuery(this.url);
query.sid && delete query.sid, Object.prototype.hasOwnProperty.call(query, "") && delete query[""];
let u = `${this.url.replace(/^http/, "ws").split("?")[0]}?sid=${this.sessionID}`;
Object.keys(query).length && (u += `&${Object.keys(query).map((attr) => void 0 === query[attr] ? attr : `${attr}=${query[attr]}`).join("&")}`), this.options?.name && !query.name && (u += `&name=${encodeURIComponent(this.options.name)}`), this.options?.token && (u += `&token=${this.options.token}`), this.socket = new (this.options.WebSocket || globalThis.WebSocket)(u);
} catch (error) {
return this.handlers.error?.forEach((cb) => cb.call(this, error)), this.close(), this;
}
return this.connectingTimer = setTimeout(() => {
this.connectingTimer = null, this.log.warn("No READY flag received in 3 seconds. Re-init"), this.close();
}, this.options.connectTimeout), this.socket && (this.socket.onopen = () => {
this.lastPong = Date.now(), this.connectionCount = 0, this.pingInterval = setInterval(() => {
if (!this.options) throw new Error("No options provided!");
if (Date.now() - this.lastPong > (this.options?.pingInterval || 5e3) - 10) try {
this.socket?.send(JSON.stringify([MESSAGE_TYPES.PING]));
} catch (e) {
return this.log.warn(`Cannot send ping. Close connection: ${e}`), this.close(), void this._garbageCollect();
}
Date.now() - this.lastPong > (this.options?.pongTimeout || 6e4) && this.close(), this._garbageCollect();
}, this.options?.pingInterval || 5e3);
}, this.socket.onclose = (event) => {
3001 === event.code ? this.log.warn("ws closed") : this.log.error(`ws connection error: ${ERRORS[event.code]}`), this.close();
}, this.socket.onerror = (error) => {
this.connected && this.socket && (1 === this.socket.readyState && this.log.error(`ws normal error: ${error.type}`), this.errorHandlers.forEach((cb) => cb.call(this, ERRORS[error.code] || "UNKNOWN"))), this.close();
}, this.socket.onmessage = (message) => {
if (this.lastPong = Date.now(), !message?.data || "string" != typeof message.data) return void console.error(`Received invalid message: ${JSON.stringify(message)}`);
let data;
try {
data = JSON.parse(message.data);
} catch {
return void console.error(`Received invalid message: ${JSON.stringify(message.data)}`);
}
const type = data[0], id = data[1], name = data[2], args = data[3];
this.authTimeout && (clearTimeout(this.authTimeout), this.authTimeout = null), type === MESSAGE_TYPES.CALLBACK ? this.findAnswer(id, args) : type === MESSAGE_TYPES.MESSAGE ? "___ready___" === name ? (this.connected = true, this.wasConnected ? this.reconnectHandlers.forEach((cb) => cb.call(this, true)) : (this.connectHandlers.forEach((cb) => cb.call(this, true)), this.wasConnected = true), this.connectingTimer && (clearTimeout(this.connectingTimer), this.connectingTimer = null), this.pending.length && (this.pending.forEach(({ name: name2, args: args2 }) => this.emit(name2, ...args2)), this.pending = [])) : args ? this.handlers[name]?.forEach((cb) => cb.apply(this, args)) : this.handlers[name]?.forEach((cb) => cb.call(this)) : type === MESSAGE_TYPES.PING ? this.socket ? this.socket.send(JSON.stringify([MESSAGE_TYPES.PONG])) : this.log.warn("Cannot do pong: connection closed") : type === MESSAGE_TYPES.PONG || this.log.warn(`Received unknown message type: ${type}`);
}), this;
}
_garbageCollect() {
Date.now();
let empty = 0;
if (empty > this.callbacks.length / 2) {
const newCallback = [];
for (let i = 0; i < this.callbacks.length; i++) this.callbacks[i] && newCallback.push(this.callbacks[i]);
this.callbacks = newCallback;
}
}
withCallback(name, id, args, cb) {
"authenticate" === name && (this.authTimeout = setTimeout(() => {
this.authTimeout = null, this.connected && (this.log.debug("Authenticate timeout"), this.handlers.error?.forEach((cb2) => cb2.call(this, "Authenticate timeout"))), this.close();
}, this.options?.authTimeout || 3e3)), this.callbacks.push({ id, cb, ts: 0 }), this.socket?.send(JSON.stringify([MESSAGE_TYPES.CALLBACK, id, name, args]));
}
findAnswer(id, args) {
for (let i = 0; i < this.callbacks.length; i++) {
const callback = this.callbacks[i];
if (callback?.id === id) {
callback.cb.call(null, ...args), this.callbacks[i] = null;
}
}
}
emit = /* @__PURE__ */ __name((name, ...args) => {
if (this.socket && this.connected) {
if (this.id++, "writeFile" === name && args && "string" != typeof args[2] && args[2]) if (void 0 !== globalThis.process) args[2] = globalThis.Buffer.from(args[2]).toString("base64");
else {
let binary = "";
const bytes = new Uint8Array(args[2]), len = bytes.byteLength;
for (let i = 0; i < len; i++) binary += String.fromCharCode(bytes[i]);
args[2] = globalThis.btoa(binary);
}
try {
if (args && "function" == typeof args[args.length - 1]) {
const _args = [...args], eventHandler = _args.pop();
this.withCallback(name, this.id, _args, eventHandler);
} else args?.length ? this.socket.send(JSON.stringify([MESSAGE_TYPES.MESSAGE, this.id, name, args])) : this.socket.send(JSON.stringify([MESSAGE_TYPES.MESSAGE, this.id, name]));
} catch (e) {
console.error(`Cannot send: ${e}`), this.close();
}
} else this.wasConnected ? this.log.warn("Not connected") : this.pending.push({ name, args });
}, "emit");
on(name, cb) {
cb && ("connect" === name ? this.connectHandlers.push(cb) : "disconnect" === name ? this.disconnectHandlers.push(cb) : "reconnect" === name ? this.reconnectHandlers.push(cb) : "error" === name ? this.errorHandlers.push(cb) : (this.handlers[name] = this.handlers[name] || [], this.handlers[name].push(cb)));
}
off(name, cb) {
if ("connect" === name) {
const pos = this.connectHandlers.indexOf(cb);
-1 !== pos && this.connectHandlers.splice(pos, 1);
} else if ("disconnect" === name) {
const pos = this.disconnectHandlers.indexOf(cb);
-1 !== pos && this.disconnectHandlers.splice(pos, 1);
} else if ("reconnect" === name) {
const pos = this.reconnectHandlers.indexOf(cb);
-1 !== pos && this.reconnectHandlers.splice(pos, 1);
} else if ("error" === name) {
const pos = this.errorHandlers.indexOf(cb);
-1 !== pos && this.errorHandlers.splice(pos, 1);
} else if (this.handlers[name]) {
const pos = this.handlers[name].indexOf(cb);
-1 !== pos && (this.handlers[name].splice(pos, 1), this.handlers[name].length || delete this.handlers[name]);
}
}
close() {
if (this.pingInterval && (clearInterval(this.pingInterval), this.pingInterval = null), this.authTimeout && (clearTimeout(this.authTimeout), this.authTimeout = null), this.connectingTimer && (clearTimeout(this.connectingTimer), this.connectingTimer = null), this.socket) {
try {
this.socket.close();
} catch {
}
this.socket = null;
}
return this.connected && (this.disconnectHandlers.forEach((cb) => cb.call(this)), this.connected = false), this.callbacks = [], this._reconnect(), this;
}
disconnect = this.close;
destroy() {
this.close(), this.connectTimer && (clearTimeout(this.connectTimer), this.connectTimer = null);
}
_reconnect() {
this.connectTimer ? this.log.debug(`Reconnect is already running ${this.connectionCount}`) : (this.log.debug(`Start reconnect ${this.connectionCount}`), this.connectTimer = setTimeout(() => {
if (!this.options) throw new Error("No options provided!");
this.connectTimer = null, this.connectionCount < (this.options?.connectMaxAttempt || 5) && this.connectionCount++, this.connect(this.url, this.options);
}, this.connectionCount * (this.options?.connectInterval || 1e3)));
}
}
function connect(url, options) {
const socketClient = new SocketClient();
return socketClient.connect(url, options), socketClient;
}
__name(connect, "connect");
globalThis.io = { connect };
//# sourceMappingURL=socket.io.min.js.map