websocket-pro-client
Advanced tools
| export declare enum WebSocketErrorCode { | ||
| MsgPackNotInstalled = "MSG_PACK_NOT_INSTALLED", | ||
| AckTimeout = "ACK_TIMEOUT", | ||
| AckMaxRetries = "ACK_MAX_RETRIES", | ||
| ClosedBeforeAck = "CLOSED_BEFORE_ACK" | ||
| } | ||
| export declare const WebSocketErrorMessage: Record<WebSocketErrorCode, string>; | ||
| export declare class WebSocketClientError extends Error { | ||
| code: WebSocketErrorCode; | ||
| constructor(code: WebSocketErrorCode); | ||
| } |
| export declare enum WebSocketEvent { | ||
| Open = "open", | ||
| Message = "message", | ||
| Close = "close", | ||
| Error = "error", | ||
| Reconnect = "reconnect", | ||
| Heartbeat = "heartbeat", | ||
| Latency = "latency", | ||
| OverMaxReconnectAttempts = "overMaxReconnectAttempts" | ||
| } | ||
| export declare const CORE_WEB_SOCKET_EVENTS: WebSocketEvent[]; | ||
| export declare const ALL_WEB_SOCKET_EVENTS: WebSocketEvent[]; |
| export declare enum HeartbeatMessage { | ||
| Ping = "PING", | ||
| Pong = "PONG" | ||
| } | ||
| /** Heartbeat 内部发出的事件名 */ | ||
| export declare enum HeartbeatEvent { | ||
| Timeout = "timeout", | ||
| Pong = "PONG" | ||
| } |
| export type LogLevel = "debug" | "info" | "warn" | "error"; | ||
| export interface Logger { | ||
| debug: (...args: any[]) => void; | ||
| info: (...args: any[]) => void; | ||
| warn: (...args: any[]) => void; | ||
| error: (...args: any[]) => void; | ||
| } | ||
| export declare const getLogger: () => Logger; | ||
| export declare const setLogger: (logger: Logger) => void; |
@@ -1,2 +0,2 @@ | ||
| import { Listener } from "../types"; | ||
| import { Listener } from '../types'; | ||
| export declare class EventEmitter { | ||
@@ -3,0 +3,0 @@ private events; |
| import { HeartbeatConfig, WebSocketConfig } from '../types'; | ||
| import { EventEmitter } from "./EventEmitter"; | ||
| import { EventEmitter } from './EventEmitter'; | ||
| export declare class Heartbeat extends EventEmitter { | ||
@@ -9,3 +9,3 @@ private config; | ||
| private timeoutId?; | ||
| constructor(config: HeartbeatConfig, sendPing: () => void); | ||
| constructor(config: HeartbeatConfig | undefined, sendPing: () => void); | ||
| start(): void; | ||
@@ -12,0 +12,0 @@ stop(): void; |
@@ -1,3 +0,3 @@ | ||
| export { EventEmitter } from "./EventEmitter"; | ||
| export { WebSocketManager } from "./WebSocketManager"; | ||
| export { WebSocketClient } from "./WebSocketClient"; | ||
| export { EventEmitter } from './EventEmitter'; | ||
| export { WebSocketManager } from './WebSocketManager'; | ||
| export { WebSocketClient } from './WebSocketClient'; |
@@ -1,3 +0,3 @@ | ||
| import { EventEmitter } from "./EventEmitter"; | ||
| import { WebSocketConfig } from "../types"; | ||
| import { EventEmitter } from './EventEmitter'; | ||
| import { WebSocketConfig } from '../types'; | ||
| export declare class WebSocketClient extends EventEmitter { | ||
@@ -14,2 +14,4 @@ private readonly url; | ||
| private readonly scheduler; | ||
| private readonly pendingAcks; | ||
| private lastInboundSeq?; | ||
| private isUpdatingConfig; | ||
@@ -23,3 +25,6 @@ private configQueue; | ||
| private flushMessageQueue; | ||
| private sendInternal; | ||
| private handleAckTimeout; | ||
| send(data: any, priority?: number): Promise<void>; | ||
| sendWithAck(data: any, priority?: number): Promise<void>; | ||
| close(code?: number, reason?: string): void; | ||
@@ -26,0 +31,0 @@ reconnect(): void; |
@@ -1,4 +0,4 @@ | ||
| import { WebSocketClient } from "./WebSocketClient"; | ||
| import { EventEmitter } from "./EventEmitter"; | ||
| import { WebSocketConfig } from "../types"; | ||
| import { WebSocketClient } from './WebSocketClient'; | ||
| import { EventEmitter } from './EventEmitter'; | ||
| import { WebSocketConfig } from '../types'; | ||
| export declare class WebSocketManager extends EventEmitter { | ||
@@ -5,0 +5,0 @@ private readonly config; |
+3
-2
@@ -1,2 +0,2 @@ | ||
| import type { WebSocketConfig, WebSocketEvent, IWebSocketManager, IWebSocketClient, Serializer } from "./types"; | ||
| import { WebSocketConfig, WebSocketEvent, IWebSocketManager, IWebSocketClient, Serializer } from './types'; | ||
| /** | ||
@@ -38,2 +38,3 @@ * 创建 WebSocket 管理器实例 | ||
| export type { WebSocketConfig, WebSocketEvent, IWebSocketManager, IWebSocketClient, Serializer, }; | ||
| export { EventEmitter, WebSocketManager, WebSocketClient } from "./core"; | ||
| export { HeartbeatMessage, HeartbeatEvent } from './constants/heartbeat'; | ||
| export { EventEmitter, WebSocketManager, WebSocketClient } from './core'; |
@@ -0,1 +1,2 @@ | ||
| import { WebSocketEvent } from '../constants/events'; | ||
| export interface Serializer<T = any> { | ||
@@ -21,2 +22,45 @@ /** | ||
| }; | ||
| /** | ||
| * 消息 ACK 配置 | ||
| */ | ||
| export type AckStrategy = { | ||
| /** 是否开启 ACK 机制 (默认: true) */ | ||
| enabled?: boolean; | ||
| /** ACK 超时时间(ms) (默认: 5000) */ | ||
| timeout?: number; | ||
| /** 最大重试次数 (默认: 2) */ | ||
| maxRetries?: number; | ||
| /** | ||
| * 生成消息 ID(默认自增) | ||
| */ | ||
| generateId?: () => string | number; | ||
| /** | ||
| * 发出消息时的包装 | ||
| * @example 默认: (id, data) => ({ id, payload: data }) | ||
| */ | ||
| wrapOutbound?: (id: string | number, data: any) => any; | ||
| /** | ||
| * 从服务端消息中解析 ACK 所属的消息 ID | ||
| * 返回 null 表示不是 ACK 消息 | ||
| */ | ||
| extractAckId?: (message: any) => string | number | null; | ||
| }; | ||
| /** | ||
| * 消息序列号配置 | ||
| */ | ||
| export type SequenceStrategy = { | ||
| /** 是否开启本地序列号 (默认: true) */ | ||
| enabled?: boolean; | ||
| /** 生成序列号(默认自增) */ | ||
| generateSeq?: () => string | number; | ||
| /** | ||
| * 对外发数据加上 seq | ||
| * @example 默认: (seq, data) => ({ seq, payload: data }) | ||
| */ | ||
| wrapOutbound?: (seq: string | number, data: any) => any; | ||
| /** | ||
| * 解析入站消息的序列号 | ||
| */ | ||
| extractInboundSeq?: (message: any) => string | number | null; | ||
| }; | ||
| export interface WebSocketConfig { | ||
@@ -41,2 +85,6 @@ /** 最大重连尝试次数 (默认: 10) */ | ||
| serializer?: Serializer; | ||
| /** 消息 ACK 配置 */ | ||
| ack?: AckStrategy; | ||
| /** 消息序列号配置 */ | ||
| sequence?: SequenceStrategy; | ||
| /** 是否需要心跳 (默认: true) */ | ||
@@ -47,5 +95,11 @@ isNeedHeartbeat?: boolean; | ||
| } | ||
| export type WebSocketEvent = "open" | "message" | "close" | "error" | "reconnect" | "heartbeat" | "latency"; | ||
| export { WebSocketEvent } from '../constants/events'; | ||
| export interface IWebSocketClient { | ||
| send(data: any, priority?: number): Promise<void>; | ||
| /** | ||
| * 发送消息并等待 ACK | ||
| * - 使用全局或自定义的 ACK 配置 | ||
| * - 返回的 Promise 会在收到 ACK、超时或重试失败后结束 | ||
| */ | ||
| sendWithAck(data: any, priority?: number): Promise<void>; | ||
| close(code?: number, reason?: string): void; | ||
@@ -52,0 +106,0 @@ reconnect(): void; |
+239
-98
@@ -1,2 +0,2 @@ | ||
| const h = { | ||
| const _ = { | ||
| maxReconnectAttempts: 10, | ||
@@ -14,2 +14,30 @@ reconnectDelay: 1e3, | ||
| }, | ||
| ack: { | ||
| enabled: !0, | ||
| timeout: 5e3, | ||
| maxRetries: 2, | ||
| generateId: () => { | ||
| window.__ws_pro_client_ack_id__ || (window.__ws_pro_client_ack_id__ = 1); | ||
| const i = window.__ws_pro_client_ack_id__; | ||
| return window.__ws_pro_client_ack_id__ = i + 1, i; | ||
| }, | ||
| wrapOutbound: (i, e) => ({ | ||
| id: i, | ||
| payload: e | ||
| }), | ||
| extractAckId: (i) => i && typeof i == "object" && "ackId" in i ? i.ackId : null | ||
| }, | ||
| sequence: { | ||
| enabled: !0, | ||
| generateSeq: () => { | ||
| window.__ws_pro_client_seq__ || (window.__ws_pro_client_seq__ = 1); | ||
| const i = window.__ws_pro_client_seq__; | ||
| return window.__ws_pro_client_seq__ = i + 1, i; | ||
| }, | ||
| wrapOutbound: (i, e) => ({ | ||
| seq: i, | ||
| payload: e | ||
| }), | ||
| extractInboundSeq: (i) => i && typeof i == "object" && "seq" in i ? i.seq : null | ||
| }, | ||
| isNeedHeartbeat: !0, | ||
@@ -22,3 +50,22 @@ heartbeat: { | ||
| }; | ||
| class u { | ||
| var k = /* @__PURE__ */ ((i) => (i.Ping = "PING", i.Pong = "PONG", i))(k || {}), p = /* @__PURE__ */ ((i) => (i.Timeout = "timeout", i.Pong = "PONG", i))(p || {}); | ||
| class P { | ||
| constructor(e = "[WebSocketPro]") { | ||
| this.prefix = e; | ||
| } | ||
| debug(...e) { | ||
| } | ||
| info(...e) { | ||
| console.info(this.prefix, ...e); | ||
| } | ||
| warn(...e) { | ||
| console.warn(this.prefix, ...e); | ||
| } | ||
| error(...e) { | ||
| console.error(this.prefix, ...e); | ||
| } | ||
| } | ||
| let R = new P(); | ||
| const w = () => R; | ||
| class A { | ||
| constructor() { | ||
@@ -31,10 +78,10 @@ this.events = {}; | ||
| off(e, t) { | ||
| this.events[e] && (this.events[e] = this.events[e].filter((i) => i !== t)); | ||
| this.events[e] && (this.events[e] = this.events[e].filter((n) => n !== t)); | ||
| } | ||
| emit(e, ...t) { | ||
| this.events[e] && this.events[e].forEach((i) => { | ||
| this.events[e] && this.events[e].forEach((n) => { | ||
| try { | ||
| i(...t); | ||
| n(...t); | ||
| } catch (s) { | ||
| console.error(`Event "${e}" listener error:`, s); | ||
| w().error(`Event "${e}" listener error:`, s); | ||
| } | ||
@@ -44,6 +91,6 @@ }); | ||
| once(e, t) { | ||
| const i = (...s) => { | ||
| this.off(e, i), t(...s); | ||
| const n = (...s) => { | ||
| this.off(e, n), t(...s); | ||
| }; | ||
| this.on(e, i); | ||
| this.on(e, n); | ||
| } | ||
@@ -54,3 +101,3 @@ removeAllListeners(e) { | ||
| } | ||
| class f extends u { | ||
| class E extends A { | ||
| constructor(e = {}, t) { | ||
@@ -60,3 +107,3 @@ 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.config || w().warn("Heartbeat config is empty"), this.stop(), this.lastPongTime = Date.now(), this.intervalId = setInterval(() => { | ||
| this.sendPing(), this.timeoutId = setTimeout(() => { | ||
@@ -71,6 +118,6 @@ this.handleDefaultTimeout(this.config.onTimeout); | ||
| handleDefaultTimeout(e) { | ||
| this.stop(), e && e(), this.emit("timeout"); | ||
| this.stop(), e && e(), this.emit(p.Timeout); | ||
| } | ||
| recordPong() { | ||
| this.lastPongTime = Date.now(), clearTimeout(this.timeoutId), this.emit("pong", Date.now() - this.lastPongTime); | ||
| this.lastPongTime = Date.now(), clearTimeout(this.timeoutId), this.emit(p.Pong, Date.now() - this.lastPongTime); | ||
| } | ||
@@ -88,3 +135,3 @@ getLastPongTime() { | ||
| } | ||
| class g { | ||
| class O { | ||
| constructor(e, t) { | ||
@@ -94,12 +141,12 @@ this.maxConcurrent = e, this.onTaskError = t, this.queue = [], this.runningCount = 0; | ||
| add(e, t) { | ||
| return new Promise((i, s) => { | ||
| const r = async () => { | ||
| var o; | ||
| return new Promise((n, s) => { | ||
| const o = async () => { | ||
| var r; | ||
| try { | ||
| await e(), i(); | ||
| await e(), n(); | ||
| } catch (a) { | ||
| (o = this.onTaskError) == null || o.call(this, a), s(a); | ||
| (r = this.onTaskError) == null || r.call(this, a), s(a); | ||
| } | ||
| }; | ||
| this.queue.push({ task: r, priority: t }), this.queue.sort((o, a) => a.priority - o.priority), this.run(); | ||
| this.queue.push({ task: o, priority: t }), this.queue.sort((r, a) => a.priority - r.priority), this.run(); | ||
| }); | ||
@@ -122,35 +169,56 @@ } | ||
| } | ||
| const c = (n, e) => { | ||
| const t = { ...n }; | ||
| for (const i in e) | ||
| e[i] instanceof Object && !Array.isArray(e[i]) ? t[i] = c(n[i] || {}, e[i]) : t[i] = e[i]; | ||
| var f = /* @__PURE__ */ ((i) => (i.Open = "open", i.Message = "message", i.Close = "close", i.Error = "error", i.Reconnect = "reconnect", i.Heartbeat = "heartbeat", i.Latency = "latency", i.OverMaxReconnectAttempts = "overMaxReconnectAttempts", i))(f || {}); | ||
| const S = [ | ||
| "open", | ||
| "message", | ||
| "close", | ||
| "error" | ||
| /* Error */ | ||
| ], y = (i, e) => { | ||
| const t = { ...i }; | ||
| for (const n in e) { | ||
| const s = e[n]; | ||
| s && typeof s == "object" && !Array.isArray(s) ? t[n] = y(i[n] || {}, s) : t[n] = s; | ||
| } | ||
| return t; | ||
| }, l = (n, e) => { | ||
| if (n === e) | ||
| }, b = (i, e) => { | ||
| if (i === 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) | ||
| if (i == null || e == null || typeof i != "object" || typeof e != "object") | ||
| return i === e; | ||
| if (Array.isArray(i) && Array.isArray(e)) { | ||
| if (i.length !== e.length) | ||
| return !1; | ||
| for (let s = 0; s < n.length; s++) | ||
| if (!l(n[s], e[s])) | ||
| for (let s = 0; s < i.length; s++) | ||
| if (!b(i[s], e[s])) | ||
| return !1; | ||
| return !0; | ||
| } | ||
| if (Array.isArray(n) || Array.isArray(e)) | ||
| if (Array.isArray(i) || Array.isArray(e)) | ||
| return !1; | ||
| const t = Object.keys(n), i = Object.keys(e); | ||
| if (t.length !== i.length) | ||
| const t = Object.keys(i), n = Object.keys(e); | ||
| if (t.length !== n.length) | ||
| return !1; | ||
| for (const s of t) | ||
| if (!e.hasOwnProperty(s) || !l(n[s], e[s])) | ||
| if (!e.hasOwnProperty(s) || !b(i[s], e[s])) | ||
| return !1; | ||
| return !0; | ||
| }; | ||
| class m extends u { | ||
| 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 = c(h, this.config), this.initHeartbeat(), this.scheduler = new g( | ||
| var d = /* @__PURE__ */ ((i) => (i.MsgPackNotInstalled = "MSG_PACK_NOT_INSTALLED", i.AckTimeout = "ACK_TIMEOUT", i.AckMaxRetries = "ACK_MAX_RETRIES", i.ClosedBeforeAck = "CLOSED_BEFORE_ACK", i))(d || {}); | ||
| const M = { | ||
| MSG_PACK_NOT_INSTALLED: "MsgPack serializer requires @msgpack/msgpack installation", | ||
| ACK_TIMEOUT: "ACK timeout", | ||
| ACK_MAX_RETRIES: "ACK timeout, maximum retry attempts reached", | ||
| CLOSED_BEFORE_ACK: "WebSocket connection closed before ACK was received" | ||
| }; | ||
| class m extends Error { | ||
| constructor(e) { | ||
| super(M[e]), this.code = e, this.name = "WebSocketClientError"; | ||
| } | ||
| } | ||
| class N extends A { | ||
| constructor(e, t, n) { | ||
| super(), this.url = e, this.protocols = t, this.config = n, this.socket = null, this.reconnectAttempts = 0, this.messageQueue = [], this.pendingAcks = /* @__PURE__ */ new Map(), this.isUpdatingConfig = !1, this.configQueue = [], this.currentConfig = y(_, this.config), this.initHeartbeat(), this.scheduler = new O( | ||
| this.currentConfig.maxConcurrent, | ||
| (s) => this.emit("error", s) | ||
| (s) => this.emit(f.Error, s) | ||
| ), this.connect(); | ||
@@ -160,8 +228,8 @@ } | ||
| initHeartbeat() { | ||
| this.currentConfig.isNeedHeartbeat && (this.heartbeat = new f(this.currentConfig.heartbeat, () => { | ||
| this.currentConfig.isNeedHeartbeat && (this.heartbeat = new E(this.currentConfig.heartbeat, () => { | ||
| var e; | ||
| this.send(((e = this.currentConfig.heartbeat) == null ? void 0 : e.message) || "PING"); | ||
| }), this.heartbeat.on("timeout", () => { | ||
| }), this.heartbeat.on(p.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(); | ||
| w().warn("Heartbeat timeout, triggering reconnect..."), ((e = this.socket) == null ? void 0 : e.readyState) === WebSocket.OPEN && this.close(1e3, "heartbeat timeout"), this.scheduleReconnect(); | ||
| })); | ||
@@ -171,13 +239,36 @@ } | ||
| 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.reconnectAttempts = 0, clearTimeout(this.reconnectTimer), this.heartbeat && this.heartbeat.start(), this.flushMessageQueue(), this.emit(f.Open, e); | ||
| }, this.socket.onmessage = (e) => { | ||
| if (e.data === "pong") { | ||
| if (e.data === k.Pong) { | ||
| this.heartbeat && this.heartbeat.recordPong(); | ||
| return; | ||
| } | ||
| this.emit("message", e.data); | ||
| const t = e.data; | ||
| let n = t; | ||
| try { | ||
| n = this.currentConfig.serializer.deserialize(t); | ||
| } catch { | ||
| n = t; | ||
| } | ||
| const s = this.currentConfig.ack; | ||
| if (s != null && s.enabled && typeof s.extractAckId == "function") { | ||
| const r = s.extractAckId(n); | ||
| if (r != null) { | ||
| const a = this.pendingAcks.get(r); | ||
| if (a) { | ||
| clearTimeout(a.timer), this.pendingAcks.delete(r), a.resolve(); | ||
| return; | ||
| } | ||
| } | ||
| } | ||
| const o = this.currentConfig.sequence; | ||
| if (o != null && o.enabled && typeof o.extractInboundSeq == "function") { | ||
| const r = o.extractInboundSeq(n); | ||
| r != null && (this.lastInboundSeq = r); | ||
| } | ||
| this.emit(f.Message, n); | ||
| }, this.socket.onclose = (e) => { | ||
| this.heartbeat && this.heartbeat.stop(), this.emit("close", e); | ||
| this.heartbeat && this.heartbeat.stop(), this.emit(f.Close, e); | ||
| }, this.socket.onerror = (e) => { | ||
| this.heartbeat && this.heartbeat.stop(), this.emit("error", e), this.scheduleReconnect(); | ||
| this.heartbeat && this.heartbeat.stop(), this.emit(f.Error, e), this.scheduleReconnect(); | ||
| }; | ||
@@ -191,3 +282,3 @@ } | ||
| if (this.reconnectAttempts >= this.currentConfig.maxReconnectAttempts) { | ||
| this.emit("overMaxReconnectAttempts"); | ||
| this.emit(f.OverMaxReconnectAttempts); | ||
| return; | ||
@@ -198,3 +289,3 @@ } | ||
| this.currentConfig.maxReconnectDelay | ||
| ), i = e * 0.2 * (Math.random() * 2 - 1), s = Math.max(1e3, e + i); | ||
| ), n = e * 0.2 * (Math.random() * 2 - 1), s = Math.max(1e3, e + n); | ||
| this.reconnectTimer = setTimeout(() => { | ||
@@ -206,21 +297,71 @@ this.reconnectAttempts++, this.connect(); | ||
| for (; this.messageQueue.length > 0; ) { | ||
| const { data: e, resolve: t, reject: i } = this.messageQueue.shift(); | ||
| this.send(e).then(t).catch(i); | ||
| const { data: e, priority: t, needAck: n, resolve: s, reject: o } = this.messageQueue.shift(); | ||
| this.sendInternal(e, t, n).then(s).catch(o); | ||
| } | ||
| } | ||
| 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, r) => { | ||
| sendInternal(e, t, n) { | ||
| var s; | ||
| return ((s = this.socket) == null ? void 0 : s.readyState) === WebSocket.OPEN ? this.scheduler.add(() => new Promise((o, r) => { | ||
| var a, C; | ||
| try { | ||
| this.sendRaw(e), s(); | ||
| } catch (o) { | ||
| r(o); | ||
| let l = e; | ||
| const u = this.currentConfig.sequence; | ||
| if (u != null && u.enabled && typeof u.wrapOutbound == "function") { | ||
| const h = (a = u.generateSeq) == null ? void 0 : a.call(u); | ||
| h !== void 0 && (l = u.wrapOutbound(h, l)); | ||
| } | ||
| const c = this.currentConfig.ack; | ||
| let g = null; | ||
| if (n && (c != null && c.enabled)) { | ||
| const h = (C = c.generateId) == null ? void 0 : C.call(c); | ||
| h != null && typeof c.wrapOutbound == "function" && (g = h, l = c.wrapOutbound(h, l)); | ||
| } | ||
| const T = this.currentConfig.serializer.serialize(l); | ||
| if (this.sendRaw(T), !n || !(c != null && c.enabled) || g === null) { | ||
| o(); | ||
| return; | ||
| } | ||
| const x = c.timeout ?? 5e3, q = c.maxRetries ?? 0, I = { | ||
| resolve: o, | ||
| reject: (h) => r(h), | ||
| retries: 0, | ||
| rawData: e, | ||
| priority: t, | ||
| timer: setTimeout(() => { | ||
| this.handleAckTimeout(g); | ||
| }, x) | ||
| }; | ||
| this.pendingAcks.set(g, I); | ||
| } catch (l) { | ||
| r(l); | ||
| } | ||
| }), t) : new Promise((s, r) => { | ||
| this.messageQueue.push({ data: e, priority: t, resolve: s, reject: r }); | ||
| }), t) : new Promise((o, r) => { | ||
| this.messageQueue.push({ data: e, priority: t, needAck: n, resolve: o, reject: r }); | ||
| }); | ||
| } | ||
| handleAckTimeout(e) { | ||
| const t = this.currentConfig.ack, n = this.pendingAcks.get(e); | ||
| if (!n || !(t != null && t.enabled)) { | ||
| n && (this.pendingAcks.delete(e), n.reject(new m(d.AckTimeout))); | ||
| return; | ||
| } | ||
| const s = t.timeout ?? 5e3, o = t.maxRetries ?? 0; | ||
| n.retries < o ? (n.retries += 1, this.sendInternal(n.rawData, n.priority, !1).catch( | ||
| n.reject | ||
| ), n.timer = setTimeout(() => { | ||
| this.handleAckTimeout(e); | ||
| }, s)) : (this.pendingAcks.delete(e), n.reject(new m(d.AckMaxRetries))); | ||
| } | ||
| send(e, t = this.currentConfig.defaultPriority) { | ||
| return this.sendInternal(e, t, !1); | ||
| } | ||
| // 发送并等待 ACK | ||
| sendWithAck(e, t = this.currentConfig.defaultPriority) { | ||
| return this.sendInternal(e, t, !0); | ||
| } | ||
| close(e, t) { | ||
| var i; | ||
| clearTimeout(this.reconnectTimer), this.heartbeat && this.heartbeat.stop(), (i = this.socket) == null || i.close(e, t), this.socket = null; | ||
| var n; | ||
| clearTimeout(this.reconnectTimer), this.heartbeat && this.heartbeat.stop(), this.pendingAcks.forEach((s, o) => { | ||
| clearTimeout(s.timer), s.reject(new m(d.ClosedBeforeAck)), this.pendingAcks.delete(o); | ||
| }), (n = this.socket) == null || n.close(e, t), this.socket = null; | ||
| } | ||
@@ -242,7 +383,7 @@ reconnect() { | ||
| const t = { ...this.currentConfig }; | ||
| this.currentConfig = c(this.currentConfig, e), this.handleConfigChange(t, this.currentConfig), this.applyConfig(); | ||
| this.currentConfig = y(this.currentConfig, e), this.handleConfigChange(t, this.currentConfig), this.applyConfig(); | ||
| } | ||
| // 处理特定配置变更 | ||
| handleConfigChange(e, t) { | ||
| l(e.heartbeat, t.heartbeat) || this.reInitHeartbeat(), (e.maxReconnectAttempts !== t.maxReconnectAttempts || e.reconnectDelay !== t.reconnectDelay || e.reconnectExponent !== t.reconnectExponent || e.maxReconnectDelay !== t.maxReconnectDelay) && this.resetReconnectTimer(); | ||
| b(e.heartbeat, t.heartbeat) || this.reInitHeartbeat(), (e.maxReconnectAttempts !== t.maxReconnectAttempts || e.reconnectDelay !== t.reconnectDelay || e.reconnectExponent !== t.reconnectExponent || e.maxReconnectDelay !== t.maxReconnectDelay) && this.resetReconnectTimer(); | ||
| } | ||
@@ -266,3 +407,3 @@ // 应用新配置到各模块 | ||
| } | ||
| class d extends u { | ||
| class D extends A { | ||
| constructor(e) { | ||
@@ -272,52 +413,52 @@ super(), this.config = e, this.clients = /* @__PURE__ */ new Map(); | ||
| connect(e, t = []) { | ||
| const i = `${e}|${t.join(",")}`; | ||
| if (this.clients.has(i)) | ||
| return this.clients.get(i); | ||
| const s = new m(e, t, this.config); | ||
| this.clients.set(i, s); | ||
| const r = (o) => (a) => { | ||
| this.emit(o, { url: e, protocols: t, data: a }); | ||
| const n = `${e}|${t.join(",")}`; | ||
| if (this.clients.has(n)) | ||
| return this.clients.get(n); | ||
| const s = new N(e, t, this.config); | ||
| this.clients.set(n, s); | ||
| const o = (r) => (a) => { | ||
| this.emit(r, { url: e, protocols: t, data: a }); | ||
| }; | ||
| return s.on("open", r("open")), s.on("message", r("message")), s.on("close", r("close")), s.on("error", r("error")), s; | ||
| return S.forEach((r) => { | ||
| s.on(r, o(r)); | ||
| }), s; | ||
| } | ||
| closeAll(e, t) { | ||
| this.clients.forEach((i) => i.close(e, t)), this.clients.clear(); | ||
| this.clients.forEach((n) => n.close(e, t)), this.clients.clear(); | ||
| } | ||
| getClient(e, t) { | ||
| const i = `${e}|${(t == null ? void 0 : t.join(",")) || ""}`; | ||
| return this.clients.get(i); | ||
| const n = `${e}|${(t == null ? void 0 : t.join(",")) || ""}`; | ||
| return this.clients.get(n); | ||
| } | ||
| } | ||
| const p = (n = {}) => { | ||
| const z = (i = {}) => { | ||
| const e = { | ||
| ...h, | ||
| ...n, | ||
| ..._, | ||
| ...i, | ||
| serializer: { | ||
| ...h.serializer, | ||
| ...n.serializer | ||
| ..._.serializer, | ||
| ...i.serializer | ||
| } | ||
| }; | ||
| return new d(e); | ||
| }, y = { | ||
| return new D(e); | ||
| }, j = { | ||
| serialize: JSON.stringify, | ||
| deserialize: JSON.parse | ||
| }, b = { | ||
| serialize: (n) => { | ||
| throw new Error( | ||
| "MsgPack serializer requires @msgpack/msgpack installation" | ||
| ); | ||
| }, v = { | ||
| serialize: (i) => { | ||
| throw new m(d.MsgPackNotInstalled); | ||
| }, | ||
| deserialize: (n) => { | ||
| throw new Error( | ||
| "MsgPack serializer requires @msgpack/msgpack installation" | ||
| ); | ||
| deserialize: (i) => { | ||
| throw new m(d.MsgPackNotInstalled); | ||
| } | ||
| }; | ||
| export { | ||
| u as EventEmitter, | ||
| y as JsonSerializer, | ||
| b as MsgPackSerializer, | ||
| m as WebSocketClient, | ||
| d as WebSocketManager, | ||
| p as createWebSocketManager | ||
| A as EventEmitter, | ||
| p as HeartbeatEvent, | ||
| k as HeartbeatMessage, | ||
| j as JsonSerializer, | ||
| v as MsgPackSerializer, | ||
| N as WebSocketClient, | ||
| D as WebSocketManager, | ||
| z as createWebSocketManager | ||
| }; |
@@ -1,1 +0,1 @@ | ||
| (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"})}); | ||
| (function(a,f){typeof exports=="object"&&typeof module<"u"?f(exports):typeof define=="function"&&define.amd?define(["exports"],f):(a=typeof globalThis<"u"?globalThis:a||self,f(a.WebSocketPro={}))})(this,function(a){"use strict";var f=typeof document<"u"?document.currentScript:null;const w={maxReconnectAttempts:10,reconnectDelay:1e3,reconnectExponent:1.5,maxReconnectDelay:3e4,connectionPoolSize:5,maxConcurrent:1,defaultPriority:1,enableCompression:!1,serializer:{serialize:JSON.stringify,deserialize:JSON.parse},ack:{enabled:!0,timeout:5e3,maxRetries:2,generateId:()=>{window.__ws_pro_client_ack_id__||(window.__ws_pro_client_ack_id__=1);const n=window.__ws_pro_client_ack_id__;return window.__ws_pro_client_ack_id__=n+1,n},wrapOutbound:(n,e)=>({id:n,payload:e}),extractAckId:n=>n&&typeof n=="object"&&"ackId"in n?n.ackId:null},sequence:{enabled:!0,generateSeq:()=>{window.__ws_pro_client_seq__||(window.__ws_pro_client_seq__=1);const n=window.__ws_pro_client_seq__;return window.__ws_pro_client_seq__=n+1,n},wrapOutbound:(n,e)=>({seq:n,payload:e}),extractInboundSeq:n=>n&&typeof n=="object"&&"seq"in n?n.seq:null},isNeedHeartbeat:!0,heartbeat:{interval:25e3,timeout:1e4,message:"PING"}};var C=(n=>(n.Ping="PING",n.Pong="PONG",n))(C||{}),p=(n=>(n.Timeout="timeout",n.Pong="PONG",n))(p||{});class I{constructor(e="[WebSocketPro]"){this.prefix=e}debug(...e){typeof document>"u"&&typeof location>"u"?require("url").pathToFileURL(__filename).href:typeof document>"u"?location.href:f&&f.tagName.toUpperCase()==="SCRIPT"&&f.src||new URL("websocket-pro.umd.js",document.baseURI).href}info(...e){console.info(this.prefix,...e)}warn(...e){console.warn(this.prefix,...e)}error(...e){console.error(this.prefix,...e)}}let E=new I;const k=()=>E;class b{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){k().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 x extends b{constructor(e={},t){super(),this.config=e,this.sendPing=t,this.lastPongTime=0}start(){this.config||k().warn("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(p.Timeout)}recordPong(){this.lastPongTime=Date.now(),clearTimeout(this.timeoutId),this.emit(p.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 O{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 r;try{await e(),i()}catch(c){(r=this.onTaskError)==null||r.call(this,c),s(c)}};this.queue.push({task:o,priority:t}),this.queue.sort((r,c)=>c.priority-r.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()}}var d=(n=>(n.Open="open",n.Message="message",n.Close="close",n.Error="error",n.Reconnect="reconnect",n.Heartbeat="heartbeat",n.Latency="latency",n.OverMaxReconnectAttempts="overMaxReconnectAttempts",n))(d||{});const M=["open","message","close","error"],A=(n,e)=>{const t={...n};for(const i in e){const s=e[i];s&&typeof s=="object"&&!Array.isArray(s)?t[i]=A(n[i]||{},s):t[i]=s}return t},T=(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(!T(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)||!T(n[s],e[s]))return!1;return!0};var g=(n=>(n.MsgPackNotInstalled="MSG_PACK_NOT_INSTALLED",n.AckTimeout="ACK_TIMEOUT",n.AckMaxRetries="ACK_MAX_RETRIES",n.ClosedBeforeAck="CLOSED_BEFORE_ACK",n))(g||{});const N={MSG_PACK_NOT_INSTALLED:"MsgPack serializer requires @msgpack/msgpack installation",ACK_TIMEOUT:"ACK timeout",ACK_MAX_RETRIES:"ACK timeout, maximum retry attempts reached",CLOSED_BEFORE_ACK:"WebSocket connection closed before ACK was received"};class _ extends Error{constructor(e){super(N[e]),this.code=e,this.name="WebSocketClientError"}}class S extends b{constructor(e,t,i){super(),this.url=e,this.protocols=t,this.config=i,this.socket=null,this.reconnectAttempts=0,this.messageQueue=[],this.pendingAcks=new Map,this.isUpdatingConfig=!1,this.configQueue=[],this.currentConfig=A(w,this.config),this.initHeartbeat(),this.scheduler=new O(this.currentConfig.maxConcurrent,s=>this.emit(d.Error,s)),this.connect()}initHeartbeat(){this.currentConfig.isNeedHeartbeat&&(this.heartbeat=new x(this.currentConfig.heartbeat,()=>{var e;this.send(((e=this.currentConfig.heartbeat)==null?void 0:e.message)||"PING")}),this.heartbeat.on(p.Timeout,()=>{var e;k().warn("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(d.Open,e)},this.socket.onmessage=e=>{if(e.data===C.Pong){this.heartbeat&&this.heartbeat.recordPong();return}const t=e.data;let i=t;try{i=this.currentConfig.serializer.deserialize(t)}catch{i=t}const s=this.currentConfig.ack;if(s!=null&&s.enabled&&typeof s.extractAckId=="function"){const r=s.extractAckId(i);if(r!=null){const c=this.pendingAcks.get(r);if(c){clearTimeout(c.timer),this.pendingAcks.delete(r),c.resolve();return}}}const o=this.currentConfig.sequence;if(o!=null&&o.enabled&&typeof o.extractInboundSeq=="function"){const r=o.extractInboundSeq(i);r!=null&&(this.lastInboundSeq=r)}this.emit(d.Message,i)},this.socket.onclose=e=>{this.heartbeat&&this.heartbeat.stop(),this.emit(d.Close,e)},this.socket.onerror=e=>{this.heartbeat&&this.heartbeat.stop(),this.emit(d.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(d.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,priority:t,needAck:i,resolve:s,reject:o}=this.messageQueue.shift();this.sendInternal(e,t,i).then(s).catch(o)}}sendInternal(e,t,i){var s;return((s=this.socket)==null?void 0:s.readyState)===WebSocket.OPEN?this.scheduler.add(()=>new Promise((o,r)=>{var c,R;try{let m=e;const l=this.currentConfig.sequence;if(l!=null&&l.enabled&&typeof l.wrapOutbound=="function"){const u=(c=l.generateSeq)==null?void 0:c.call(l);u!==void 0&&(m=l.wrapOutbound(u,m))}const h=this.currentConfig.ack;let y=null;if(i&&(h!=null&&h.enabled)){const u=(R=h.generateId)==null?void 0:R.call(h);u!=null&&typeof h.wrapOutbound=="function"&&(y=u,m=h.wrapOutbound(u,m))}const z=this.currentConfig.serializer.serialize(m);if(this.sendRaw(z),!i||!(h!=null&&h.enabled)||y===null){o();return}const v=h.timeout??5e3,K=h.maxRetries??0,L={resolve:o,reject:u=>r(u),retries:0,rawData:e,priority:t,timer:setTimeout(()=>{this.handleAckTimeout(y)},v)};this.pendingAcks.set(y,L)}catch(m){r(m)}}),t):new Promise((o,r)=>{this.messageQueue.push({data:e,priority:t,needAck:i,resolve:o,reject:r})})}handleAckTimeout(e){const t=this.currentConfig.ack,i=this.pendingAcks.get(e);if(!i||!(t!=null&&t.enabled)){i&&(this.pendingAcks.delete(e),i.reject(new _(g.AckTimeout)));return}const s=t.timeout??5e3,o=t.maxRetries??0;i.retries<o?(i.retries+=1,this.sendInternal(i.rawData,i.priority,!1).catch(i.reject),i.timer=setTimeout(()=>{this.handleAckTimeout(e)},s)):(this.pendingAcks.delete(e),i.reject(new _(g.AckMaxRetries)))}send(e,t=this.currentConfig.defaultPriority){return this.sendInternal(e,t,!1)}sendWithAck(e,t=this.currentConfig.defaultPriority){return this.sendInternal(e,t,!0)}close(e,t){var i;clearTimeout(this.reconnectTimer),this.heartbeat&&this.heartbeat.stop(),this.pendingAcks.forEach((s,o)=>{clearTimeout(s.timer),s.reject(new _(g.ClosedBeforeAck)),this.pendingAcks.delete(o)}),(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=A(this.currentConfig,e),this.handleConfigChange(t,this.currentConfig),this.applyConfig()}handleConfigChange(e,t){T(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 P extends b{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 S(e,t,this.config);this.clients.set(i,s);const o=r=>c=>{this.emit(r,{url:e,protocols:t,data:c})};return M.forEach(r=>{s.on(r,o(r))}),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 D=(n={})=>{const e={...w,...n,serializer:{...w.serializer,...n.serializer}};return new P(e)},q={serialize:JSON.stringify,deserialize:JSON.parse},j={serialize:n=>{throw new _(g.MsgPackNotInstalled)},deserialize:n=>{throw new _(g.MsgPackNotInstalled)}};a.EventEmitter=b,a.HeartbeatEvent=p,a.HeartbeatMessage=C,a.JsonSerializer=q,a.MsgPackSerializer=j,a.WebSocketClient=S,a.WebSocketManager=P,a.createWebSocketManager=D,Object.defineProperty(a,Symbol.toStringTag,{value:"Module"})}); |
+2
-2
| { | ||
| "name": "websocket-pro-client", | ||
| "version": "1.0.2-beta.1", | ||
| "version": "1.0.2-beta.2", | ||
| "description": "High-performance WebSocket client with auto-reconnect, heartbeat and priority messaging", | ||
@@ -49,3 +49,3 @@ "keywords": [ | ||
| "vite": "^4.0.0", | ||
| "vite-plugin-dts": "^2.0.0", | ||
| "vite-plugin-dts": "^4.3.0", | ||
| "vitest": "^0.25.0" | ||
@@ -52,0 +52,0 @@ }, |
+284
-27
@@ -1,2 +0,2 @@ | ||
| # WebSocket Pro Client | ||
| ## WebSocket Pro Client | ||
@@ -6,24 +6,17 @@ [](https://www.npmjs.com/package/websocket-pro-client) | ||
| 高性能 WebSocket 客户端,专为现代 Web 应用设计。 | ||
| 高性能 WebSocket 客户端,专为现代 Web 应用设计,内置自动重连、心跳、消息优先级调度、连接池管理,以及**可配置的消息 ACK 与序列号机制**。 | ||
| ## 特性 | ||
| ### 特性一览 | ||
| ### v1.0.2-Beta | ||
| - 🚀 自动重连 + 指数退避算法 | ||
| - 💓 心跳检测(支持自定义心跳内容与超时处理) | ||
| - 🎯 消息优先级调度 & 最大并发控制 | ||
| - 📦 连接池管理(同一 url/protocol 只维护一个连接实例) | ||
| - 🔄 运行时更新配置(心跳、重连策略等) | ||
| - ✅ 内置消息 ACK 机制(默认实现 + 完全可自定义) | ||
| - 🔢 消息序列号支持(默认自增,可自定义包装与解析) | ||
| - 🔍 完整 TypeScript 类型定义 | ||
| - 支持自定义心跳消息 | ||
| - 支持动态更新部分 websocket 配置 | ||
| --- | ||
| 下个版本迭代内容: | ||
| 1. 配置校验以及支持回滚 | ||
| 2. 完善的配置动态更新支持 | ||
| ### v1.0.1 | ||
| - 🚀 自动重连 + 智能退避算法 | ||
| - 💓 可配置心跳检测 | ||
| - 🎯 消息优先级调度 | ||
| - 📦 多连接池管理 | ||
| - 🔍 完整的 TypeScript 支持 | ||
| ## 安装 | ||
@@ -37,20 +30,280 @@ | ||
| --- | ||
| ## 快速开始 | ||
| ```typescript | ||
| import { createWebSocketManager } from "websocket-pro-client"; | ||
| import { createWebSocketManager } from 'websocket-pro-client' | ||
| // 1. 创建全局 WebSocket 管理器(可配置重连、心跳、ACK 等) | ||
| const manager = createWebSocketManager({ | ||
| maxReconnectAttempts: 5, | ||
| }); | ||
| }) | ||
| const client = manager.connect("wss://api.example.com"); | ||
| // 2. 建立连接(同一 url + protocols 只会创建一个底层连接) | ||
| const client = manager.connect('wss://api.example.com') | ||
| client.on("message", (data) => { | ||
| console.log("Received:", data); | ||
| }); | ||
| // 3. 监听消息 | ||
| client.on('message', (data) => { | ||
| console.log('Received:', data) | ||
| }) | ||
| client.send("hello word"); | ||
| // 4. 发送消息(不关心 ACK) | ||
| client.send({ type: 'ping' }) | ||
| ``` | ||
| --- | ||
| ## API 说明 | ||
| ### 1. 顶层方法 | ||
| - **`createWebSocketManager(config?: Partial<WebSocketConfig>): IWebSocketManager`** | ||
| - 创建一个 WebSocket 连接管理器。 | ||
| - 内部会将传入的配置与库内的 `DEFAULT_CONFIG` 深度合并。 | ||
| - **`JsonSerializer / MsgPackSerializer`** | ||
| - `JsonSerializer`: 默认使用 `JSON.stringify/JSON.parse` 的序列化器。 | ||
| - `MsgPackSerializer`: MessagePack 示例(需要自行安装 `@msgpack/msgpack` 后替换实现)。 | ||
| --- | ||
| ### 2. WebSocketManager | ||
| `IWebSocketManager` 接口: | ||
| ```ts | ||
| export interface IWebSocketManager { | ||
| connect(url: string, protocols?: string[]): IWebSocketClient | ||
| closeAll(code?: number, reason?: string): void | ||
| on(event: WebSocketEvent, listener: (data: any) => void): void | ||
| } | ||
| ``` | ||
| - **`connect(url, protocols?)`** | ||
| - 返回一个 `IWebSocketClient` 实例。 | ||
| - 同一 `url + protocols` 会复用同一个底层连接。 | ||
| - **`closeAll(code?, reason?)`** | ||
| - 关闭当前 manager 管理的所有连接。 | ||
| - **`on(event, listener)`** | ||
| - 监听所有客户端转发上来的事件(`open/message/close/error` 等),回调中会携带 `{ url, protocols, data }`。 | ||
| 示例: | ||
| ```ts | ||
| import { WebSocketEvent } from 'websocket-pro-client' | ||
| manager.on(WebSocketEvent.Error, ({ url, data }) => { | ||
| console.error('ws error:', url, data) | ||
| }) | ||
| ``` | ||
| --- | ||
| ### 3. WebSocketClient | ||
| `IWebSocketClient` 接口: | ||
| ```ts | ||
| export interface IWebSocketClient { | ||
| send(data: any, priority?: number): Promise<void> | ||
| sendWithAck(data: any, priority?: number): Promise<void> | ||
| close(code?: number, reason?: string): void | ||
| reconnect(): void | ||
| on(event: WebSocketEvent, listener: (data: any) => void): void | ||
| off(event: WebSocketEvent, listener: (data: any) => void): void | ||
| } | ||
| ``` | ||
| - **`send(data, priority?)`** | ||
| - 发送一条消息,不等待 ACK。 | ||
| - 返回的 Promise 仅表示“客户端已成功发送到 WebSocket”,**不代表服务端处理成功**。 | ||
| - **`sendWithAck(data, priority?)`** | ||
| - 发送一条消息,并基于全局 `ack` 配置等待服务端 ACK。 | ||
| - 默认行为: | ||
| - 发送的数据会被包装成:`{ id, payload: { seq, payload: 原始data } }` | ||
| - 要求服务端在处理完成后,发送一条包含 `ackId` 字段的消息(例如 `{ ackId: id }`),表示已确认。 | ||
| - 支持超时与自动重试(由 `ack.timeout` 和 `ack.maxRetries` 控制)。 | ||
| - **`close(code?, reason?)`** | ||
| - 主动关闭当前连接,并清理所有等待中的 ACK。 | ||
| - **`reconnect()`** | ||
| - 立即重连一次(会重置重连计数和退避延迟)。 | ||
| - **事件监听** | ||
| ```ts | ||
| import { WebSocketEvent } from 'websocket-pro-client' | ||
| client.on(WebSocketEvent.Open, () => { | ||
| console.log('ws open') | ||
| }) | ||
| client.on(WebSocketEvent.Message, (data) => { | ||
| console.log('message:', data) | ||
| }) | ||
| client.on(WebSocketEvent.Close, (event) => { | ||
| console.log('closed:', event) | ||
| }) | ||
| client.on(WebSocketEvent.Error, (err) => { | ||
| console.error('ws error:', err) | ||
| }) | ||
| ``` | ||
| --- | ||
| ## 配置说明(WebSocketConfig) | ||
| ```ts | ||
| export interface WebSocketConfig { | ||
| // 重连相关 | ||
| maxReconnectAttempts?: number | ||
| reconnectDelay?: number | ||
| reconnectExponent?: number | ||
| maxReconnectDelay?: number | ||
| // 任务调度 & 连接池 | ||
| connectionPoolSize?: number | ||
| maxConcurrent?: number | ||
| defaultPriority?: number | ||
| // 序列化 | ||
| enableCompression?: boolean | ||
| serializer?: Serializer | ||
| // 心跳 | ||
| isNeedHeartbeat?: boolean | ||
| heartbeat?: HeartbeatConfig | ||
| // 消息 ACK | ||
| ack?: AckStrategy | ||
| // 消息序列号 | ||
| sequence?: SequenceStrategy | ||
| } | ||
| ``` | ||
| ### 1. 心跳配置 HeartbeatConfig | ||
| ```ts | ||
| export type HeartbeatConfig = { | ||
| interval?: number // 心跳间隔(ms),默认 25000 | ||
| timeout?: number // 心跳超时(ms),默认 10000 | ||
| message?: string // 心跳消息内容,默认 "PING" | ||
| onTimeout?: () => void // 心跳超时时的自定义回调 | ||
| } | ||
| ``` | ||
| 默认情况下,客户端会周期性发送心跳消息,并在超时时自动触发重连。 | ||
| ### 2. 消息 ACK 配置 AckStrategy | ||
| ```ts | ||
| export type AckStrategy = { | ||
| enabled?: boolean | ||
| timeout?: number | ||
| maxRetries?: number | ||
| generateId?: () => string | number | ||
| wrapOutbound?: (id: string | number, data: any) => any | ||
| extractAckId?: (message: any) => string | number | null | ||
| } | ||
| ``` | ||
| 默认实现(不配置时): | ||
| - `enabled: true` | ||
| - `timeout: 5000` | ||
| - `maxRetries: 2` | ||
| - `generateId`: 使用浏览器 `window` 上的自增计数。 | ||
| - `wrapOutbound`: `({ id, payload })` | ||
| - `extractAckId`: 从 `message.ackId` 中提取 ACK 对应的 ID。 | ||
| > 如需和现有服务端协议对齐,只需要重写 `wrapOutbound` 和 `extractAckId` 即可。 | ||
| #### 自定义 ACK 协议示例 | ||
| 后端规定: | ||
| - 出站:`{ msgId, body }` | ||
| - ACK 消息:`{ type: 'ACK', msgId }` | ||
| 对应配置: | ||
| ```ts | ||
| const manager = createWebSocketManager({ | ||
| ack: { | ||
| enabled: true, | ||
| wrapOutbound: (id, data) => ({ msgId: id, body: data }), | ||
| extractAckId: (msg) => | ||
| msg && msg.type === 'ACK' && msg.msgId != null ? msg.msgId : null, | ||
| }, | ||
| }) | ||
| ``` | ||
| ### 3. 消息序列号配置 SequenceStrategy | ||
| ```ts | ||
| export type SequenceStrategy = { | ||
| enabled?: boolean | ||
| generateSeq?: () => string | number | ||
| wrapOutbound?: (seq: string | number, data: any) => any | ||
| extractInboundSeq?: (message: any) => string | number | null | ||
| } | ||
| ``` | ||
| 默认实现: | ||
| - `enabled: true` | ||
| - `generateSeq`: 使用浏览器 `window` 上的自增计数。 | ||
| - `wrapOutbound`: `({ seq, payload })` | ||
| - `extractInboundSeq`: 从 `message.seq` 中提取序列号。 | ||
| > 库内部只负责“生成/包装/解析”序列号,不做强制的乱序丢弃;你可以在 `message` 监听回调中结合 `seq` 做业务上的顺序控制。 | ||
| --- | ||
| ## 使用 ACK 与序列号的完整示例 | ||
| ```ts | ||
| import { | ||
| createWebSocketManager, | ||
| WebSocketEvent, | ||
| } from 'websocket-pro-client' | ||
| const manager = createWebSocketManager({ | ||
| ack: { | ||
| enabled: true, | ||
| timeout: 3000, | ||
| maxRetries: 1, | ||
| }, | ||
| }) | ||
| const client = manager.connect('wss://api.example.com') | ||
| client.on(WebSocketEvent.Open, async () => { | ||
| // 发送一条不需要 ACK 的消息 | ||
| await client.send({ type: 'ping' }) | ||
| // 发送一条需要 ACK 的消息 | ||
| try { | ||
| await client.sendWithAck({ type: 'update', payload: { id: 1 } }) | ||
| console.log('update confirmed by server') | ||
| } catch (e) { | ||
| console.error('update failed (no ACK):', e) | ||
| } | ||
| }) | ||
| client.on(WebSocketEvent.Message, (msg) => { | ||
| // msg 是反序列化后的对象,例如: | ||
| // { seq, payload: { ... } } 或根据你自定义的包装形态 | ||
| console.log('inbound message:', msg) | ||
| }) | ||
| ``` | ||
| --- | ||
| ## 开发调试 | ||
@@ -65,6 +318,8 @@ | ||
| # 运行demo | ||
| # 运行 demo | ||
| npm run demo | ||
| ``` | ||
| --- | ||
| ## 贡献指南 | ||
@@ -78,4 +333,6 @@ | ||
| --- | ||
| ## 许可证 | ||
| MIT © 2023 BetaCatPro |
46964
66.7%19
26.67%778
49.62%335
329.49%