UNPKG

@iobroker/ws

Version:
183 lines (182 loc) 11.7 kB
"use strict"; var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); /*! * ioBroker WebSockets * Copyright 2020-2025, bluefox <dogafox@gmail.com> * Released under the MIT License. * v 3.0.3 (2025_06_21) */ 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