UNPKG

mstf-kit

Version:

一个现代化的 JavaScript/TypeScript 工具库,提供了丰富的常用工具函数

851 lines (850 loc) 25.7 kB
var WebSocketState = /* @__PURE__ */ ((WebSocketState2) => { WebSocketState2["CONNECTING"] = "connecting"; WebSocketState2["OPEN"] = "open"; WebSocketState2["CLOSING"] = "closing"; WebSocketState2["CLOSED"] = "closed"; WebSocketState2["RECONNECTING"] = "reconnecting"; return WebSocketState2; })(WebSocketState || {}); var MessagePriority = /* @__PURE__ */ ((MessagePriority2) => { MessagePriority2["HIGH"] = "high"; MessagePriority2["NORMAL"] = "normal"; MessagePriority2["LOW"] = "low"; return MessagePriority2; })(MessagePriority || {}); class WebSocketClient { /** * 创建WebSocket客户端实例 * @param options WebSocket配置选项 */ constructor(options) { this.socket = null; this.state = "closed"; this.messageQueue = []; this.pendingMessages = /* @__PURE__ */ new Map(); this.reconnectAttempts = 0; this.reconnectTimer = null; this.heartbeatTimer = null; this.heartbeatTimeoutTimer = null; this.batchTimer = null; this.batchedMessages = []; this.eventListeners = /* @__PURE__ */ 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: true, interval: 3e4, message: { type: "ping" }, timeout: 5e3 }; this.defaultReconnectConfig = { enabled: true, maxRetries: -1, // 无限重试 initialDelay: 1e3, maxDelay: 3e4, factor: 1.5, jitter: 0.5 }; this.defaultMessageConfig = { timeout: 1e4, maxQueueSize: 100, autoJsonStringify: true, autoJsonParse: true, batchThreshold: 0, // 默认不开启批处理 batchTimeWindow: 50 }; this.options = options; this.heartbeatConfig = { ...this.defaultHeartbeatConfig, ...options.heartbeat }; this.reconnectConfig = { ...this.defaultReconnectConfig, ...options.reconnect }; this.messageConfig = { ...this.defaultMessageConfig, ...options.message }; ["open", "close", "error", "message", "reconnect", "reconnect_failed", "reconnecting", "heartbeat"].forEach((event) => { this.eventListeners.set(event, /* @__PURE__ */ new Set()); }); } /** * 连接到WebSocket服务器 * @returns 连接Promise */ connect() { if (this.socket && (this.socket.readyState === WebSocket.OPEN || this.socket.readyState === WebSocket.CONNECTING)) { return Promise.resolve(this); } this.setState( "connecting" /* CONNECTING */ ); this.connectionStartTime = Date.now(); return new Promise((resolve, reject) => { try { this.socket = new WebSocket(this.options.url, this.options.protocols); this.socket.binaryType = "arraybuffer"; const connectionTimeout = setTimeout(() => { if (this.state === "connecting") { const error = new Error(`连接超时: ${this.options.connectionTimeout}ms`); this.log("error", error.message); this.cleanupSocket(); reject(error); this.handleReconnect(); } }, this.options.connectionTimeout || 1e4); this.socket.onopen = async (event) => { clearTimeout(connectionTimeout); this.setState( "open" /* OPEN */ ); this.reconnectAttempts = 0; this.log("info", `连接成功,耗时: ${Date.now() - this.connectionStartTime}ms`); if (this.options.authHandler) { try { await this.options.authHandler(this); this.log("info", "认证成功"); } catch (authError) { this.log("error", "认证失败", authError); this.close(4e3, "认证失败"); reject(new Error("认证失败")); return; } } this.startHeartbeat(); this.processPendingMessages(); this.emit("open", event); resolve(this); }; this.socket.onmessage = (event) => { this.lastMessageTime = Date.now(); let data = event.data; this.metrics.messagesReceived++; if (typeof data === "string") { this.metrics.bytesReceived += data.length; } else if (data instanceof ArrayBuffer) { this.metrics.bytesReceived += data.byteLength; } else if (data instanceof Blob) { this.metrics.bytesReceived += data.size; } if (this.messageConfig.autoJsonParse && typeof data === "string") { try { data = JSON.parse(data); } catch (e) { this.log("warn", "无法解析JSON消息", e); } } if (data && typeof data === "object" && (data.type === "pong" || data.type === "ping" || data.type === "heartbeat")) { this.handleHeartbeatResponse(data); return; } if (data && typeof data === "object" && data.id && this.pendingMessages.has(data.id)) { const pending = this.pendingMessages.get(data.id); clearTimeout(pending.timeout); this.pendingMessages.delete(data.id); pending.resolve(data); this.log("debug", `收到消息响应: ${data.id}`); } else { if (this.options.messageHandler) { this.options.messageHandler(data, this); } this.emit("message", data); } }; this.socket.onclose = (event) => { clearTimeout(connectionTimeout); const wasConnected = this.state === "open"; this.setState( "closed" /* CLOSED */ ); this.log("info", `连接关闭: [${event.code}] ${event.reason || "未提供原因"}`); this.stopHeartbeat(); if (wasConnected && event.code !== 1e3 && event.code !== 1001) { this.handleReconnect(); } this.emit("close", event); if (this.state === "connecting") { reject(new Error(`连接失败: [${event.code}] ${event.reason || "未提供原因"}`)); } }; this.socket.onerror = (event) => { this.metrics.errors++; this.log("error", "连接错误", event); this.emit("error", event); if (this.state === "connecting") { clearTimeout(connectionTimeout); reject(new Error("连接错误")); } }; } catch (error) { this.log("error", "创建WebSocket实例失败", error); this.setState( "closed" /* CLOSED */ ); reject(error); } }); } /** * 关闭WebSocket连接 * @param code 关闭代码 * @param reason 关闭原因 */ close(code, reason) { if (!this.socket || this.state === "closed") { return; } this.log("info", `主动关闭连接: [${code || 1e3}] ${reason || ""}`); this.stopHeartbeat(); this.stopReconnect(); this.rejectAllPendingMessages(new Error("连接关闭")); try { this.setState( "closing" /* CLOSING */ ); this.socket.close(code || 1e3, reason); } catch (e) { this.log("error", "关闭连接错误", e); } } /** * 发送消息 * @param data 要发送的数据 * @param options 发送选项 * @returns 如果expectResponse为true,则返回响应数据的Promise */ send(data, options = {}) { const defaultOptions = { id: this.generateId(), expectResponse: false, timeout: this.messageConfig.timeout, retries: 0, retryDelay: 1e3, priority: "normal", cacheIfOffline: true, forceText: false }; const sendOptions = { ...defaultOptions, ...options }; return new Promise((resolve, reject) => { const message = { id: sendOptions.id, data, options: sendOptions, resolve, reject, timestamp: Date.now(), retryCount: 0 }; if (this.state !== "open") { if (sendOptions.cacheIfOffline) { this.queueMessage(message); this.log("debug", `消息已加入队列 (ID: ${message.id})`); return; } else { return reject(new Error("未连接到服务器")); } } if (this.messageConfig.batchThreshold > 0 && !sendOptions.expectResponse) { this.addToBatch(message); return; } this.sendMessage(message); }); } /** * 订阅WebSocket事件 * @param event 事件名称 * @param listener 监听器函数 * @returns this 实例,用于链式调用 */ on(event, listener) { if (!this.eventListeners.has(event)) { this.eventListeners.set(event, /* @__PURE__ */ new Set()); } this.eventListeners.get(event).add(listener); return this; } /** * 取消订阅WebSocket事件 * @param event 事件名称 * @param listener 监听器函数 * @returns this 实例,用于链式调用 */ off(event, listener) { if (this.eventListeners.has(event)) { this.eventListeners.get(event).delete(listener); } return this; } /** * 获取当前WebSocket状态 * @returns 当前状态 */ getState() { return this.state; } /** * 获取延迟时间 (从最后一次心跳响应计算) * @returns 延迟时间 (ms) */ getLatency() { return this.lastMessageTime > 0 ? Date.now() - this.lastMessageTime : -1; } /** * 获取连接统计信息 * @returns 统计指标 */ getMetrics() { return { ...this.metrics }; } /** * 清除消息队列 */ clearQueue() { this.messageQueue.forEach((message) => { message.reject(new Error("消息队列已清除")); }); this.messageQueue = []; } /** * 检查连接是否活跃 * @returns 是否为活跃连接 */ isConnected() { return this.state === "open"; } /** * 手动触发重连 * @returns 重连Promise */ reconnect() { if (this.state === "open" || this.state === "connecting") { this.cleanupSocket(); } this.reconnectAttempts = 0; return this.handleReconnect(true); } // ===== 私有辅助方法 ===== /** * 触发事件 * @param event 事件名称 * @param data 事件数据 */ emit(event, data) { if (this.eventListeners.has(event)) { this.eventListeners.get(event).forEach((listener) => { try { listener(data); } catch (e) { this.log("error", `事件监听器错误 (${event})`, e); } }); } } /** * 设置WebSocket状态 * @param state 新状态 */ setState(state) { this.log("debug", `状态变更: ${this.state} -> ${state}`); this.state = state; } /** * 生成唯一消息ID * @returns 唯一ID */ generateId() { return Date.now().toString(36) + Math.random().toString(36).substr(2, 5); } /** * 发送实际的WebSocket消息 * @param message 消息对象 */ sendMessage(message) { var _a; if (!this.socket || this.state !== "open") { if (message.options.cacheIfOffline) { this.queueMessage(message); } else { message.reject(new Error("未连接到服务器")); } return; } try { let sendData = message.data; let isBinary = message.options.binary === true; if (this.messageConfig.autoJsonStringify && typeof sendData !== "string" && !(sendData instanceof ArrayBuffer) && !(sendData instanceof Blob)) { if (message.options.expectResponse && typeof sendData === "object") { sendData = { ...sendData, id: message.id }; } sendData = JSON.stringify(sendData); isBinary = false; } this.socket.send(sendData); this.metrics.messagesSent++; if (typeof sendData === "string") { this.metrics.bytesSent += sendData.length; } else if (sendData instanceof ArrayBuffer) { this.metrics.bytesSent += sendData.byteLength; } else if (sendData instanceof Blob) { this.metrics.bytesSent += sendData.size; } this.log("debug", `已发送消息 (ID: ${message.id})`); if (message.options.expectResponse) { this.pendingMessages.set(message.id, message); message.timeout = setTimeout(() => { this.pendingMessages.delete(message.id); if (message.retryCount < (message.options.retries || 0)) { message.retryCount++; this.log("warn", `消息超时,正在重试 (${message.retryCount}/${message.options.retries}) ID: ${message.id}`); setTimeout(() => { this.sendMessage(message); }, message.options.retryDelay); } else { message.reject(new Error(`消息超时 (ID: ${message.id})`)); } }, message.options.timeout); } else { message.resolve(void 0); } } catch (error) { this.log("error", "发送消息失败", error); if (message.options.cacheIfOffline && (((_a = this.socket) == null ? void 0 : _a.readyState) !== WebSocket.OPEN || error instanceof Error && error.message.includes("INVALID_STATE_ERR"))) { this.queueMessage(message); } else { message.reject(error); } if (this.state === "open") { this.handleConnectionFailure(); } } } /** * 将消息添加到队列 * @param message 消息对象 */ queueMessage(message) { if (this.messageQueue.length >= this.messageConfig.maxQueueSize) { if (message.options.priority === "high") { const lowPriorityIndex = this.messageQueue.findIndex( (m) => m.options.priority === "low" /* LOW */ ); if (lowPriorityIndex >= 0) { const removed = this.messageQueue.splice(lowPriorityIndex, 1)[0]; removed.reject(new Error("由于队列已满,低优先级消息被丢弃")); } else { message.reject(new Error("消息队列已满")); return; } } else { message.reject(new Error("消息队列已满")); return; } } if (message.options.priority === "high") { this.messageQueue.unshift(message); } else if (message.options.priority === "low") { this.messageQueue.push(message); } else { const lastNormalIndex = [...this.messageQueue].reverse().findIndex( (m) => m.options.priority !== "low" /* LOW */ ); if (lastNormalIndex >= 0) { this.messageQueue.splice(this.messageQueue.length - lastNormalIndex, 0, message); } else { this.messageQueue.push(message); } } } /** * 处理队列中的消息 */ processPendingMessages() { if (this.messageQueue.length === 0 || this.state !== "open") { return; } this.log("info", `处理排队消息: ${this.messageQueue.length}条`); const pendingMessages = [...this.messageQueue]; this.messageQueue = []; pendingMessages.forEach((message) => { this.sendMessage(message); }); } /** * 将消息添加到批处理 * @param message 消息对象 */ addToBatch(message) { this.batchedMessages.push(message.data); if (this.batchedMessages.length >= this.messageConfig.batchThreshold || this.batchedMessages.length === 1 && this.batchTimer === null) { this.flushBatchMessages(); } message.resolve(void 0); } /** * 发送批处理消息 */ flushBatchMessages() { if (this.batchedMessages.length === 0) { return; } if (this.batchTimer !== null) { clearTimeout(this.batchTimer); this.batchTimer = null; } if (this.state === "open") { const batchData = { type: "batch", messages: [...this.batchedMessages], count: this.batchedMessages.length }; try { const data = this.messageConfig.autoJsonStringify || typeof batchData !== "string" ? JSON.stringify(batchData) : batchData; this.socket.send(data); this.metrics.messagesSent++; if (typeof data === "string") { this.metrics.bytesSent += data.length; } this.log("debug", `发送批量消息: ${batchData.count}条`); } catch (error) { this.log("error", "发送批量消息失败", error); if (this.state === "open") { this.handleConnectionFailure(); } } } else { this.batchedMessages.forEach((data) => { this.queueMessage({ id: this.generateId(), data, options: { cacheIfOffline: true, priority: "normal" /* NORMAL */ }, resolve: () => { }, reject: () => { }, timestamp: Date.now(), retryCount: 0 }); }); } this.batchedMessages = []; this.batchTimer = setTimeout(() => { this.batchTimer = null; this.flushBatchMessages(); }, this.messageConfig.batchTimeWindow); } /** * 启动心跳 */ startHeartbeat() { if (!this.heartbeatConfig.enabled || this.heartbeatTimer !== null) { return; } this.log("debug", `启动心跳: ${this.heartbeatConfig.interval}ms`); this.heartbeatTimer = setInterval(() => { this.sendHeartbeat(); }, this.heartbeatConfig.interval); } /** * 发送心跳 */ sendHeartbeat() { if (this.state !== "open") { return; } this.log("debug", "发送心跳"); try { const data = this.messageConfig.autoJsonStringify ? JSON.stringify(this.heartbeatConfig.message) : this.heartbeatConfig.message; this.socket.send(data); this.emit("heartbeat", "sent"); this.heartbeatTimeoutTimer = setTimeout(() => { this.log("warn", "心跳超时"); this.emit("heartbeat", "timeout"); this.handleConnectionFailure(); }, this.heartbeatConfig.timeout); } catch (error) { this.log("error", "发送心跳失败", error); this.handleConnectionFailure(); } } /** * 处理心跳响应 * @param data 响应数据 */ handleHeartbeatResponse(data) { if (this.heartbeatTimeoutTimer !== null) { clearTimeout(this.heartbeatTimeoutTimer); this.heartbeatTimeoutTimer = null; } this.log("debug", "收到心跳响应"); this.emit("heartbeat", "received"); } /** * 停止心跳 */ stopHeartbeat() { if (this.heartbeatTimer !== null) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; } if (this.heartbeatTimeoutTimer !== null) { clearTimeout(this.heartbeatTimeoutTimer); this.heartbeatTimeoutTimer = null; } } /** * 处理连接失败 */ handleConnectionFailure() { if (this.state !== "open") { return; } this.log("warn", "检测到连接问题"); this.cleanupSocket(); this.handleReconnect(); } /** * 处理重连逻辑 * @param immediate 是否立即重连 * @returns 重连Promise */ handleReconnect(immediate = false) { if (!this.reconnectConfig.enabled) { this.log("info", "自动重连已禁用"); return Promise.reject(new Error("自动重连已禁用")); } if (this.reconnectTimer !== null) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } if (this.reconnectConfig.maxRetries !== -1 && this.reconnectAttempts >= this.reconnectConfig.maxRetries) { this.log("error", `重连失败: 已超过最大重试次数 (${this.reconnectConfig.maxRetries})`); this.emit("reconnect_failed", this.reconnectAttempts); return Promise.reject(new Error(`已超过最大重试次数 (${this.reconnectConfig.maxRetries})`)); } let delay = immediate ? 0 : this.calculateReconnectDelay(); this.setState( "reconnecting" /* RECONNECTING */ ); this.reconnectAttempts++; this.log("info", `尝试重连: 第${this.reconnectAttempts}次 (延迟: ${delay}ms)`); this.emit("reconnecting", this.reconnectAttempts); return new Promise((resolve, reject) => { this.reconnectTimer = setTimeout(async () => { this.reconnectTimer = null; try { await this.connect(); this.metrics.reconnects++; this.emit("reconnect", this.reconnectAttempts); resolve(this); } catch (error) { this.log("error", `重连失败: ${error}`); this.handleReconnect().then(resolve).catch(reject); } }, delay); }); } /** * 计算重连延迟时间 * @returns 延迟时间 (ms) */ calculateReconnectDelay() { let delay = this.reconnectConfig.initialDelay * Math.pow(this.reconnectConfig.factor, this.reconnectAttempts); const jitter = this.reconnectConfig.jitter; if (jitter > 0) { delay = delay * (1 + jitter * (Math.random() * 2 - 1)); } return Math.min(delay, this.reconnectConfig.maxDelay); } /** * 清理Socket资源 */ cleanupSocket() { if (!this.socket) { return; } this.log("debug", "清理Socket资源"); this.stopHeartbeat(); this.socket.onopen = null; this.socket.onclose = null; this.socket.onerror = null; this.socket.onmessage = null; try { if (this.socket.readyState === WebSocket.OPEN || this.socket.readyState === WebSocket.CONNECTING) { this.socket.close(); } } catch (e) { this.log("error", "关闭socket错误", e); } this.socket = null; } /** * 停止重连尝试 */ stopReconnect() { if (this.reconnectTimer !== null) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } } /** * 拒绝所有等待的消息 * @param reason 拒绝原因 */ rejectAllPendingMessages(reason) { this.log("debug", `拒绝 ${this.pendingMessages.size} 个等待响应的消息`); this.pendingMessages.forEach((message) => { if (message.timeout) { clearTimeout(message.timeout); } message.reject(reason); }); this.pendingMessages.clear(); } /** * 记录日志 * @param level 日志级别 * @param message 日志消息 * @param error 错误对象(可选) */ log(level, message, error) { if (!this.options.debug && level === "debug") { return; } const timestamp = (/* @__PURE__ */ new Date()).toISOString(); const logMessage = `[${timestamp}] [${level.toUpperCase()}] ${message}`; switch (level) { case "debug": console.debug(logMessage); break; case "info": console.info(logMessage); break; case "warn": console.warn(logMessage); break; case "error": console.error(logMessage, error || ""); break; } } /** * 获取连接统计信息 * @returns 统计信息对象 */ getConnectionStats() { const uptime = this.connectionStartTime > 0 ? Date.now() - this.connectionStartTime : 0; return { uptime, 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(); } /** * 设置心跳配置 * @param config 心跳配置 */ setHeartbeatConfig(config) { this.heartbeatConfig = { ...this.heartbeatConfig, ...config }; if (this.state === "open") { this.stopHeartbeat(); this.startHeartbeat(); } } /** * 设置重连配置 * @param config 重连配置 */ setReconnectConfig(config) { this.reconnectConfig = { ...this.reconnectConfig, ...config }; } /** * 设置消息配置 * @param config 消息配置 */ setMessageConfig(config) { this.messageConfig = { ...this.messageConfig, ...config }; if (this.batchTimer !== null) { clearTimeout(this.batchTimer); this.batchTimer = null; } if (this.messageConfig.batchThreshold > 0 && this.batchedMessages.length > 0) { this.flushBatchMessages(); } } /** * 获取当前配置 * @returns 当前配置对象 */ getConfig() { return { heartbeat: { ...this.heartbeatConfig }, reconnect: { ...this.reconnectConfig }, message: { ...this.messageConfig } }; } /** * 销毁WebSocket客户端 */ destroy() { this.log("info", "销毁WebSocket客户端"); this.stopHeartbeat(); this.stopReconnect(); if (this.batchTimer !== null) { 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(); } } export { MessagePriority, WebSocketClient, WebSocketState };