mstf-kit
Version:
一个现代化的 JavaScript/TypeScript 工具库,提供了丰富的常用工具函数
2 lines (1 loc) • 14.5 kB
JavaScript
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});var e=(e=>(e.CONNECTING="connecting",e.OPEN="open",e.CLOSING="closing",e.CLOSED="closed",e.RECONNECTING="reconnecting",e))(e||{}),t=(e=>(e.HIGH="high",e.NORMAL="normal",e.LOW="low",e))(t||{});exports.MessagePriority=t,exports.WebSocketClient=class{constructor(e){this.socket=null,this.state="closed",this.messageQueue=[],this.pendingMessages=new Map,this.reconnectAttempts=0,this.reconnectTimer=null,this.heartbeatTimer=null,this.heartbeatTimeoutTimer=null,this.batchTimer=null,this.batchedMessages=[],this.eventListeners=new Map,this.connectionStartTime=0,this.lastMessageTime=0,this.metrics={messagesSent:0,messagesReceived:0,bytesReceived:0,bytesSent:0,errors:0,reconnects:0},this.defaultHeartbeatConfig={enabled:!0,interval:3e4,message:{type:"ping"},timeout:5e3},this.defaultReconnectConfig={enabled:!0,maxRetries:-1,initialDelay:1e3,maxDelay:3e4,factor:1.5,jitter:.5},this.defaultMessageConfig={timeout:1e4,maxQueueSize:100,autoJsonStringify:!0,autoJsonParse:!0,batchThreshold:0,batchTimeWindow:50},this.options=e,this.heartbeatConfig={...this.defaultHeartbeatConfig,...e.heartbeat},this.reconnectConfig={...this.defaultReconnectConfig,...e.reconnect},this.messageConfig={...this.defaultMessageConfig,...e.message},["open","close","error","message","reconnect","reconnect_failed","reconnecting","heartbeat"].forEach((e=>{this.eventListeners.set(e,new Set)}))}connect(){return!this.socket||this.socket.readyState!==WebSocket.OPEN&&this.socket.readyState!==WebSocket.CONNECTING?(this.setState("connecting"),this.connectionStartTime=Date.now(),new Promise(((e,t)=>{try{this.socket=new WebSocket(this.options.url,this.options.protocols),this.socket.binaryType="arraybuffer";const s=setTimeout((()=>{if("connecting"===this.state){const e=new Error(`连接超时: ${this.options.connectionTimeout}ms`);this.log("error",e.message),this.cleanupSocket(),t(e),this.handleReconnect()}}),this.options.connectionTimeout||1e4);this.socket.onopen=async i=>{if(clearTimeout(s),this.setState("open"),this.reconnectAttempts=0,this.log("info",`连接成功,耗时: ${Date.now()-this.connectionStartTime}ms`),this.options.authHandler)try{await this.options.authHandler(this),this.log("info","认证成功")}catch(n){return this.log("error","认证失败",n),this.close(4e3,"认证失败"),void t(new Error("认证失败"))}this.startHeartbeat(),this.processPendingMessages(),this.emit("open",i),e(this)},this.socket.onmessage=e=>{this.lastMessageTime=Date.now();let t=e.data;if(this.metrics.messagesReceived++,"string"==typeof t?this.metrics.bytesReceived+=t.length:t instanceof ArrayBuffer?this.metrics.bytesReceived+=t.byteLength:t instanceof Blob&&(this.metrics.bytesReceived+=t.size),this.messageConfig.autoJsonParse&&"string"==typeof t)try{t=JSON.parse(t)}catch(s){this.log("warn","无法解析JSON消息",s)}if(!t||"object"!=typeof t||"pong"!==t.type&&"ping"!==t.type&&"heartbeat"!==t.type)if(t&&"object"==typeof t&&t.id&&this.pendingMessages.has(t.id)){const e=this.pendingMessages.get(t.id);clearTimeout(e.timeout),this.pendingMessages.delete(t.id),e.resolve(t),this.log("debug",`收到消息响应: ${t.id}`)}else this.options.messageHandler&&this.options.messageHandler(t,this),this.emit("message",t);else this.handleHeartbeatResponse(t)},this.socket.onclose=e=>{clearTimeout(s);const i="open"===this.state;this.setState("closed"),this.log("info",`连接关闭: [${e.code}] ${e.reason||"未提供原因"}`),this.stopHeartbeat(),i&&1e3!==e.code&&1001!==e.code&&this.handleReconnect(),this.emit("close",e),"connecting"===this.state&&t(new Error(`连接失败: [${e.code}] ${e.reason||"未提供原因"}`))},this.socket.onerror=e=>{this.metrics.errors++,this.log("error","连接错误",e),this.emit("error",e),"connecting"===this.state&&(clearTimeout(s),t(new Error("连接错误")))}}catch(s){this.log("error","创建WebSocket实例失败",s),this.setState("closed"),t(s)}}))):Promise.resolve(this)}close(e,t){if(this.socket&&"closed"!==this.state){this.log("info",`主动关闭连接: [${e||1e3}] ${t||""}`),this.stopHeartbeat(),this.stopReconnect(),this.rejectAllPendingMessages(new Error("连接关闭"));try{this.setState("closing"),this.socket.close(e||1e3,t)}catch(s){this.log("error","关闭连接错误",s)}}}send(e,t={}){const s={id:this.generateId(),expectResponse:!1,timeout:this.messageConfig.timeout,retries:0,retryDelay:1e3,priority:"normal",cacheIfOffline:!0,forceText:!1,...t};return new Promise(((t,i)=>{const n={id:s.id,data:e,options:s,resolve:t,reject:i,timestamp:Date.now(),retryCount:0};if("open"!==this.state)return s.cacheIfOffline?(this.queueMessage(n),void this.log("debug",`消息已加入队列 (ID: ${n.id})`)):i(new Error("未连接到服务器"));this.messageConfig.batchThreshold>0&&!s.expectResponse?this.addToBatch(n):this.sendMessage(n)}))}on(e,t){return this.eventListeners.has(e)||this.eventListeners.set(e,new Set),this.eventListeners.get(e).add(t),this}off(e,t){return this.eventListeners.has(e)&&this.eventListeners.get(e).delete(t),this}getState(){return this.state}getLatency(){return this.lastMessageTime>0?Date.now()-this.lastMessageTime:-1}getMetrics(){return{...this.metrics}}clearQueue(){this.messageQueue.forEach((e=>{e.reject(new Error("消息队列已清除"))})),this.messageQueue=[]}isConnected(){return"open"===this.state}reconnect(){return"open"!==this.state&&"connecting"!==this.state||this.cleanupSocket(),this.reconnectAttempts=0,this.handleReconnect(!0)}emit(e,t){this.eventListeners.has(e)&&this.eventListeners.get(e).forEach((s=>{try{s(t)}catch(i){this.log("error",`事件监听器错误 (${e})`,i)}}))}setState(e){this.log("debug",`状态变更: ${this.state} -> ${e}`),this.state=e}generateId(){return Date.now().toString(36)+Math.random().toString(36).substr(2,5)}sendMessage(e){var t;if(this.socket&&"open"===this.state)try{let t=e.data,s=!0===e.options.binary;!this.messageConfig.autoJsonStringify||"string"==typeof t||t instanceof ArrayBuffer||t instanceof Blob||(e.options.expectResponse&&"object"==typeof t&&(t={...t,id:e.id}),t=JSON.stringify(t),s=!1),this.socket.send(t),this.metrics.messagesSent++,"string"==typeof t?this.metrics.bytesSent+=t.length:t instanceof ArrayBuffer?this.metrics.bytesSent+=t.byteLength:t instanceof Blob&&(this.metrics.bytesSent+=t.size),this.log("debug",`已发送消息 (ID: ${e.id})`),e.options.expectResponse?(this.pendingMessages.set(e.id,e),e.timeout=setTimeout((()=>{this.pendingMessages.delete(e.id),e.retryCount<(e.options.retries||0)?(e.retryCount++,this.log("warn",`消息超时,正在重试 (${e.retryCount}/${e.options.retries}) ID: ${e.id}`),setTimeout((()=>{this.sendMessage(e)}),e.options.retryDelay)):e.reject(new Error(`消息超时 (ID: ${e.id})`))}),e.options.timeout)):e.resolve(void 0)}catch(s){this.log("error","发送消息失败",s),e.options.cacheIfOffline&&((null==(t=this.socket)?void 0:t.readyState)!==WebSocket.OPEN||s instanceof Error&&s.message.includes("INVALID_STATE_ERR"))?this.queueMessage(e):e.reject(s),"open"===this.state&&this.handleConnectionFailure()}else e.options.cacheIfOffline?this.queueMessage(e):e.reject(new Error("未连接到服务器"))}queueMessage(e){if(this.messageQueue.length>=this.messageConfig.maxQueueSize){if("high"!==e.options.priority)return void e.reject(new Error("消息队列已满"));{const t=this.messageQueue.findIndex((e=>"low"===e.options.priority));if(!(t>=0))return void e.reject(new Error("消息队列已满"));this.messageQueue.splice(t,1)[0].reject(new Error("由于队列已满,低优先级消息被丢弃"))}}if("high"===e.options.priority)this.messageQueue.unshift(e);else if("low"===e.options.priority)this.messageQueue.push(e);else{const t=[...this.messageQueue].reverse().findIndex((e=>"low"!==e.options.priority));t>=0?this.messageQueue.splice(this.messageQueue.length-t,0,e):this.messageQueue.push(e)}}processPendingMessages(){if(0===this.messageQueue.length||"open"!==this.state)return;this.log("info",`处理排队消息: ${this.messageQueue.length}条`);const e=[...this.messageQueue];this.messageQueue=[],e.forEach((e=>{this.sendMessage(e)}))}addToBatch(e){this.batchedMessages.push(e.data),(this.batchedMessages.length>=this.messageConfig.batchThreshold||1===this.batchedMessages.length&&null===this.batchTimer)&&this.flushBatchMessages(),e.resolve(void 0)}flushBatchMessages(){if(0!==this.batchedMessages.length){if(null!==this.batchTimer&&(clearTimeout(this.batchTimer),this.batchTimer=null),"open"===this.state){const t={type:"batch",messages:[...this.batchedMessages],count:this.batchedMessages.length};try{const e=this.messageConfig.autoJsonStringify||"string"!=typeof t?JSON.stringify(t):t;this.socket.send(e),this.metrics.messagesSent++,"string"==typeof e&&(this.metrics.bytesSent+=e.length),this.log("debug",`发送批量消息: ${t.count}条`)}catch(e){this.log("error","发送批量消息失败",e),"open"===this.state&&this.handleConnectionFailure()}}else this.batchedMessages.forEach((e=>{this.queueMessage({id:this.generateId(),data:e,options:{cacheIfOffline:!0,priority:"normal"},resolve:()=>{},reject:()=>{},timestamp:Date.now(),retryCount:0})}));this.batchedMessages=[],this.batchTimer=setTimeout((()=>{this.batchTimer=null,this.flushBatchMessages()}),this.messageConfig.batchTimeWindow)}}startHeartbeat(){this.heartbeatConfig.enabled&&null===this.heartbeatTimer&&(this.log("debug",`启动心跳: ${this.heartbeatConfig.interval}ms`),this.heartbeatTimer=setInterval((()=>{this.sendHeartbeat()}),this.heartbeatConfig.interval))}sendHeartbeat(){if("open"===this.state){this.log("debug","发送心跳");try{const e=this.messageConfig.autoJsonStringify?JSON.stringify(this.heartbeatConfig.message):this.heartbeatConfig.message;this.socket.send(e),this.emit("heartbeat","sent"),this.heartbeatTimeoutTimer=setTimeout((()=>{this.log("warn","心跳超时"),this.emit("heartbeat","timeout"),this.handleConnectionFailure()}),this.heartbeatConfig.timeout)}catch(e){this.log("error","发送心跳失败",e),this.handleConnectionFailure()}}}handleHeartbeatResponse(e){null!==this.heartbeatTimeoutTimer&&(clearTimeout(this.heartbeatTimeoutTimer),this.heartbeatTimeoutTimer=null),this.log("debug","收到心跳响应"),this.emit("heartbeat","received")}stopHeartbeat(){null!==this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null),null!==this.heartbeatTimeoutTimer&&(clearTimeout(this.heartbeatTimeoutTimer),this.heartbeatTimeoutTimer=null)}handleConnectionFailure(){"open"===this.state&&(this.log("warn","检测到连接问题"),this.cleanupSocket(),this.handleReconnect())}handleReconnect(e=!1){if(!this.reconnectConfig.enabled)return this.log("info","自动重连已禁用"),Promise.reject(new Error("自动重连已禁用"));if(null!==this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),-1!==this.reconnectConfig.maxRetries&&this.reconnectAttempts>=this.reconnectConfig.maxRetries)return this.log("error",`重连失败: 已超过最大重试次数 (${this.reconnectConfig.maxRetries})`),this.emit("reconnect_failed",this.reconnectAttempts),Promise.reject(new Error(`已超过最大重试次数 (${this.reconnectConfig.maxRetries})`));let t=e?0:this.calculateReconnectDelay();return this.setState("reconnecting"),this.reconnectAttempts++,this.log("info",`尝试重连: 第${this.reconnectAttempts}次 (延迟: ${t}ms)`),this.emit("reconnecting",this.reconnectAttempts),new Promise(((e,s)=>{this.reconnectTimer=setTimeout((async()=>{this.reconnectTimer=null;try{await this.connect(),this.metrics.reconnects++,this.emit("reconnect",this.reconnectAttempts),e(this)}catch(t){this.log("error",`重连失败: ${t}`),this.handleReconnect().then(e).catch(s)}}),t)}))}calculateReconnectDelay(){let e=this.reconnectConfig.initialDelay*Math.pow(this.reconnectConfig.factor,this.reconnectAttempts);const t=this.reconnectConfig.jitter;return t>0&&(e*=1+t*(2*Math.random()-1)),Math.min(e,this.reconnectConfig.maxDelay)}cleanupSocket(){if(this.socket){this.log("debug","清理Socket资源"),this.stopHeartbeat(),this.socket.onopen=null,this.socket.onclose=null,this.socket.onerror=null,this.socket.onmessage=null;try{this.socket.readyState!==WebSocket.OPEN&&this.socket.readyState!==WebSocket.CONNECTING||this.socket.close()}catch(e){this.log("error","关闭socket错误",e)}this.socket=null}}stopReconnect(){null!==this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null)}rejectAllPendingMessages(e){this.log("debug",`拒绝 ${this.pendingMessages.size} 个等待响应的消息`),this.pendingMessages.forEach((t=>{t.timeout&&clearTimeout(t.timeout),t.reject(e)})),this.pendingMessages.clear()}log(e,t,s){if(!this.options.debug&&"debug"===e)return;const i=`[${(new Date).toISOString()}] [${e.toUpperCase()}] ${t}`;switch(e){case"debug":console.debug(i);break;case"info":console.info(i);break;case"warn":console.warn(i);break;case"error":console.error(i,s||"")}}getConnectionStats(){return{uptime:this.connectionStartTime>0?Date.now()-this.connectionStartTime:0,messagesSent:this.metrics.messagesSent,messagesReceived:this.metrics.messagesReceived,bytesSent:this.metrics.bytesSent,bytesReceived:this.metrics.bytesReceived,errors:this.metrics.errors,reconnects:this.metrics.reconnects,latency:this.getLatency(),queueSize:this.messageQueue.length,pendingResponses:this.pendingMessages.size}}resetStats(){this.metrics={messagesSent:0,messagesReceived:0,bytesReceived:0,bytesSent:0,errors:0,reconnects:0},this.connectionStartTime=Date.now()}setHeartbeatConfig(e){this.heartbeatConfig={...this.heartbeatConfig,...e},"open"===this.state&&(this.stopHeartbeat(),this.startHeartbeat())}setReconnectConfig(e){this.reconnectConfig={...this.reconnectConfig,...e}}setMessageConfig(e){this.messageConfig={...this.messageConfig,...e},null!==this.batchTimer&&(clearTimeout(this.batchTimer),this.batchTimer=null),this.messageConfig.batchThreshold>0&&this.batchedMessages.length>0&&this.flushBatchMessages()}getConfig(){return{heartbeat:{...this.heartbeatConfig},reconnect:{...this.reconnectConfig},message:{...this.messageConfig}}}destroy(){this.log("info","销毁WebSocket客户端"),this.stopHeartbeat(),this.stopReconnect(),null!==this.batchTimer&&(clearTimeout(this.batchTimer),this.batchTimer=null),this.clearQueue(),this.rejectAllPendingMessages(new Error("客户端已销毁")),this.close(),this.eventListeners.clear(),this.state="closed",this.socket=null,this.batchedMessages=[],this.reconnectAttempts=0,this.connectionStartTime=0,this.lastMessageTime=0,this.resetStats()}},exports.WebSocketState=e;