@brewer/dj-common 1.0.0-beta.10 → 1.0.0-beta.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -4
- package/dist/MessageSocket.cjs.js +1 -1
- package/dist/MessageSocket.cjs.js.map +1 -1
- package/dist/MessageSocket.d.ts +25 -12
- package/dist/MessageSocket.d.ts.map +1 -1
- package/dist/MessageSocket.esm.js +1 -1
- package/dist/MessageSocket.esm.js.map +1 -1
- package/dist/WebSocketClient.cjs.js +1 -1
- package/dist/WebSocketClient.cjs.js.map +1 -1
- package/dist/WebSocketClient.d.ts +3 -12
- package/dist/WebSocketClient.d.ts.map +1 -1
- package/dist/WebSocketClient.esm.js +1 -1
- package/dist/WebSocketClient.esm.js.map +1 -1
- package/dist/index.cjs.js +1 -1
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/logger.cjs.js +2 -0
- package/dist/logger.cjs.js.map +1 -0
- package/dist/logger.d.ts +16 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.esm.js +2 -0
- package/dist/logger.esm.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
- 🔧 **可配置** - 所有参数都可灵活配置
|
|
11
11
|
- 🔄 **自动重连** - 内置智能重连机制
|
|
12
12
|
- 💓 **心跳检测** - 自动维持连接活性
|
|
13
|
+
- 📝 **日志系统** - 内置可配置的日志系统,支持多级别控制
|
|
13
14
|
- 🎯 **类型安全** - 完整的 TypeScript 类型支持
|
|
14
15
|
|
|
15
16
|
## 安装
|
|
@@ -32,6 +33,9 @@ import { WebSocketClient } from '@brewer/dj-common/WebSocketClient'
|
|
|
32
33
|
|
|
33
34
|
// 只引入 MessageSocket
|
|
34
35
|
import { MessageSocket } from '@brewer/dj-common/MessageSocket'
|
|
36
|
+
|
|
37
|
+
// 只引入 Logger
|
|
38
|
+
import { Logger } from '@brewer/dj-common/logger'
|
|
35
39
|
```
|
|
36
40
|
|
|
37
41
|
## TypeScript 支持
|
|
@@ -46,6 +50,7 @@ import type {
|
|
|
46
50
|
MessageCallbackEntry,
|
|
47
51
|
MessageSocketConfig,
|
|
48
52
|
MessageSocketStartOptions,
|
|
53
|
+
LogLevel,
|
|
49
54
|
} from '@brewer/dj-common'
|
|
50
55
|
```
|
|
51
56
|
|
|
@@ -108,20 +113,111 @@ npm publish
|
|
|
108
113
|
│ ├── 心跳检测
|
|
109
114
|
│ ├── 自动重连
|
|
110
115
|
│ ├── 消息回调
|
|
116
|
+
│ ├── 日志系统
|
|
111
117
|
│ └── 生命周期钩子
|
|
112
118
|
│
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
119
|
+
├── MessageSocket (业务类)
|
|
120
|
+
│ ├── 继承 WebSocketClient
|
|
121
|
+
│ ├── 用户认证
|
|
122
|
+
│ └── 消息管理
|
|
123
|
+
│
|
|
124
|
+
└── Logger (日志类)
|
|
125
|
+
├── 多级别日志(debug/info/warn/error/silent)
|
|
126
|
+
├── 可配置日志级别
|
|
127
|
+
└── 带名称前缀
|
|
117
128
|
```
|
|
118
129
|
|
|
119
130
|
**设计理念:**
|
|
120
131
|
|
|
121
132
|
- `WebSocketClient` 是通用的 WebSocket 基础封装,不依赖具体业务
|
|
122
133
|
- `MessageSocket` 基于 `WebSocketClient`,添加用户认证等业务功能
|
|
134
|
+
- `Logger` 提供统一的日志管理,支持多级别控制
|
|
123
135
|
- 职责分离,易于扩展和维护
|
|
124
136
|
|
|
137
|
+
## 日志系统
|
|
138
|
+
|
|
139
|
+
库内置了日志系统,支持 5 种日志级别:
|
|
140
|
+
|
|
141
|
+
- `debug` - 调试信息(最详细)
|
|
142
|
+
- `info` - 一般信息
|
|
143
|
+
- `warn` - 警告信息(默认级别)
|
|
144
|
+
- `error` - 错误信息
|
|
145
|
+
- `silent` - 静默模式(不输出任何日志)
|
|
146
|
+
|
|
147
|
+
### 配置日志级别
|
|
148
|
+
|
|
149
|
+
**WebSocketClient:**
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
import { WebSocketClient } from '@brewer/dj-common'
|
|
153
|
+
|
|
154
|
+
const client = new WebSocketClient({
|
|
155
|
+
url: 'ws://localhost:8080',
|
|
156
|
+
logLevel: 'debug', // 设置日志级别
|
|
157
|
+
})
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**MessageSocket:**
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
import { MessageSocket } from '@brewer/dj-common'
|
|
164
|
+
|
|
165
|
+
MessageSocket.setConfig({
|
|
166
|
+
url: 'ws://localhost:8080',
|
|
167
|
+
logLevel: 'info', // 设置日志级别
|
|
168
|
+
})
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### 使用独立的 Logger
|
|
172
|
+
|
|
173
|
+
你也可以在自己的代码中使用 Logger:
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
import { Logger } from '@brewer/dj-common'
|
|
177
|
+
|
|
178
|
+
const logger = new Logger('MyApp', 'debug')
|
|
179
|
+
|
|
180
|
+
logger.debug('调试信息')
|
|
181
|
+
logger.info('普通信息')
|
|
182
|
+
logger.warn('警告信息')
|
|
183
|
+
logger.error('错误信息')
|
|
184
|
+
|
|
185
|
+
// 动态修改日志级别
|
|
186
|
+
logger.setLevel('warn')
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## 多标签页管理
|
|
190
|
+
|
|
191
|
+
在 Web 应用中,用户可能会在多个标签页打开同一个应用。为了避免多个标签页同时建立 WebSocket 连接造成资源浪费,可以启用页面可见性管理:
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
import { MessageSocket } from '@brewer/dj-common'
|
|
195
|
+
|
|
196
|
+
MessageSocket.setConfig({
|
|
197
|
+
url: 'ws://localhost:8080',
|
|
198
|
+
enableVisibilityManagement: true, // 启用页面可见性管理
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
MessageSocket.start({
|
|
202
|
+
userId: '123',
|
|
203
|
+
token: 'your-token',
|
|
204
|
+
})
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**功能说明:**
|
|
208
|
+
|
|
209
|
+
- 当标签页切换到后台时,自动断开 WebSocket 连接
|
|
210
|
+
- 当标签页切换到前台时,自动重新连接
|
|
211
|
+
- 避免多个标签页同时维持连接,节省服务器资源
|
|
212
|
+
- 用户始终能在当前活跃的标签页接收实时消息
|
|
213
|
+
|
|
214
|
+
**适用场景:**
|
|
215
|
+
|
|
216
|
+
- ✅ 用户可能打开多个标签页的应用
|
|
217
|
+
- ✅ 需要优化服务器连接数的场景
|
|
218
|
+
- ✅ 移动端 WebView 应用(页面切换到后台)
|
|
219
|
+
- ❌ 需要在后台持续接收消息的场景
|
|
220
|
+
|
|
125
221
|
## 本地测试
|
|
126
222
|
|
|
127
223
|
```ts
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";const e={debug:10,info:20,warn:30,error:40,silent:50};class t{
|
|
1
|
+
"use strict";const e={debug:10,info:20,warn:30,error:40,silent:50};class t{constructor(e,t="warn"){this.name=e,this.level=t}setLevel(e){this.level=e}getLevel(){return this.level}debug(...e){this.logAtLevel("debug",console.debug,e)}info(...e){this.logAtLevel("info",console.info,e)}warn(...e){this.logAtLevel("warn",console.warn,e)}error(...e){this.logAtLevel("error",console.error,e)}shouldLog(t){return"silent"!==t&&e[t]>=e[this.level]}logAtLevel(e,t,i){this.shouldLog(e)&&t(`[${this.name}]`,...i)}}class i{constructor(e={}){this.socket=null,this.heartbeatTimer=null,this.reconnectTimer=null,this.callbackList=[],this.currentUrl=null,this.reconnectAttempts=0,this.manualClose=!1,this.config={...i.DEFAULT_CONFIG,...e},this.logger=new t("WebSocketClient",this.config.logLevel)}updateConfig(e){this.config={...this.config,...e},this.logger.setLevel(this.config.logLevel??i.DEFAULT_CONFIG.logLevel)}connect(e){const t=e||this.config.url;if(t){this.currentUrl=t,this.manualClose=!1;try{this.socket=new WebSocket(t),this.socket.onopen=()=>{this.logger.info("[WebSocketClient] 连接成功"),this.reconnectAttempts=0,this.startHeartbeat(),this.onOpen()},this.socket.onmessage=e=>{this.handleIncoming(e.data)},this.socket.onclose=e=>{this.logger.info("[WebSocketClient] 连接关闭",e.code,e.reason),this.stopHeartbeat(),this.onClose(e),!this.manualClose&&this.config.autoReconnect&&this.scheduleReconnect()},this.socket.onerror=e=>{this.logger.error("[WebSocketClient] 连接错误",e),this.stopHeartbeat(),this.onError(e)}}catch(e){this.logger.error("[WebSocketClient] 连接失败",e),this.config.autoReconnect&&!this.manualClose&&this.scheduleReconnect()}}else this.logger.error("[WebSocketClient] 缺少 WebSocket URL")}scheduleReconnect(){if(this.reconnectAttempts>=this.config.maxReconnectAttempts||!this.currentUrl||this.manualClose)return void(this.reconnectAttempts>=this.config.maxReconnectAttempts&&this.logger.warn("[WebSocketClient] 已达到最大重连次数"));this.reconnectAttempts+=1;const e=Math.min(this.config.reconnectDelay*this.reconnectAttempts,this.config.reconnectDelayMax);this.logger.debug(`[WebSocketClient] 将在 ${e}ms 后进行第 ${this.reconnectAttempts} 次重连`),this.reconnectTimer=window.setTimeout(()=>{this.connect(this.currentUrl)},e)}disconnect(){this.manualClose=!0,this.stopHeartbeat(),this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.socket&&(this.socket.close(),this.socket=null),this.currentUrl=null,this.reconnectAttempts=0}send(e){if(!this.socket||this.socket.readyState!==WebSocket.OPEN)return void this.logger.warn("[WebSocketClient] WebSocket 未连接,无法发送消息");const t="string"==typeof e?e:JSON.stringify(e);this.socket.send(t)}handleIncoming(e){if(!e)return;let t;try{t=JSON.parse(e)}catch{return void this.logger.warn("[WebSocketClient] 无法解析消息",e)}if(!t?.type)return;this.callbackList.filter(e=>e.type===t.type).forEach(({callback:e})=>{try{e(t.data,t)}catch(e){this.logger.error("[WebSocketClient] 回调执行失败",e)}}),this.onMessage(t)}startHeartbeat(){this.stopHeartbeat(),this.socket&&this.socket.readyState===WebSocket.OPEN&&(this.heartbeatTimer=window.setInterval(()=>{if(!this.socket||this.socket.readyState!==WebSocket.OPEN)return;const e=this.config.heartbeatMessage();this.send(e)},this.config.heartbeatInterval))}stopHeartbeat(){this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null)}on(e,t){const i="string"==typeof e?{type:e,callback:t}:e;i.type&&"function"==typeof i.callback?this.callbackList.push(i):this.logger.warn("[WebSocketClient] 无效的回调配置",i)}off(e,t){this.callbackList=t?this.callbackList.filter(i=>!(i.type===e&&i.callback===t)):this.callbackList.filter(t=>t.type!==e)}clearCallbacks(){this.callbackList=[]}getReadyState(){return this.socket?.readyState??WebSocket.CLOSED}isConnected(){return this.socket?.readyState===WebSocket.OPEN}onOpen(){this.logger.info("[WebSocketClient] 连接打开")}onClose(e){this.logger.info("[WebSocketClient] 连接打开")}onError(e){this.logger.error("[WebSocketClient] 连接错误",e)}onMessage(e){this.logger.debug("[WebSocketClient] 收到消息",e)}}i.DEFAULT_CONFIG={url:"",heartbeatInterval:25e3,maxReconnectAttempts:10,reconnectDelay:3e3,reconnectDelayMax:1e4,autoReconnect:!0,heartbeatMessage:()=>({type:"PING",timestamp:Date.now()}),logLevel:"warn"};class n{static updateLoggerLevel(){n.logger.setLevel(n.config.logLevel??n.DEFAULT_CONFIG.logLevel)}static initVisibilityListener(){"undefined"==typeof document||n.visibilityListenerInitialized||(document.addEventListener("visibilitychange",n.handleVisibilityChange),n.visibilityListenerInitialized=!0,n.logger.debug("[MessageSocket] 已初始化页面可见性监听"))}static removeVisibilityListener(){"undefined"!=typeof document&&n.visibilityListenerInitialized&&(document.removeEventListener("visibilitychange",n.handleVisibilityChange),n.visibilityListenerInitialized=!1,n.logger.debug("[MessageSocket] 已移除页面可见性监听"))}static configure(e){n.config={...n.config,...e}}static setConfig(e){return n.config={...n.config,...e},n.updateLoggerLevel(),n.config.callbacks&&n.config.callbacks.length>0&&n.setCallbacks(n.config.callbacks),n}static setCallbacks(e){return e&&0!==e.length?(n.config.callbacks=e,n):(this.logger.warn("[MessageSocket] 回调列表为空,无法设置回调"),n)}static start(e){if(!n.config.url)return void this.logger.error("[MessageSocket] 缺少配置 url, 请先调用 setConfig 设置配置!");const{userId:t,token:s}=e;if(!t||!s)return void this.logger.error("[MessageSocket] 缺少 userId 或 token,无法启动");this.logger.info("[MessageSocket] 开始连接",t);if(n.client&&n.client.isConnected()&&n.currentUserId===t&&n.currentToken===s)return void this.logger.debug("[MessageSocket] 复用现有连接");n.stop(),n.currentUserId=t,n.currentToken=s;const{url:o,...c}=n.config,r=`${o}/${t}?token=${encodeURIComponent(s)}`;n.client=new i({...c,url:r}),n.config.callbacks&&n.config.callbacks.length>0&&n.config.callbacks.forEach(e=>n.registerCallbacks(e)),n.config.enableVisibilityManagement&&n.initVisibilityListener(),n.client.connect()}static stop(){n.client&&(n.client.disconnect(),n.client.clearCallbacks(),n.client=null),n.removeVisibilityListener(),n.currentUserId=null,n.currentToken=null}static registerCallbacks(e){e?"object"==typeof e?"function"==typeof e.callback?"string"==typeof e.type?n.client?n.client.on(e):this.logger.warn("[MessageSocket] WebSocket 客户端未初始化,无法注册回调"):this.logger.warn("[MessageSocket] 注册回调失败,type 不是字符串",e):this.logger.warn("[MessageSocket] 注册回调失败,callback 不是函数",e):this.logger.warn("[MessageSocket] 注册回调失败,entry 不是对象",e):this.logger.warn("[MessageSocket] 注册回调失败,缺少 entry",e)}static unregisterCallbacks(e,t){n.client&&n.client.off(e,t)}static send(e){n.client?n.client.send(e):this.logger.warn("[MessageSocket] WebSocket 客户端未初始化,无法发送消息")}static getReadyState(){return n.client?.getReadyState()??WebSocket.CLOSED}static isConnected(){return n.client?.isConnected()??!1}static getCurrentUserId(){return n.currentUserId}static getCurrentToken(){return n.currentToken}}n.DEFAULT_CONFIG={url:"",heartbeatInterval:25e3,maxReconnectAttempts:10,reconnectDelay:3e3,reconnectDelayMax:1e4,autoReconnect:!0,callbacks:[],heartbeatMessage:()=>({type:"PING",timestamp:Date.now()}),logLevel:"warn",enableVisibilityManagement:!1},n.client=null,n.logger=new t("MessageSocket",n.DEFAULT_CONFIG.logLevel),n.currentUserId=null,n.currentToken=null,n.config={...n.DEFAULT_CONFIG},n.visibilityListenerInitialized=!1,n.handleVisibilityChange=()=>{if("undefined"==typeof document)return;!document.hidden?n.currentUserId&&n.currentToken&&(n.logger.info("[MessageSocket] 页面可见,尝试重新连接"),n.client&&n.client.isConnected()||n.start({userId:n.currentUserId,token:n.currentToken})):(n.logger.info("[MessageSocket] 页面不可见,断开连接"),n.client&&n.client.disconnect())},exports.MessageSocket=n;
|
|
2
2
|
//# sourceMappingURL=MessageSocket.cjs.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MessageSocket.cjs.js","sources":["../src/WebSocketClient.ts","../src/MessageSocket.ts"],"sourcesContent":["export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent'\n\nexport const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {\n debug: 10,\n info: 20,\n warn: 30,\n error: 40,\n silent: 50,\n}\n\ntype NonSilentLogLevel = Exclude<LogLevel, 'silent'>\n\n/**\n * WebSocket 配置选项\n */\nexport interface WebSocketConfig {\n /** WebSocket 服务器地址(可选,可以在 connect 时指定) */\n url?: string\n /** 心跳间隔(毫秒),默认 25000 */\n heartbeatInterval?: number\n /** 最大重连次数,默认 10 */\n maxReconnectAttempts?: number\n /** 重连延迟(毫秒),默认 3000 */\n reconnectDelay?: number\n /** 最大重连延迟(毫秒),默认 10000 */\n reconnectDelayMax?: number\n /** 心跳消息生成器 */\n heartbeatMessage?: () => string | object\n /** 是否自动重连,默认 true */\n autoReconnect?: boolean\n /** 日志输出等级 */\n logLevel?: LogLevel\n}\n\n/**\n * 消息数据结构\n */\nexport interface MessageData<T = unknown> {\n /** 消息类型 */\n type: string\n /** 消息数据 */\n data: T\n /** 元数据 */\n meta?: Record<string, unknown>\n /** 时间戳 */\n timestamp?: number\n}\n\n/**\n * 消息回调函数\n */\nexport type MessageCallback<T = unknown> = (data: T, message?: MessageData<T>) => void\n\n/**\n * 消息回调配置\n */\nexport interface MessageCallbackEntry<T = unknown> {\n /** 消息类型 */\n type: string\n /** 回调函数 */\n callback: MessageCallback<T>\n}\n\n/**\n * WebSocket 基础封装类\n * 提供连接管理、心跳、自动重连、消息回调等基础功能\n */\nexport class WebSocketClient {\n /** WebSocket 连接实例 */\n protected socket: WebSocket | null = null\n\n /** 心跳定时器 */\n protected heartbeatTimer: number | null = null\n\n /** 重连定时器 */\n protected reconnectTimer: number | null = null\n\n /** 消息回调列表 */\n protected callbackList: MessageCallbackEntry<unknown>[] = []\n\n /** 当前连接的 URL */\n protected currentUrl: string | null = null\n\n /** 配置选项 */\n protected config: Required<WebSocketConfig>\n\n /** 重连次数 */\n protected reconnectAttempts = 0\n\n /** 是否手动关闭 */\n protected manualClose = false\n\n /**\n * 是否应输出某个等级的日志\n */\n protected shouldLog(level: NonSilentLogLevel): boolean {\n const currentLevel = this.config.logLevel ?? WebSocketClient.DEFAULT_CONFIG.logLevel\n return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[currentLevel]\n }\n\n protected logDebug(...values: unknown[]): void {\n if (!this.shouldLog('debug')) {\n return\n }\n console.debug(...values)\n }\n\n protected logInfo(...values: unknown[]): void {\n if (!this.shouldLog('info')) {\n return\n }\n console.info(...values)\n }\n\n protected logWarn(...values: unknown[]): void {\n if (!this.shouldLog('warn')) {\n return\n }\n console.warn(...values)\n }\n\n protected logError(...values: unknown[]): void {\n if (!this.shouldLog('error')) {\n return\n }\n console.error(...values)\n }\n\n /**\n * 默认配置\n */\n protected static readonly DEFAULT_CONFIG: Required<WebSocketConfig> = {\n url: '',\n heartbeatInterval: 25000,\n maxReconnectAttempts: 10,\n reconnectDelay: 3000,\n reconnectDelayMax: 10000,\n autoReconnect: true,\n heartbeatMessage: () => ({\n type: 'PING',\n timestamp: Date.now(),\n }),\n logLevel: 'warn',\n }\n\n /**\n * 构造函数\n * @param config 配置选项\n */\n constructor(config: WebSocketConfig = {}) {\n this.config = { ...WebSocketClient.DEFAULT_CONFIG, ...config }\n }\n\n /**\n * 更新配置\n * @param config 新的配置选项\n */\n public updateConfig(config: Partial<WebSocketConfig>): void {\n this.config = { ...this.config, ...config }\n }\n\n /**\n * 连接到 WebSocket 服务器\n * @param url WebSocket 地址(可选,如果不传则使用配置中的 url)\n */\n public connect(url?: string): void {\n const targetUrl = url || this.config.url\n if (!targetUrl) {\n this.logError('[WebSocketClient] 缺少 WebSocket URL')\n return\n }\n\n this.currentUrl = targetUrl\n this.manualClose = false\n\n try {\n this.socket = new WebSocket(targetUrl)\n\n this.socket.onopen = () => {\n this.logInfo('[WebSocketClient] 连接成功')\n this.reconnectAttempts = 0\n this.startHeartbeat()\n this.onOpen()\n }\n\n this.socket.onmessage = (event: MessageEvent) => {\n this.handleIncoming(event.data)\n }\n\n this.socket.onclose = (event: CloseEvent) => {\n this.logInfo('[WebSocketClient] 连接关闭', event.code, event.reason)\n this.stopHeartbeat()\n this.onClose(event)\n\n if (!this.manualClose && this.config.autoReconnect) {\n this.scheduleReconnect()\n }\n }\n\n this.socket.onerror = (event: Event) => {\n this.logError('[WebSocketClient] 连接错误', event)\n this.stopHeartbeat()\n this.onError(event)\n }\n } catch (error) {\n this.logError('[WebSocketClient] 连接失败', error)\n if (this.config.autoReconnect && !this.manualClose) {\n this.scheduleReconnect()\n }\n }\n }\n\n /**\n * 计划重连\n */\n protected scheduleReconnect(): void {\n if (this.reconnectAttempts >= this.config.maxReconnectAttempts || !this.currentUrl || this.manualClose) {\n if (this.reconnectAttempts >= this.config.maxReconnectAttempts) {\n this.logWarn('[WebSocketClient] 已达到最大重连次数')\n }\n return\n }\n\n this.reconnectAttempts += 1\n const delay = Math.min(this.config.reconnectDelay * this.reconnectAttempts, this.config.reconnectDelayMax)\n\n this.logDebug(`[WebSocketClient] 将在 ${delay}ms 后进行第 ${this.reconnectAttempts} 次重连`)\n\n this.reconnectTimer = window.setTimeout(() => {\n this.connect(this.currentUrl!)\n }, delay)\n }\n\n /**\n * 断开连接\n */\n public disconnect(): void {\n this.manualClose = true\n this.stopHeartbeat()\n\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer)\n this.reconnectTimer = null\n }\n\n if (this.socket) {\n this.socket.close()\n this.socket = null\n }\n\n this.currentUrl = null\n this.reconnectAttempts = 0\n }\n\n /**\n * 发送消息\n * @param data 消息数据\n */\n public send(data: string | object): void {\n if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {\n this.logWarn('[WebSocketClient] WebSocket 未连接,无法发送消息')\n return\n }\n\n const message = typeof data === 'string' ? data : JSON.stringify(data)\n this.socket.send(message)\n }\n\n /**\n * 处理接收的消息\n * @param data 接收到的数据\n */\n protected handleIncoming(data: string): void {\n if (!data) return\n\n let message: MessageData\n try {\n message = JSON.parse(data)\n } catch {\n this.logWarn('[WebSocketClient] 无法解析消息', data)\n return\n }\n\n if (!message?.type) {\n return\n }\n\n // 触发匹配的回调\n const matched = this.callbackList.filter((entry) => entry.type === message.type)\n matched.forEach(({ callback }) => {\n try {\n callback(message.data, message)\n } catch (error) {\n this.logError('[WebSocketClient] 回调执行失败', error)\n }\n })\n\n // 调用钩子\n this.onMessage(message)\n }\n\n /**\n * 启动心跳\n */\n protected startHeartbeat(): void {\n this.stopHeartbeat()\n\n if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {\n return\n }\n\n this.heartbeatTimer = window.setInterval(() => {\n if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {\n return\n }\n\n const heartbeatData = this.config.heartbeatMessage()\n this.send(heartbeatData)\n }, this.config.heartbeatInterval)\n }\n\n /**\n * 停止心跳\n */\n protected stopHeartbeat(): void {\n if (this.heartbeatTimer) {\n clearInterval(this.heartbeatTimer)\n this.heartbeatTimer = null\n }\n }\n\n /**\n * 注册消息回调\n * @param entry 回调配置\n */\n public on<T = unknown>(type: string, callback: MessageCallback<T>): void\n public on<T = unknown>(entry: MessageCallbackEntry<T>): void\n public on<T = unknown>(typeOrEntry: string | MessageCallbackEntry<T>, callback?: MessageCallback<T>): void {\n const entry: MessageCallbackEntry<T> =\n typeof typeOrEntry === 'string' ? { type: typeOrEntry, callback: callback! } : typeOrEntry\n\n if (!entry.type || typeof entry.callback !== 'function') {\n this.logWarn('[WebSocketClient] 无效的回调配置', entry)\n return\n }\n\n this.callbackList.push(entry as MessageCallbackEntry<unknown>)\n }\n\n /**\n * 取消注册消息回调\n * @param type 消息类型\n * @param callback 回调函数(可选,如果不传则移除该类型的所有回调)\n */\n public off<T = unknown>(type: string, callback?: MessageCallback<T>): void {\n if (callback) {\n this.callbackList = this.callbackList.filter((entry) => !(entry.type === type && entry.callback === callback))\n } else {\n this.callbackList = this.callbackList.filter((entry) => entry.type !== type)\n }\n }\n\n /**\n * 清空所有回调\n */\n public clearCallbacks(): void {\n this.callbackList = []\n }\n\n /**\n * 获取当前连接状态\n */\n public getReadyState(): number {\n return this.socket?.readyState ?? WebSocket.CLOSED\n }\n\n /**\n * 是否已连接\n */\n public isConnected(): boolean {\n return this.socket?.readyState === WebSocket.OPEN\n }\n\n // ========== 钩子方法(子类可以重写) ==========\n\n /**\n * 连接打开时的钩子\n */\n protected onOpen(): void {\n // 子类可以重写\n this.logInfo('[WebSocketClient] 连接打开')\n }\n\n /**\n * 连接关闭时的钩子\n */\n protected onClose(_event: CloseEvent): void {\n // 子类可以重写\n this.logInfo('[WebSocketClient] 连接打开')\n }\n\n /**\n * 连接错误时的钩子\n */\n protected onError(_event: Event): void {\n // 子类可以重写\n this.logError('[WebSocketClient] 连接错误', _event)\n }\n\n /**\n * 收到消息时的钩子\n */\n protected onMessage(_message: MessageData): void {\n // 子类可以重写\n this.logDebug('[WebSocketClient] 收到消息', _message)\n }\n}\n","import { WebSocketClient, WebSocketConfig, MessageCallbackEntry, LogLevel, LOG_LEVEL_PRIORITY } from './WebSocketClient'\n\ntype NonSilentLogLevel = Exclude<LogLevel, 'silent'>\n\n/**\n * MessageSocket 配置选项\n */\nexport interface MessageSocketConfig extends WebSocketConfig {\n /** WebSocket 服务器基础地址,默认 'ws://dev-gateway.chinamarket.cn' */\n baseUrl?: string\n /** WebSocket 路径,默认 '/api/user-web/websocket/messageServer' */\n path?: string\n /** 消息回调列表 */\n callbacks?: MessageCallbackEntry[]\n /** 日志级别 */\n logLevel?: LogLevel\n}\n\n/**\n * MessageSocket 启动选项\n */\nexport interface MessageSocketStartOptions {\n /** 用户ID(必需) */\n userId: string\n /** 认证令牌(必需) */\n token: string\n}\n\n/**\n * 消息 Socket 类\n * 基于 WebSocketClient,提供用户认证和消息管理功能\n * 用于获取用户未读消息数量等业务场景\n *\n * @example\n * ```typescript\n * // 启动连接\n * MessageSocket.start({\n * userId: '1234567890',\n * token: 'your-token',\n * })\n *\n * // 停止连接\n * MessageSocket.stop()\n *\n * // 设置配置\n * MessageSocket.setConfig({\n * baseUrl: 'ws://your-server.com',\n * path: '/your/path',\n * })\n *\n * // 初始化时设置回调\n * MessageSocket.setCallbacks([\n * {\n * type: 'UNREAD_COUNT',\n * callback: (payload) => {\n * console.log('未读消息数:', payload)\n * }\n * }\n * ])\n * // 动态注册回调\n * MessageSocket.registerCallbacks({\n * type: 'NEW_MESSAGE',\n * callback: (payload) => {\n * console.log('新消息:', payload)\n * }\n * })\n * ```\n */\nexport class MessageSocket {\n /** 默认配置 */\n private static readonly DEFAULT_CONFIG: MessageSocketConfig = {\n baseUrl: '',\n path: '',\n heartbeatInterval: 25000,\n maxReconnectAttempts: 10,\n reconnectDelay: 3000,\n reconnectDelayMax: 10000,\n autoReconnect: true,\n callbacks: [],\n logLevel: 'warn',\n }\n\n /** WebSocket 客户端实例 */\n private static client: WebSocketClient | null = null\n\n private static readonly DEFAULT_LOG_LEVEL: LogLevel = 'warn'\n private static shouldLog(level: NonSilentLogLevel): boolean {\n const currentLevel = MessageSocket.config.logLevel ?? MessageSocket.DEFAULT_LOG_LEVEL\n return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[currentLevel]\n }\n private static log(level: NonSilentLogLevel, ...values: unknown[]): void {\n if (!MessageSocket.shouldLog(level)) {\n return\n }\n switch (level) {\n case 'debug':\n console.debug(...values)\n break\n case 'info':\n console.info(...values)\n break\n case 'warn':\n console.warn(...values)\n break\n case 'error':\n console.error(...values)\n break\n }\n }\n private static logDebug(...values: unknown[]): void {\n MessageSocket.log('debug', ...values)\n }\n private static logInfo(...values: unknown[]): void {\n MessageSocket.log('info', ...values)\n }\n private static logWarn(...values: unknown[]): void {\n MessageSocket.log('warn', ...values)\n }\n private static logError(...values: unknown[]): void {\n MessageSocket.log('error', ...values)\n }\n\n /** 当前用户ID */\n private static currentUserId: string | null = null\n\n /** 当前token */\n private static currentToken: string | null = null\n\n /** 当前配置 */\n private static config: MessageSocketConfig = { ...MessageSocket.DEFAULT_CONFIG }\n\n /**\n * 配置 MessageSocket\n * @param config 配置选项\n */\n public static configure(config: Partial<MessageSocketConfig>): void {\n MessageSocket.config = { ...MessageSocket.config, ...config }\n }\n\n /**\n * 设置配置\n * @param config 配置选项\n * @returns MessageSocket\n */\n public static setConfig(config: Partial<MessageSocketConfig>): typeof MessageSocket {\n MessageSocket.config = { ...MessageSocket.config, ...config }\n if (MessageSocket.config.callbacks && MessageSocket.config.callbacks.length > 0) {\n MessageSocket.setCallbacks(MessageSocket.config.callbacks)\n }\n return MessageSocket\n }\n\n /**\n * 设置回调\n * @param callbacks 回调列表\n * @returns MessageSocket\n */\n public static setCallbacks(callbacks: MessageCallbackEntry[]): typeof MessageSocket {\n if (!callbacks || callbacks.length === 0) {\n MessageSocket.logWarn('[MessageSocket] 回调列表为空,无法设置回调')\n return MessageSocket\n }\n\n MessageSocket.config.callbacks = callbacks\n return MessageSocket\n }\n\n /**\n * 启动连接\n * @param options 启动选项\n * 回调在开始的时候注册,这种能\n */\n public static start(options: MessageSocketStartOptions): void {\n if (!MessageSocket.config.baseUrl || !MessageSocket.config.path) {\n MessageSocket.logError('[MessageSocket] 缺少配置 baseUrl 或 path, 请先调用 setConfig 设置配置!')\n return\n }\n\n const { userId, token } = options\n\n if (!userId || !token) {\n MessageSocket.logError('[MessageSocket] 缺少 userId 或 token,无法启动')\n return\n }\n\n MessageSocket.logInfo('[MessageSocket] 开始连接', userId)\n\n // 检查是否可以复用现有连接\n const shouldReuse =\n MessageSocket.client &&\n MessageSocket.client.isConnected() &&\n MessageSocket.currentUserId === userId &&\n MessageSocket.currentToken === token\n\n if (shouldReuse) {\n MessageSocket.logDebug('[MessageSocket] 复用现有连接')\n return\n }\n\n // 停止旧连接\n MessageSocket.stop()\n\n // 保存当前用户信息\n MessageSocket.currentUserId = userId\n MessageSocket.currentToken = token\n\n // 构建 WebSocket URL\n const { baseUrl, path, ...clientConfig } = MessageSocket.config\n const url = `${baseUrl}${path}/${userId}?token=${encodeURIComponent(token)}`\n\n // 创建新的 WebSocket 客户端\n MessageSocket.client = new WebSocketClient({\n ...clientConfig,\n url,\n })\n\n if (MessageSocket.config.callbacks && MessageSocket.config.callbacks.length > 0) {\n MessageSocket.config.callbacks.forEach((entry) => MessageSocket.registerCallbacks(entry))\n }\n\n // 连接\n MessageSocket.client.connect()\n }\n\n /**\n * 停止连接\n */\n public static stop(): void {\n if (MessageSocket.client) {\n MessageSocket.client.disconnect()\n MessageSocket.client.clearCallbacks()\n MessageSocket.client = null\n }\n\n MessageSocket.currentUserId = null\n MessageSocket.currentToken = null\n }\n\n /**\n * 注册消息回调\n * @param entry 回调配置\n */\n public static registerCallbacks<T = unknown>(entry: MessageCallbackEntry<T>): void {\n if (!entry) {\n MessageSocket.logWarn('[MessageSocket] 注册回调失败,缺少 entry', entry)\n return\n }\n\n if (typeof entry !== 'object') {\n MessageSocket.logWarn('[MessageSocket] 注册回调失败,entry 不是对象', entry)\n return\n }\n\n if (typeof entry.callback !== 'function') {\n MessageSocket.logWarn('[MessageSocket] 注册回调失败,callback 不是函数', entry)\n return\n }\n\n if (typeof entry.type !== 'string') {\n MessageSocket.logWarn('[MessageSocket] 注册回调失败,type 不是字符串', entry)\n return\n }\n\n if (!MessageSocket.client) {\n MessageSocket.logWarn('[MessageSocket] WebSocket 客户端未初始化,无法注册回调')\n return\n }\n\n MessageSocket.client.on(entry)\n }\n\n /**\n * 取消注册消息回调\n * @param type 消息类型\n * @param callback 回调函数(可选)\n */\n public static unregisterCallbacks<T = unknown>(type: string, callback?: (data: T, message?: unknown) => void): void {\n if (!MessageSocket.client) {\n return\n }\n\n MessageSocket.client.off(type, callback)\n }\n\n /**\n * 发送消息\n * @param data 消息数据\n */\n public static send(data: string | object): void {\n if (!MessageSocket.client) {\n MessageSocket.logWarn('[MessageSocket] WebSocket 客户端未初始化,无法发送消息')\n return\n }\n\n MessageSocket.client.send(data)\n }\n\n /**\n * 获取连接状态\n */\n public static getReadyState(): number {\n return MessageSocket.client?.getReadyState() ?? WebSocket.CLOSED\n }\n\n /**\n * 是否已连接\n */\n public static isConnected(): boolean {\n return MessageSocket.client?.isConnected() ?? false\n }\n\n /**\n * 获取当前用户ID\n */\n public static getCurrentUserId(): string | null {\n return MessageSocket.currentUserId\n }\n\n /**\n * 获取当前token\n */\n public static getCurrentToken(): string | null {\n return MessageSocket.currentToken\n }\n}\n"],"names":["LOG_LEVEL_PRIORITY","debug","info","warn","error","silent","WebSocketClient","shouldLog","level","currentLevel","this","config","logLevel","DEFAULT_CONFIG","logDebug","values","console","logInfo","logWarn","logError","constructor","socket","heartbeatTimer","reconnectTimer","callbackList","currentUrl","reconnectAttempts","manualClose","updateConfig","connect","url","targetUrl","WebSocket","onopen","startHeartbeat","onOpen","onmessage","event","handleIncoming","data","onclose","code","reason","stopHeartbeat","onClose","autoReconnect","scheduleReconnect","onerror","onError","maxReconnectAttempts","delay","Math","min","reconnectDelay","reconnectDelayMax","window","setTimeout","disconnect","clearTimeout","close","send","readyState","OPEN","message","JSON","stringify","parse","type","filter","entry","forEach","callback","onMessage","setInterval","heartbeatData","heartbeatMessage","heartbeatInterval","clearInterval","on","typeOrEntry","push","off","clearCallbacks","getReadyState","CLOSED","isConnected","_event","_message","timestamp","Date","now","MessageSocket","DEFAULT_LOG_LEVEL","log","configure","setConfig","callbacks","length","setCallbacks","start","options","baseUrl","path","userId","token","client","currentUserId","currentToken","stop","clientConfig","encodeURIComponent","registerCallbacks","unregisterCallbacks","getCurrentUserId","getCurrentToken"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"MessageSocket.cjs.js","sources":["../src/logger.ts","../src/WebSocketClient.ts","../src/MessageSocket.ts"],"sourcesContent":["export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent'\n\nexport const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {\n debug: 10,\n info: 20,\n warn: 30,\n error: 40,\n silent: 50,\n}\n\nexport class Logger {\n private level: LogLevel\n\n constructor(\n private readonly name: string,\n level: LogLevel = 'warn'\n ) {\n this.level = level\n }\n\n setLevel(level: LogLevel): void {\n this.level = level\n }\n\n getLevel(): LogLevel {\n return this.level\n }\n\n debug(...values: unknown[]): void {\n this.logAtLevel('debug', console.debug, values)\n }\n\n info(...values: unknown[]): void {\n this.logAtLevel('info', console.info, values)\n }\n\n warn(...values: unknown[]): void {\n this.logAtLevel('warn', console.warn, values)\n }\n\n error(...values: unknown[]): void {\n this.logAtLevel('error', console.error, values)\n }\n\n private shouldLog(level: LogLevel): boolean {\n if (level === 'silent') {\n return false\n }\n return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[this.level]\n }\n\n private logAtLevel(level: LogLevel, writer: (...args: unknown[]) => void, values: unknown[]): void {\n if (!this.shouldLog(level)) {\n return\n }\n writer(`[${this.name}]`, ...values)\n }\n}\n","import { Logger, LogLevel } from './logger'\n\n/**\n * WebSocket 配置选项\n */\nexport interface WebSocketConfig {\n /** WebSocket 服务器地址(可选,可以在 connect 时指定) */\n url?: string\n /** 心跳间隔(毫秒),默认 25000 */\n heartbeatInterval?: number\n /** 最大重连次数,默认 10 */\n maxReconnectAttempts?: number\n /** 重连延迟(毫秒),默认 3000 */\n reconnectDelay?: number\n /** 最大重连延迟(毫秒),默认 10000 */\n reconnectDelayMax?: number\n /** 心跳消息生成器 */\n heartbeatMessage?: () => string | object\n /** 是否自动重连,默认 true */\n autoReconnect?: boolean\n /** 日志输出等级 */\n logLevel?: LogLevel\n}\n\n/**\n * 消息数据结构\n */\nexport interface MessageData<T = unknown> {\n /** 消息类型 */\n type: string\n /** 消息数据 */\n data: T\n /** 元数据 */\n meta?: Record<string, unknown>\n /** 时间戳 */\n timestamp?: number\n}\n\n/**\n * 消息回调函数\n */\nexport type MessageCallback<T = unknown> = (data: T, message?: MessageData<T>) => void\n\n/**\n * 消息回调配置\n */\nexport interface MessageCallbackEntry<T = unknown> {\n /** 消息类型 */\n type: string\n /** 回调函数 */\n callback: MessageCallback<T>\n}\n\n/**\n * WebSocket 基础封装类\n * 提供连接管理、心跳、自动重连、消息回调等基础功能\n */\nexport class WebSocketClient {\n /** WebSocket 连接实例 */\n protected socket: WebSocket | null = null\n\n /** 心跳定时器 */\n protected heartbeatTimer: number | null = null\n\n /** 重连定时器 */\n protected reconnectTimer: number | null = null\n\n /** 消息回调列表 */\n protected callbackList: MessageCallbackEntry<unknown>[] = []\n\n /** 当前连接的 URL */\n protected currentUrl: string | null = null\n\n /** 配置选项 */\n protected config: Required<WebSocketConfig>\n\n /** 重连次数 */\n protected reconnectAttempts = 0\n\n /** 是否手动关闭 */\n protected manualClose = false\n\n /** 日志器 */\n protected readonly logger: Logger\n\n /**\n * 默认配置\n */\n protected static readonly DEFAULT_CONFIG: Required<WebSocketConfig> = {\n url: '',\n heartbeatInterval: 25000,\n maxReconnectAttempts: 10,\n reconnectDelay: 3000,\n reconnectDelayMax: 10000,\n autoReconnect: true,\n heartbeatMessage: () => ({\n type: 'PING',\n timestamp: Date.now(),\n }),\n logLevel: 'warn',\n }\n\n /**\n * 构造函数\n * @param config 配置选项\n */\n constructor(config: WebSocketConfig = {}) {\n this.config = { ...WebSocketClient.DEFAULT_CONFIG, ...config }\n this.logger = new Logger('WebSocketClient', this.config.logLevel)\n }\n\n /**\n * 更新配置\n * @param config 新的配置选项\n */\n public updateConfig(config: Partial<WebSocketConfig>): void {\n this.config = { ...this.config, ...config }\n this.logger.setLevel(this.config.logLevel ?? WebSocketClient.DEFAULT_CONFIG.logLevel)\n }\n\n /**\n * 连接到 WebSocket 服务器\n * @param url WebSocket 地址(可选,如果不传则使用配置中的 url)\n */\n public connect(url?: string): void {\n const targetUrl = url || this.config.url\n if (!targetUrl) {\n this.logger.error('[WebSocketClient] 缺少 WebSocket URL')\n return\n }\n\n this.currentUrl = targetUrl\n this.manualClose = false\n\n try {\n this.socket = new WebSocket(targetUrl)\n\n this.socket.onopen = () => {\n this.logger.info('[WebSocketClient] 连接成功')\n this.reconnectAttempts = 0\n this.startHeartbeat()\n this.onOpen()\n }\n\n this.socket.onmessage = (event: MessageEvent) => {\n this.handleIncoming(event.data)\n }\n\n this.socket.onclose = (event: CloseEvent) => {\n this.logger.info('[WebSocketClient] 连接关闭', event.code, event.reason)\n this.stopHeartbeat()\n this.onClose(event)\n\n if (!this.manualClose && this.config.autoReconnect) {\n this.scheduleReconnect()\n }\n }\n\n this.socket.onerror = (event: Event) => {\n this.logger.error('[WebSocketClient] 连接错误', event)\n this.stopHeartbeat()\n this.onError(event)\n }\n } catch (error) {\n this.logger.error('[WebSocketClient] 连接失败', error)\n if (this.config.autoReconnect && !this.manualClose) {\n this.scheduleReconnect()\n }\n }\n }\n\n /**\n * 计划重连\n */\n protected scheduleReconnect(): void {\n if (this.reconnectAttempts >= this.config.maxReconnectAttempts || !this.currentUrl || this.manualClose) {\n if (this.reconnectAttempts >= this.config.maxReconnectAttempts) {\n this.logger.warn('[WebSocketClient] 已达到最大重连次数')\n }\n return\n }\n\n this.reconnectAttempts += 1\n const delay = Math.min(this.config.reconnectDelay * this.reconnectAttempts, this.config.reconnectDelayMax)\n\n this.logger.debug(`[WebSocketClient] 将在 ${delay}ms 后进行第 ${this.reconnectAttempts} 次重连`)\n\n this.reconnectTimer = window.setTimeout(() => {\n this.connect(this.currentUrl!)\n }, delay)\n }\n\n /**\n * 断开连接\n */\n public disconnect(): void {\n this.manualClose = true\n this.stopHeartbeat()\n\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer)\n this.reconnectTimer = null\n }\n\n if (this.socket) {\n this.socket.close()\n this.socket = null\n }\n\n this.currentUrl = null\n this.reconnectAttempts = 0\n }\n\n /**\n * 发送消息\n * @param data 消息数据\n */\n public send(data: string | object): void {\n if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {\n this.logger.warn('[WebSocketClient] WebSocket 未连接,无法发送消息')\n return\n }\n\n const message = typeof data === 'string' ? data : JSON.stringify(data)\n this.socket.send(message)\n }\n\n /**\n * 处理接收的消息\n * @param data 接收到的数据\n */\n protected handleIncoming(data: string): void {\n if (!data) return\n\n let message: MessageData\n try {\n message = JSON.parse(data)\n } catch {\n this.logger.warn('[WebSocketClient] 无法解析消息', data)\n return\n }\n\n if (!message?.type) {\n return\n }\n\n // 触发匹配的回调\n const matched = this.callbackList.filter((entry) => entry.type === message.type)\n matched.forEach(({ callback }) => {\n try {\n callback(message.data, message)\n } catch (error) {\n this.logger.error('[WebSocketClient] 回调执行失败', error)\n }\n })\n\n // 调用钩子\n this.onMessage(message)\n }\n\n /**\n * 启动心跳\n */\n protected startHeartbeat(): void {\n this.stopHeartbeat()\n\n if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {\n return\n }\n\n this.heartbeatTimer = window.setInterval(() => {\n if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {\n return\n }\n\n const heartbeatData = this.config.heartbeatMessage()\n this.send(heartbeatData)\n }, this.config.heartbeatInterval)\n }\n\n /**\n * 停止心跳\n */\n protected stopHeartbeat(): void {\n if (this.heartbeatTimer) {\n clearInterval(this.heartbeatTimer)\n this.heartbeatTimer = null\n }\n }\n\n /**\n * 注册消息回调\n * @param entry 回调配置\n */\n public on<T = unknown>(type: string, callback: MessageCallback<T>): void\n public on<T = unknown>(entry: MessageCallbackEntry<T>): void\n public on<T = unknown>(typeOrEntry: string | MessageCallbackEntry<T>, callback?: MessageCallback<T>): void {\n const entry: MessageCallbackEntry<T> =\n typeof typeOrEntry === 'string' ? { type: typeOrEntry, callback: callback! } : typeOrEntry\n\n if (!entry.type || typeof entry.callback !== 'function') {\n this.logger.warn('[WebSocketClient] 无效的回调配置', entry)\n return\n }\n\n this.callbackList.push(entry as MessageCallbackEntry<unknown>)\n }\n\n /**\n * 取消注册消息回调\n * @param type 消息类型\n * @param callback 回调函数(可选,如果不传则移除该类型的所有回调)\n */\n public off<T = unknown>(type: string, callback?: MessageCallback<T>): void {\n if (callback) {\n this.callbackList = this.callbackList.filter((entry) => !(entry.type === type && entry.callback === callback))\n } else {\n this.callbackList = this.callbackList.filter((entry) => entry.type !== type)\n }\n }\n\n /**\n * 清空所有回调\n */\n public clearCallbacks(): void {\n this.callbackList = []\n }\n\n /**\n * 获取当前连接状态\n */\n public getReadyState(): number {\n return this.socket?.readyState ?? WebSocket.CLOSED\n }\n\n /**\n * 是否已连接\n */\n public isConnected(): boolean {\n return this.socket?.readyState === WebSocket.OPEN\n }\n\n // ========== 钩子方法(子类可以重写) ==========\n\n /**\n * 连接打开时的钩子\n */\n protected onOpen(): void {\n // 子类可以重写\n this.logger.info('[WebSocketClient] 连接打开')\n }\n\n /**\n * 连接关闭时的钩子\n */\n protected onClose(_event: CloseEvent): void {\n // 子类可以重写\n this.logger.info('[WebSocketClient] 连接打开')\n }\n\n /**\n * 连接错误时的钩子\n */\n protected onError(_event: Event): void {\n // 子类可以重写\n this.logger.error('[WebSocketClient] 连接错误', _event)\n }\n\n /**\n * 收到消息时的钩子\n */\n protected onMessage(_message: MessageData): void {\n // 子类可以重写\n this.logger.debug('[WebSocketClient] 收到消息', _message)\n }\n}\n","import { WebSocketClient, WebSocketConfig, MessageCallbackEntry } from './WebSocketClient'\nimport { Logger, LogLevel } from './logger'\n\n/**\n * MessageSocket 配置选项\n */\nexport interface MessageSocketConfig extends WebSocketConfig {\n /** WebSocket 服务器地址,默认 '' */\n url?: string\n /** 消息回调列表 */\n callbacks?: MessageCallbackEntry[]\n /** 日志级别 */\n logLevel?: LogLevel\n /** 是否启用页面可见性管理(标签页切换时自动断开/重连),默认 false */\n enableVisibilityManagement?: boolean\n}\n\n/**\n * MessageSocket 启动选项\n */\nexport interface MessageSocketStartOptions {\n /** 用户ID(必需) */\n userId: string\n /** 认证令牌(必需) */\n token: string\n}\n\n/**\n * 消息 Socket 类\n * 基于 WebSocketClient,提供用户认证和消息管理功能\n * 用于获取用户未读消息数量等业务场景\n *\n * @example\n * ```typescript\n * // 启动连接\n * MessageSocket.start({\n * userId: '1234567890',\n * token: 'your-token',\n * })\n *\n * // 停止连接\n * MessageSocket.stop()\n *\n * // 设置配置\n * MessageSocket.setConfig({\n * baseUrl: 'ws://your-server.com',\n * path: '/your/path',\n * })\n *\n * // 初始化时设置回调\n * MessageSocket.setCallbacks([\n * {\n * type: 'UNREAD_COUNT',\n * callback: (payload) => {\n * console.log('未读消息数:', payload)\n * }\n * }\n * ])\n * // 动态注册回调\n * MessageSocket.registerCallbacks({\n * type: 'NEW_MESSAGE',\n * callback: (payload) => {\n * console.log('新消息:', payload)\n * }\n * })\n * ```\n * // logLevel 设置日志级别,默认 'warn',可选 'debug', 'info', 'warn', 'error', 'silent'\n *\n */\nexport class MessageSocket {\n /** 默认配置 */\n private static readonly DEFAULT_CONFIG: Required<MessageSocketConfig> = {\n url: '',\n heartbeatInterval: 25000,\n maxReconnectAttempts: 10,\n reconnectDelay: 3000,\n reconnectDelayMax: 10000,\n autoReconnect: true,\n callbacks: [],\n heartbeatMessage: () => ({\n type: 'PING',\n timestamp: Date.now(),\n }),\n logLevel: 'warn',\n enableVisibilityManagement: false,\n }\n\n /** WebSocket 客户端实例 */\n private static client: WebSocketClient | null = null\n\n private static readonly logger = new Logger('MessageSocket', MessageSocket.DEFAULT_CONFIG.logLevel)\n\n private static updateLoggerLevel(): void {\n MessageSocket.logger.setLevel(MessageSocket.config.logLevel ?? MessageSocket.DEFAULT_CONFIG.logLevel)\n }\n\n /** 当前用户ID */\n private static currentUserId: string | null = null\n\n /** 当前token */\n private static currentToken: string | null = null\n\n /** 当前配置 */\n private static config: MessageSocketConfig = { ...MessageSocket.DEFAULT_CONFIG }\n\n /** 是否已初始化页面可见性监听 */\n private static visibilityListenerInitialized = false\n\n /**\n * 页面可见性变化处理器\n * 当标签页不可见时断开连接,可见时重新连接\n */\n private static handleVisibilityChange = (): void => {\n if (typeof document === 'undefined') return\n\n const isVisible = !document.hidden\n\n if (!isVisible) {\n // 页面不可见时断开连接,避免多标签页重复连接\n MessageSocket.logger.info('[MessageSocket] 页面不可见,断开连接')\n if (MessageSocket.client) {\n MessageSocket.client.disconnect()\n }\n } else {\n // 页面可见时重新连接\n if (MessageSocket.currentUserId && MessageSocket.currentToken) {\n MessageSocket.logger.info('[MessageSocket] 页面可见,尝试重新连接')\n // 检查是否已经有活跃连接\n if (!MessageSocket.client || !MessageSocket.client.isConnected()) {\n MessageSocket.start({\n userId: MessageSocket.currentUserId,\n token: MessageSocket.currentToken,\n })\n }\n }\n }\n }\n\n /**\n * 初始化页面可见性监听\n */\n private static initVisibilityListener(): void {\n if (typeof document === 'undefined' || MessageSocket.visibilityListenerInitialized) {\n return\n }\n\n document.addEventListener('visibilitychange', MessageSocket.handleVisibilityChange)\n MessageSocket.visibilityListenerInitialized = true\n MessageSocket.logger.debug('[MessageSocket] 已初始化页面可见性监听')\n }\n\n /**\n * 移除页面可见性监听\n */\n private static removeVisibilityListener(): void {\n if (typeof document === 'undefined' || !MessageSocket.visibilityListenerInitialized) {\n return\n }\n\n document.removeEventListener('visibilitychange', MessageSocket.handleVisibilityChange)\n MessageSocket.visibilityListenerInitialized = false\n MessageSocket.logger.debug('[MessageSocket] 已移除页面可见性监听')\n }\n\n /**\n * 配置 MessageSocket\n * @param config 配置选项\n */\n public static configure(config: Partial<MessageSocketConfig>): void {\n MessageSocket.config = { ...MessageSocket.config, ...config }\n }\n\n /**\n * 设置配置\n * @param config 配置选项\n * @returns MessageSocket\n */\n public static setConfig(config: Partial<MessageSocketConfig>): typeof MessageSocket {\n MessageSocket.config = { ...MessageSocket.config, ...config }\n MessageSocket.updateLoggerLevel()\n if (MessageSocket.config.callbacks && MessageSocket.config.callbacks.length > 0) {\n MessageSocket.setCallbacks(MessageSocket.config.callbacks)\n }\n return MessageSocket\n }\n\n /**\n * 设置回调\n * @param callbacks 回调列表\n * @returns MessageSocket\n */\n public static setCallbacks(callbacks: MessageCallbackEntry[]): typeof MessageSocket {\n if (!callbacks || callbacks.length === 0) {\n this.logger.warn('[MessageSocket] 回调列表为空,无法设置回调')\n return MessageSocket\n }\n\n MessageSocket.config.callbacks = callbacks\n return MessageSocket\n }\n\n /**\n * 启动连接\n * @param options 启动选项\n * 回调在开始的时候注册,这种能\n */\n public static start(options: MessageSocketStartOptions): void {\n if (!MessageSocket.config.url) {\n this.logger.error('[MessageSocket] 缺少配置 url, 请先调用 setConfig 设置配置!')\n return\n }\n\n const { userId, token } = options\n\n if (!userId || !token) {\n this.logger.error('[MessageSocket] 缺少 userId 或 token,无法启动')\n return\n }\n\n this.logger.info('[MessageSocket] 开始连接', userId)\n\n // 检查是否可以复用现有连接\n const shouldReuse =\n MessageSocket.client &&\n MessageSocket.client.isConnected() &&\n MessageSocket.currentUserId === userId &&\n MessageSocket.currentToken === token\n\n if (shouldReuse) {\n this.logger.debug('[MessageSocket] 复用现有连接')\n return\n }\n\n // 停止旧连接\n MessageSocket.stop()\n\n // 保存当前用户信息\n MessageSocket.currentUserId = userId\n MessageSocket.currentToken = token\n\n // 构建 WebSocket URL\n const { url, ...clientConfig } = MessageSocket.config\n const targetUrl = `${url}/${userId}?token=${encodeURIComponent(token)}`\n\n // 创建新的 WebSocket 客户端\n MessageSocket.client = new WebSocketClient({\n ...clientConfig,\n url: targetUrl,\n })\n\n if (MessageSocket.config.callbacks && MessageSocket.config.callbacks.length > 0) {\n MessageSocket.config.callbacks.forEach((entry) => MessageSocket.registerCallbacks(entry))\n }\n\n // 初始化页面可见性监听(如果启用)\n if (MessageSocket.config.enableVisibilityManagement) {\n MessageSocket.initVisibilityListener()\n }\n\n // 连接\n MessageSocket.client.connect()\n }\n\n /**\n * 停止连接\n */\n public static stop(): void {\n if (MessageSocket.client) {\n MessageSocket.client.disconnect()\n MessageSocket.client.clearCallbacks()\n MessageSocket.client = null\n }\n\n // 清理页面可见性监听\n MessageSocket.removeVisibilityListener()\n\n MessageSocket.currentUserId = null\n MessageSocket.currentToken = null\n }\n\n /**\n * 注册消息回调\n * @param entry 回调配置\n */\n public static registerCallbacks<T = unknown>(entry: MessageCallbackEntry<T>): void {\n if (!entry) {\n this.logger.warn('[MessageSocket] 注册回调失败,缺少 entry', entry)\n return\n }\n\n if (typeof entry !== 'object') {\n this.logger.warn('[MessageSocket] 注册回调失败,entry 不是对象', entry)\n return\n }\n\n if (typeof entry.callback !== 'function') {\n this.logger.warn('[MessageSocket] 注册回调失败,callback 不是函数', entry)\n return\n }\n\n if (typeof entry.type !== 'string') {\n this.logger.warn('[MessageSocket] 注册回调失败,type 不是字符串', entry)\n return\n }\n\n if (!MessageSocket.client) {\n this.logger.warn('[MessageSocket] WebSocket 客户端未初始化,无法注册回调')\n return\n }\n\n MessageSocket.client.on(entry)\n }\n\n /**\n * 取消注册消息回调\n * @param type 消息类型\n * @param callback 回调函数(可选)\n */\n public static unregisterCallbacks<T = unknown>(type: string, callback?: (data: T, message?: unknown) => void): void {\n if (!MessageSocket.client) {\n return\n }\n\n MessageSocket.client.off(type, callback)\n }\n\n /**\n * 发送消息\n * @param data 消息数据\n */\n public static send(data: string | object): void {\n if (!MessageSocket.client) {\n this.logger.warn('[MessageSocket] WebSocket 客户端未初始化,无法发送消息')\n return\n }\n\n MessageSocket.client.send(data)\n }\n\n /**\n * 获取连接状态\n */\n public static getReadyState(): number {\n return MessageSocket.client?.getReadyState() ?? WebSocket.CLOSED\n }\n\n /**\n * 是否已连接\n */\n public static isConnected(): boolean {\n return MessageSocket.client?.isConnected() ?? false\n }\n\n /**\n * 获取当前用户ID\n */\n public static getCurrentUserId(): string | null {\n return MessageSocket.currentUserId\n }\n\n /**\n * 获取当前token\n */\n public static getCurrentToken(): string | null {\n return MessageSocket.currentToken\n }\n}\n"],"names":["LOG_LEVEL_PRIORITY","debug","info","warn","error","silent","Logger","constructor","name","level","this","setLevel","getLevel","values","logAtLevel","console","shouldLog","writer","WebSocketClient","config","socket","heartbeatTimer","reconnectTimer","callbackList","currentUrl","reconnectAttempts","manualClose","DEFAULT_CONFIG","logger","logLevel","updateConfig","connect","url","targetUrl","WebSocket","onopen","startHeartbeat","onOpen","onmessage","event","handleIncoming","data","onclose","code","reason","stopHeartbeat","onClose","autoReconnect","scheduleReconnect","onerror","onError","maxReconnectAttempts","delay","Math","min","reconnectDelay","reconnectDelayMax","window","setTimeout","disconnect","clearTimeout","close","send","readyState","OPEN","message","JSON","stringify","parse","type","filter","entry","forEach","callback","onMessage","setInterval","heartbeatData","heartbeatMessage","heartbeatInterval","clearInterval","on","typeOrEntry","push","off","clearCallbacks","getReadyState","CLOSED","isConnected","_event","_message","timestamp","Date","now","MessageSocket","updateLoggerLevel","initVisibilityListener","document","visibilityListenerInitialized","addEventListener","handleVisibilityChange","removeVisibilityListener","removeEventListener","configure","setConfig","callbacks","length","setCallbacks","start","options","userId","token","client","currentUserId","currentToken","stop","clientConfig","encodeURIComponent","registerCallbacks","enableVisibilityManagement","unregisterCallbacks","getCurrentUserId","getCurrentToken","hidden"],"mappings":"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"}
|
package/dist/MessageSocket.d.ts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import { WebSocketConfig, MessageCallbackEntry
|
|
1
|
+
import { WebSocketConfig, MessageCallbackEntry } from './WebSocketClient';
|
|
2
|
+
import { LogLevel } from './logger';
|
|
2
3
|
/**
|
|
3
4
|
* MessageSocket 配置选项
|
|
4
5
|
*/
|
|
5
6
|
export interface MessageSocketConfig extends WebSocketConfig {
|
|
6
|
-
/** WebSocket
|
|
7
|
-
|
|
8
|
-
/** WebSocket 路径,默认 '/api/user-web/websocket/messageServer' */
|
|
9
|
-
path?: string;
|
|
7
|
+
/** WebSocket 服务器地址,默认 '' */
|
|
8
|
+
url?: string;
|
|
10
9
|
/** 消息回调列表 */
|
|
11
10
|
callbacks?: MessageCallbackEntry[];
|
|
12
11
|
/** 日志级别 */
|
|
13
12
|
logLevel?: LogLevel;
|
|
13
|
+
/** 是否启用页面可见性管理(标签页切换时自动断开/重连),默认 false */
|
|
14
|
+
enableVisibilityManagement?: boolean;
|
|
14
15
|
}
|
|
15
16
|
/**
|
|
16
17
|
* MessageSocket 启动选项
|
|
@@ -60,25 +61,37 @@ export interface MessageSocketStartOptions {
|
|
|
60
61
|
* }
|
|
61
62
|
* })
|
|
62
63
|
* ```
|
|
64
|
+
* // logLevel 设置日志级别,默认 'warn',可选 'debug', 'info', 'warn', 'error', 'silent'
|
|
65
|
+
*
|
|
63
66
|
*/
|
|
64
67
|
export declare class MessageSocket {
|
|
65
68
|
/** 默认配置 */
|
|
66
69
|
private static readonly DEFAULT_CONFIG;
|
|
67
70
|
/** WebSocket 客户端实例 */
|
|
68
71
|
private static client;
|
|
69
|
-
private static readonly
|
|
70
|
-
private static
|
|
71
|
-
private static log;
|
|
72
|
-
private static logDebug;
|
|
73
|
-
private static logInfo;
|
|
74
|
-
private static logWarn;
|
|
75
|
-
private static logError;
|
|
72
|
+
private static readonly logger;
|
|
73
|
+
private static updateLoggerLevel;
|
|
76
74
|
/** 当前用户ID */
|
|
77
75
|
private static currentUserId;
|
|
78
76
|
/** 当前token */
|
|
79
77
|
private static currentToken;
|
|
80
78
|
/** 当前配置 */
|
|
81
79
|
private static config;
|
|
80
|
+
/** 是否已初始化页面可见性监听 */
|
|
81
|
+
private static visibilityListenerInitialized;
|
|
82
|
+
/**
|
|
83
|
+
* 页面可见性变化处理器
|
|
84
|
+
* 当标签页不可见时断开连接,可见时重新连接
|
|
85
|
+
*/
|
|
86
|
+
private static handleVisibilityChange;
|
|
87
|
+
/**
|
|
88
|
+
* 初始化页面可见性监听
|
|
89
|
+
*/
|
|
90
|
+
private static initVisibilityListener;
|
|
91
|
+
/**
|
|
92
|
+
* 移除页面可见性监听
|
|
93
|
+
*/
|
|
94
|
+
private static removeVisibilityListener;
|
|
82
95
|
/**
|
|
83
96
|
* 配置 MessageSocket
|
|
84
97
|
* @param config 配置选项
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MessageSocket.d.ts","sourceRoot":"","sources":["../src/MessageSocket.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,eAAe,EAAE,oBAAoB,EAAE,QAAQ,
|
|
1
|
+
{"version":3,"file":"MessageSocket.d.ts","sourceRoot":"","sources":["../src/MessageSocket.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,eAAe,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AAC1F,OAAO,EAAU,QAAQ,EAAE,MAAM,UAAU,CAAA;AAE3C;;GAEG;AACH,MAAM,WAAW,mBAAoB,SAAQ,eAAe;IAC1D,4BAA4B;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,aAAa;IACb,SAAS,CAAC,EAAE,oBAAoB,EAAE,CAAA;IAClC,WAAW;IACX,QAAQ,CAAC,EAAE,QAAQ,CAAA;IACnB,0CAA0C;IAC1C,0BAA0B,CAAC,EAAE,OAAO,CAAA;CACrC;AAED;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC,eAAe;IACf,MAAM,EAAE,MAAM,CAAA;IACd,eAAe;IACf,KAAK,EAAE,MAAM,CAAA;CACd;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,qBAAa,aAAa;IACxB,WAAW;IACX,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAcrC;IAED,sBAAsB;IACtB,OAAO,CAAC,MAAM,CAAC,MAAM,CAA+B;IAEpD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAqE;IAEnG,OAAO,CAAC,MAAM,CAAC,iBAAiB;IAIhC,aAAa;IACb,OAAO,CAAC,MAAM,CAAC,aAAa,CAAsB;IAElD,cAAc;IACd,OAAO,CAAC,MAAM,CAAC,YAAY,CAAsB;IAEjD,WAAW;IACX,OAAO,CAAC,MAAM,CAAC,MAAM,CAA2D;IAEhF,oBAAoB;IACpB,OAAO,CAAC,MAAM,CAAC,6BAA6B,CAAQ;IAEpD;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,sBAAsB,CAwBpC;IAED;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAUrC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,wBAAwB;IAUvC;;;OAGG;WACW,SAAS,CAAC,MAAM,EAAE,OAAO,CAAC,mBAAmB,CAAC,GAAG,IAAI;IAInE;;;;OAIG;WACW,SAAS,CAAC,MAAM,EAAE,OAAO,CAAC,mBAAmB,CAAC,GAAG,OAAO,aAAa;IASnF;;;;OAIG;WACW,YAAY,CAAC,SAAS,EAAE,oBAAoB,EAAE,GAAG,OAAO,aAAa;IAUnF;;;;OAIG;WACW,KAAK,CAAC,OAAO,EAAE,yBAAyB,GAAG,IAAI;IAyD7D;;OAEG;WACW,IAAI,IAAI,IAAI;IAc1B;;;OAGG;WACW,iBAAiB,CAAC,CAAC,GAAG,OAAO,EAAE,KAAK,EAAE,oBAAoB,CAAC,CAAC,CAAC,GAAG,IAAI;IA6BlF;;;;OAIG;WACW,mBAAmB,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,OAAO,KAAK,IAAI,GAAG,IAAI;IAQnH;;;OAGG;WACW,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAS/C;;OAEG;WACW,aAAa,IAAI,MAAM;IAIrC;;OAEG;WACW,WAAW,IAAI,OAAO;IAIpC;;OAEG;WACW,gBAAgB,IAAI,MAAM,GAAG,IAAI;IAI/C;;OAEG;WACW,eAAe,IAAI,MAAM,GAAG,IAAI;CAG/C"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e={debug:10,info:20,warn:30,error:40,silent:50};class t{
|
|
1
|
+
const e={debug:10,info:20,warn:30,error:40,silent:50};class t{constructor(e,t="warn"){this.name=e,this.level=t}setLevel(e){this.level=e}getLevel(){return this.level}debug(...e){this.logAtLevel("debug",console.debug,e)}info(...e){this.logAtLevel("info",console.info,e)}warn(...e){this.logAtLevel("warn",console.warn,e)}error(...e){this.logAtLevel("error",console.error,e)}shouldLog(t){return"silent"!==t&&e[t]>=e[this.level]}logAtLevel(e,t,i){this.shouldLog(e)&&t(`[${this.name}]`,...i)}}class i{constructor(e={}){this.socket=null,this.heartbeatTimer=null,this.reconnectTimer=null,this.callbackList=[],this.currentUrl=null,this.reconnectAttempts=0,this.manualClose=!1,this.config={...i.DEFAULT_CONFIG,...e},this.logger=new t("WebSocketClient",this.config.logLevel)}updateConfig(e){this.config={...this.config,...e},this.logger.setLevel(this.config.logLevel??i.DEFAULT_CONFIG.logLevel)}connect(e){const t=e||this.config.url;if(t){this.currentUrl=t,this.manualClose=!1;try{this.socket=new WebSocket(t),this.socket.onopen=()=>{this.logger.info("[WebSocketClient] 连接成功"),this.reconnectAttempts=0,this.startHeartbeat(),this.onOpen()},this.socket.onmessage=e=>{this.handleIncoming(e.data)},this.socket.onclose=e=>{this.logger.info("[WebSocketClient] 连接关闭",e.code,e.reason),this.stopHeartbeat(),this.onClose(e),!this.manualClose&&this.config.autoReconnect&&this.scheduleReconnect()},this.socket.onerror=e=>{this.logger.error("[WebSocketClient] 连接错误",e),this.stopHeartbeat(),this.onError(e)}}catch(e){this.logger.error("[WebSocketClient] 连接失败",e),this.config.autoReconnect&&!this.manualClose&&this.scheduleReconnect()}}else this.logger.error("[WebSocketClient] 缺少 WebSocket URL")}scheduleReconnect(){if(this.reconnectAttempts>=this.config.maxReconnectAttempts||!this.currentUrl||this.manualClose)return void(this.reconnectAttempts>=this.config.maxReconnectAttempts&&this.logger.warn("[WebSocketClient] 已达到最大重连次数"));this.reconnectAttempts+=1;const e=Math.min(this.config.reconnectDelay*this.reconnectAttempts,this.config.reconnectDelayMax);this.logger.debug(`[WebSocketClient] 将在 ${e}ms 后进行第 ${this.reconnectAttempts} 次重连`),this.reconnectTimer=window.setTimeout(()=>{this.connect(this.currentUrl)},e)}disconnect(){this.manualClose=!0,this.stopHeartbeat(),this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.socket&&(this.socket.close(),this.socket=null),this.currentUrl=null,this.reconnectAttempts=0}send(e){if(!this.socket||this.socket.readyState!==WebSocket.OPEN)return void this.logger.warn("[WebSocketClient] WebSocket 未连接,无法发送消息");const t="string"==typeof e?e:JSON.stringify(e);this.socket.send(t)}handleIncoming(e){if(!e)return;let t;try{t=JSON.parse(e)}catch{return void this.logger.warn("[WebSocketClient] 无法解析消息",e)}if(!t?.type)return;this.callbackList.filter(e=>e.type===t.type).forEach(({callback:e})=>{try{e(t.data,t)}catch(e){this.logger.error("[WebSocketClient] 回调执行失败",e)}}),this.onMessage(t)}startHeartbeat(){this.stopHeartbeat(),this.socket&&this.socket.readyState===WebSocket.OPEN&&(this.heartbeatTimer=window.setInterval(()=>{if(!this.socket||this.socket.readyState!==WebSocket.OPEN)return;const e=this.config.heartbeatMessage();this.send(e)},this.config.heartbeatInterval))}stopHeartbeat(){this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null)}on(e,t){const i="string"==typeof e?{type:e,callback:t}:e;i.type&&"function"==typeof i.callback?this.callbackList.push(i):this.logger.warn("[WebSocketClient] 无效的回调配置",i)}off(e,t){this.callbackList=t?this.callbackList.filter(i=>!(i.type===e&&i.callback===t)):this.callbackList.filter(t=>t.type!==e)}clearCallbacks(){this.callbackList=[]}getReadyState(){return this.socket?.readyState??WebSocket.CLOSED}isConnected(){return this.socket?.readyState===WebSocket.OPEN}onOpen(){this.logger.info("[WebSocketClient] 连接打开")}onClose(e){this.logger.info("[WebSocketClient] 连接打开")}onError(e){this.logger.error("[WebSocketClient] 连接错误",e)}onMessage(e){this.logger.debug("[WebSocketClient] 收到消息",e)}}i.DEFAULT_CONFIG={url:"",heartbeatInterval:25e3,maxReconnectAttempts:10,reconnectDelay:3e3,reconnectDelayMax:1e4,autoReconnect:!0,heartbeatMessage:()=>({type:"PING",timestamp:Date.now()}),logLevel:"warn"};class n{static updateLoggerLevel(){n.logger.setLevel(n.config.logLevel??n.DEFAULT_CONFIG.logLevel)}static initVisibilityListener(){"undefined"==typeof document||n.visibilityListenerInitialized||(document.addEventListener("visibilitychange",n.handleVisibilityChange),n.visibilityListenerInitialized=!0,n.logger.debug("[MessageSocket] 已初始化页面可见性监听"))}static removeVisibilityListener(){"undefined"!=typeof document&&n.visibilityListenerInitialized&&(document.removeEventListener("visibilitychange",n.handleVisibilityChange),n.visibilityListenerInitialized=!1,n.logger.debug("[MessageSocket] 已移除页面可见性监听"))}static configure(e){n.config={...n.config,...e}}static setConfig(e){return n.config={...n.config,...e},n.updateLoggerLevel(),n.config.callbacks&&n.config.callbacks.length>0&&n.setCallbacks(n.config.callbacks),n}static setCallbacks(e){return e&&0!==e.length?(n.config.callbacks=e,n):(this.logger.warn("[MessageSocket] 回调列表为空,无法设置回调"),n)}static start(e){if(!n.config.url)return void this.logger.error("[MessageSocket] 缺少配置 url, 请先调用 setConfig 设置配置!");const{userId:t,token:s}=e;if(!t||!s)return void this.logger.error("[MessageSocket] 缺少 userId 或 token,无法启动");this.logger.info("[MessageSocket] 开始连接",t);if(n.client&&n.client.isConnected()&&n.currentUserId===t&&n.currentToken===s)return void this.logger.debug("[MessageSocket] 复用现有连接");n.stop(),n.currentUserId=t,n.currentToken=s;const{url:o,...c}=n.config,r=`${o}/${t}?token=${encodeURIComponent(s)}`;n.client=new i({...c,url:r}),n.config.callbacks&&n.config.callbacks.length>0&&n.config.callbacks.forEach(e=>n.registerCallbacks(e)),n.config.enableVisibilityManagement&&n.initVisibilityListener(),n.client.connect()}static stop(){n.client&&(n.client.disconnect(),n.client.clearCallbacks(),n.client=null),n.removeVisibilityListener(),n.currentUserId=null,n.currentToken=null}static registerCallbacks(e){e?"object"==typeof e?"function"==typeof e.callback?"string"==typeof e.type?n.client?n.client.on(e):this.logger.warn("[MessageSocket] WebSocket 客户端未初始化,无法注册回调"):this.logger.warn("[MessageSocket] 注册回调失败,type 不是字符串",e):this.logger.warn("[MessageSocket] 注册回调失败,callback 不是函数",e):this.logger.warn("[MessageSocket] 注册回调失败,entry 不是对象",e):this.logger.warn("[MessageSocket] 注册回调失败,缺少 entry",e)}static unregisterCallbacks(e,t){n.client&&n.client.off(e,t)}static send(e){n.client?n.client.send(e):this.logger.warn("[MessageSocket] WebSocket 客户端未初始化,无法发送消息")}static getReadyState(){return n.client?.getReadyState()??WebSocket.CLOSED}static isConnected(){return n.client?.isConnected()??!1}static getCurrentUserId(){return n.currentUserId}static getCurrentToken(){return n.currentToken}}n.DEFAULT_CONFIG={url:"",heartbeatInterval:25e3,maxReconnectAttempts:10,reconnectDelay:3e3,reconnectDelayMax:1e4,autoReconnect:!0,callbacks:[],heartbeatMessage:()=>({type:"PING",timestamp:Date.now()}),logLevel:"warn",enableVisibilityManagement:!1},n.client=null,n.logger=new t("MessageSocket",n.DEFAULT_CONFIG.logLevel),n.currentUserId=null,n.currentToken=null,n.config={...n.DEFAULT_CONFIG},n.visibilityListenerInitialized=!1,n.handleVisibilityChange=()=>{if("undefined"==typeof document)return;!document.hidden?n.currentUserId&&n.currentToken&&(n.logger.info("[MessageSocket] 页面可见,尝试重新连接"),n.client&&n.client.isConnected()||n.start({userId:n.currentUserId,token:n.currentToken})):(n.logger.info("[MessageSocket] 页面不可见,断开连接"),n.client&&n.client.disconnect())};export{n as MessageSocket};
|
|
2
2
|
//# sourceMappingURL=MessageSocket.esm.js.map
|