UNPKG

@iobroker/ws

Version:
7 lines 9.38 kB
"use strict"; /*! * 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:()=>{}});const MESSAGE_TYPES={MESSAGE:0,PING:1,PONG:2,CALLBACK:3},DEBUG=!0,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\tServer",1015:"TLS handshake fail"};class SocketClient{connectHandlers=[];reconnectHandlers=[];disconnectHandlers=[];errorHandlers=[];handlers={};wasConnected=!1;connectTimer=null;connectingTimer=null;connectionCount=0;callbacks=[];pending=[];id=0;lastPong=0;socket=null;url="";options=null;pingInterval=null;sessionID=0;authTimeout=null;connected=!1;log;constructor(){this.log={debug:text=>{console.log(`[${(new Date).toISOString()}] ${text}`)},warn:text=>console.warn(`[${(new Date).toISOString()}] ${text}`),error:text=>console.error(`[${(new Date).toISOString()}] ${text}`)}}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=!0,this.wasConnected?this.reconnectHandlers.forEach(cb=>cb.call(this,!0)):(this.connectHandlers.forEach(cb=>cb.call(this,!0)),this.wasConnected=!0),this.connectingTimer&&(clearTimeout(this.connectingTimer),this.connectingTimer=null),this.pending.length&&(this.pending.forEach(({name:name,args:args})=>this.emit(name,...args)),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(cb=>cb.call(this,"Authenticate timeout"))),this.close()},this.options?.authTimeout||3e3)),this.callbacks.push({id:id,cb: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=(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:name,args:args})};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=!1),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}globalThis.io={connect:connect};