websocket-pro-client
Version:
High-performance WebSocket client with auto-reconnect, heartbeat and priority messaging
2 lines (1 loc) • 7.72 kB
JavaScript
(function(r,h){typeof exports=="object"&&typeof module<"u"?h(exports):typeof define=="function"&&define.amd?define(["exports"],h):(r=typeof globalThis<"u"?globalThis:r||self,h(r.WebSocketPro={}))})(this,function(r){"use strict";const h={maxReconnectAttempts:10,reconnectDelay:1e3,reconnectExponent:1.5,maxReconnectDelay:3e4,connectionPoolSize:5,maxConcurrent:1,defaultPriority:1,enableCompression:!1,serializer:{serialize:JSON.stringify,deserialize:JSON.parse},isNeedHeartbeat:!0,heartbeat:{interval:25e3,timeout:1e4,message:"PING"}};class l{constructor(){this.events={}}on(e,t){return this.events[e]||(this.events[e]=[]),this.events[e].push(t),()=>this.off(e,t)}off(e,t){this.events[e]&&(this.events[e]=this.events[e].filter(i=>i!==t))}emit(e,...t){this.events[e]&&this.events[e].forEach(i=>{try{i(...t)}catch(s){console.error(`Event "${e}" listener error:`,s)}})}once(e,t){const i=(...s)=>{this.off(e,i),t(...s)};this.on(e,i)}removeAllListeners(e){e?delete this.events[e]:this.events={}}}class d extends l{constructor(e={},t){super(),this.config=e,this.sendPing=t,this.lastPongTime=0}start(){this.config||console.log("[Heartbeat] config is empty"),this.stop(),this.lastPongTime=Date.now(),this.intervalId=setInterval(()=>{this.sendPing(),this.timeoutId=setTimeout(()=>{this.handleDefaultTimeout(this.config.onTimeout)},this.config.timeout)},this.config.interval)}stop(){clearInterval(this.intervalId),clearTimeout(this.timeoutId)}handleDefaultTimeout(e){this.stop(),e&&e(),this.emit("timeout")}recordPong(){this.lastPongTime=Date.now(),clearTimeout(this.timeoutId),this.emit("pong",Date.now()-this.lastPongTime)}getLastPongTime(){return this.lastPongTime}updateConfig(e){if(!(e!=null&&e.isNeedHeartbeat)){this.stop();return}this.config={...this.config,...e.heartbeat},this.stop(),this.start()}}class p{constructor(e,t){this.maxConcurrent=e,this.onTaskError=t,this.queue=[],this.runningCount=0}add(e,t){return new Promise((i,s)=>{const o=async()=>{var a;try{await e(),i()}catch(c){(a=this.onTaskError)==null||a.call(this,c),s(c)}};this.queue.push({task:o,priority:t}),this.queue.sort((a,c)=>c.priority-a.priority),this.run()})}run(){for(;this.runningCount<this.maxConcurrent&&this.queue.length>0;){const{task:e}=this.queue.shift();this.runningCount++,e().finally(()=>{this.runningCount--,this.run()})}}clear(){this.queue=[]}updateThresholds(e){e!==void 0&&(this.maxConcurrent=e),this.run()}}const u=(n,e)=>{const t={...n};for(const i in e)e[i]instanceof Object&&!Array.isArray(e[i])?t[i]=u(n[i]||{},e[i]):t[i]=e[i];return t},f=(n,e)=>{if(n===e)return!0;if(n==null||e==null||typeof n!="object"||typeof e!="object")return n===e;if(Array.isArray(n)&&Array.isArray(e)){if(n.length!==e.length)return!1;for(let s=0;s<n.length;s++)if(!f(n[s],e[s]))return!1;return!0}if(Array.isArray(n)||Array.isArray(e))return!1;const t=Object.keys(n),i=Object.keys(e);if(t.length!==i.length)return!1;for(const s of t)if(!e.hasOwnProperty(s)||!f(n[s],e[s]))return!1;return!0};class g extends l{constructor(e,t,i){super(),this.url=e,this.protocols=t,this.config=i,this.socket=null,this.reconnectAttempts=0,this.messageQueue=[],this.isUpdatingConfig=!1,this.configQueue=[],this.currentConfig=u(h,this.config),this.initHeartbeat(),this.scheduler=new p(this.currentConfig.maxConcurrent,s=>this.emit("error",s)),this.connect()}initHeartbeat(){this.currentConfig.isNeedHeartbeat&&(this.heartbeat=new d(this.currentConfig.heartbeat,()=>{var e;this.send(((e=this.currentConfig.heartbeat)==null?void 0:e.message)||"PING")}),this.heartbeat.on("timeout",()=>{var e;console.log("Heartbeat timeout, triggering reconnect..."),((e=this.socket)==null?void 0:e.readyState)===WebSocket.OPEN&&this.close(1e3,"heartbeat timeout"),this.scheduleReconnect()}))}connect(){this.socket=new WebSocket(this.url,this.protocols),this.socket.binaryType="arraybuffer",this.socket.onopen=e=>{this.reconnectAttempts=0,clearTimeout(this.reconnectTimer),this.heartbeat&&this.heartbeat.start(),this.flushMessageQueue(),this.emit("open",e)},this.socket.onmessage=e=>{if(e.data==="pong"){this.heartbeat&&this.heartbeat.recordPong();return}this.emit("message",e.data)},this.socket.onclose=e=>{this.heartbeat&&this.heartbeat.stop(),this.emit("close",e)},this.socket.onerror=e=>{this.heartbeat&&this.heartbeat.stop(),this.emit("error",e),this.scheduleReconnect()}}sendRaw(e){var t;((t=this.socket)==null?void 0:t.readyState)===WebSocket.OPEN&&this.socket.send(e)}scheduleReconnect(){if(this.reconnectAttempts>=this.currentConfig.maxReconnectAttempts){this.emit("overMaxReconnectAttempts");return}const e=Math.min(this.currentConfig.reconnectDelay*Math.pow(this.currentConfig.reconnectExponent,this.reconnectAttempts),this.currentConfig.maxReconnectDelay),i=e*.2*(Math.random()*2-1),s=Math.max(1e3,e+i);this.reconnectTimer=setTimeout(()=>{this.reconnectAttempts++,this.connect()},s)}flushMessageQueue(){for(;this.messageQueue.length>0;){const{data:e,resolve:t,reject:i}=this.messageQueue.shift();this.send(e).then(t).catch(i)}}send(e,t=this.currentConfig.defaultPriority){var i;return((i=this.socket)==null?void 0:i.readyState)===WebSocket.OPEN?this.scheduler.add(()=>new Promise((s,o)=>{try{this.sendRaw(e),s()}catch(a){o(a)}}),t):new Promise((s,o)=>{this.messageQueue.push({data:e,priority:t,resolve:s,reject:o})})}close(e,t){var i;clearTimeout(this.reconnectTimer),this.heartbeat&&this.heartbeat.stop(),(i=this.socket)==null||i.close(e,t),this.socket=null}reconnect(){clearTimeout(this.reconnectTimer),this.reconnectAttempts=0,this.close(),this.connect()}async updateConfig(e){if(this.configQueue.push(e),!this.isUpdatingConfig){for(this.isUpdatingConfig=!0;this.configQueue.length>0;){const t=this.configQueue.shift();await this.applyConfigSafely(t)}this.isUpdatingConfig=!1}}applyConfigSafely(e){const t={...this.currentConfig};this.currentConfig=u(this.currentConfig,e),this.handleConfigChange(t,this.currentConfig),this.applyConfig()}handleConfigChange(e,t){f(e.heartbeat,t.heartbeat)||this.reInitHeartbeat(),(e.maxReconnectAttempts!==t.maxReconnectAttempts||e.reconnectDelay!==t.reconnectDelay||e.reconnectExponent!==t.reconnectExponent||e.maxReconnectDelay!==t.maxReconnectDelay)&&this.resetReconnectTimer()}applyConfig(){var e,t;(e=this.heartbeat)==null||e.updateConfig(this.currentConfig),(t=this.scheduler)==null||t.updateThresholds(this.currentConfig.maxConcurrent)}reInitHeartbeat(){var e;if(!this.currentConfig.isNeedHeartbeat){(e=this.heartbeat)==null||e.stop(),this.heartbeat=void 0;return}this.heartbeat?(this.heartbeat.stop(),this.heartbeat.start()):this.initHeartbeat()}resetReconnectTimer(){this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.scheduleReconnect())}}class m extends l{constructor(e){super(),this.config=e,this.clients=new Map}connect(e,t=[]){const i=`${e}|${t.join(",")}`;if(this.clients.has(i))return this.clients.get(i);const s=new g(e,t,this.config);this.clients.set(i,s);const o=a=>c=>{this.emit(a,{url:e,protocols:t,data:c})};return s.on("open",o("open")),s.on("message",o("message")),s.on("close",o("close")),s.on("error",o("error")),s}closeAll(e,t){this.clients.forEach(i=>i.close(e,t)),this.clients.clear()}getClient(e,t){const i=`${e}|${(t==null?void 0:t.join(","))||""}`;return this.clients.get(i)}}const y=(n={})=>{const e={...h,...n,serializer:{...h.serializer,...n.serializer}};return new m(e)},b={serialize:JSON.stringify,deserialize:JSON.parse},C={serialize:n=>{throw new Error("MsgPack serializer requires @msgpack/msgpack installation")},deserialize:n=>{throw new Error("MsgPack serializer requires @msgpack/msgpack installation")}};r.EventEmitter=l,r.JsonSerializer=b,r.MsgPackSerializer=C,r.WebSocketClient=g,r.WebSocketManager=m,r.createWebSocketManager=y,Object.defineProperty(r,Symbol.toStringTag,{value:"Module"})});