easyjssdk 1.0.15 → 1.1.0

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.
@@ -0,0 +1,1025 @@
1
+ "use strict";
2
+ // import { v4 as uuidv4 } from 'uuid'; // Remove this line
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.WKIMDeviceFlag = exports.WKIMEvent = exports.WKIMChannelType = exports.WKIM = exports.DeviceFlag = exports.ReasonCode = exports.Event = exports.ChannelType = exports.currentPlatform = exports.PlatformType = void 0;
5
+ // WebSocket readyState constants
6
+ const WS_CONNECTING = 0;
7
+ const WS_OPEN = 1;
8
+ const WS_CLOSING = 2;
9
+ const WS_CLOSED = 3;
10
+ // --- Platform Type Enum ---
11
+ var PlatformType;
12
+ (function (PlatformType) {
13
+ PlatformType["Browser"] = "browser";
14
+ PlatformType["NodeJS"] = "nodejs";
15
+ PlatformType["WeChat"] = "wechat";
16
+ PlatformType["Alipay"] = "alipay";
17
+ PlatformType["UniApp"] = "uniapp";
18
+ })(PlatformType || (exports.PlatformType = PlatformType = {}));
19
+ // --- WeChat Mini Program WebSocket Adapter ---
20
+ class WeChatWebSocketAdapter {
21
+ constructor(url) {
22
+ this.socketTask = null;
23
+ this._readyState = WS_CONNECTING;
24
+ this.onopen = null;
25
+ this.onmessage = null;
26
+ this.onerror = null;
27
+ this.onclose = null;
28
+ if (typeof wx === 'undefined') {
29
+ throw new Error('WeChat Mini Program environment not detected');
30
+ }
31
+ this.socketTask = wx.connectSocket({
32
+ url: url,
33
+ success: () => {
34
+ console.log('WeChat WebSocket connecting...');
35
+ },
36
+ fail: (err) => {
37
+ console.error('WeChat WebSocket connection failed:', err);
38
+ this._readyState = WS_CLOSED;
39
+ if (this.onerror) {
40
+ this.onerror({ message: err.errMsg || 'Connection failed' });
41
+ }
42
+ }
43
+ });
44
+ this.socketTask.onOpen((res) => {
45
+ this._readyState = WS_OPEN;
46
+ if (this.onopen) {
47
+ this.onopen(res);
48
+ }
49
+ });
50
+ this.socketTask.onMessage((res) => {
51
+ if (this.onmessage) {
52
+ const data = res.data instanceof ArrayBuffer
53
+ ? new TextDecoder().decode(res.data)
54
+ : res.data;
55
+ this.onmessage({ data });
56
+ }
57
+ });
58
+ this.socketTask.onError((res) => {
59
+ console.error('WeChat WebSocket error:', res);
60
+ if (this.onerror) {
61
+ this.onerror({ message: res.errMsg || 'WebSocket error' });
62
+ }
63
+ });
64
+ this.socketTask.onClose((res) => {
65
+ this._readyState = WS_CLOSED;
66
+ if (this.onclose) {
67
+ this.onclose({ code: res.code || 1000, reason: res.reason || '' });
68
+ }
69
+ });
70
+ }
71
+ get readyState() {
72
+ return this._readyState;
73
+ }
74
+ send(data) {
75
+ if (this._readyState !== WS_OPEN || !this.socketTask) {
76
+ throw new Error('WebSocket is not open');
77
+ }
78
+ this.socketTask.send({
79
+ data: data,
80
+ fail: (err) => {
81
+ console.error('WeChat WebSocket send failed:', err);
82
+ if (this.onerror) {
83
+ this.onerror({ message: 'Send failed' });
84
+ }
85
+ }
86
+ });
87
+ }
88
+ close(code, reason) {
89
+ if (this._readyState === WS_CLOSED || this._readyState === WS_CLOSING) {
90
+ return;
91
+ }
92
+ this._readyState = WS_CLOSING;
93
+ if (this.socketTask) {
94
+ this.socketTask.close({
95
+ code: code || 1000,
96
+ reason: reason || '',
97
+ fail: (err) => {
98
+ console.error('WeChat WebSocket close failed:', err);
99
+ }
100
+ });
101
+ }
102
+ }
103
+ }
104
+ // --- Alipay Mini Program WebSocket Adapter ---
105
+ class AlipayWebSocketAdapter {
106
+ constructor(url) {
107
+ this._readyState = WS_CONNECTING;
108
+ this.boundOnOpen = null;
109
+ this.boundOnMessage = null;
110
+ this.boundOnError = null;
111
+ this.boundOnClose = null;
112
+ this.onopen = null;
113
+ this.onmessage = null;
114
+ this.onerror = null;
115
+ this.onclose = null;
116
+ if (typeof my === 'undefined') {
117
+ throw new Error('Alipay Mini Program environment not detected');
118
+ }
119
+ // Bind event handlers
120
+ this.boundOnOpen = (res) => {
121
+ this._readyState = WS_OPEN;
122
+ if (this.onopen) {
123
+ this.onopen(res);
124
+ }
125
+ };
126
+ this.boundOnMessage = (res) => {
127
+ if (this.onmessage) {
128
+ const data = res.data instanceof ArrayBuffer
129
+ ? new TextDecoder().decode(res.data)
130
+ : res.data;
131
+ this.onmessage({ data });
132
+ }
133
+ };
134
+ this.boundOnError = (res) => {
135
+ console.error('Alipay WebSocket error:', res);
136
+ if (this.onerror) {
137
+ this.onerror({ message: res.errorMessage || 'WebSocket error' });
138
+ }
139
+ };
140
+ this.boundOnClose = (res) => {
141
+ this._readyState = WS_CLOSED;
142
+ this.cleanup();
143
+ if (this.onclose) {
144
+ this.onclose({ code: res.code || 1000, reason: res.reason || '' });
145
+ }
146
+ };
147
+ // Register global event listeners
148
+ my.onSocketOpen(this.boundOnOpen);
149
+ my.onSocketMessage(this.boundOnMessage);
150
+ my.onSocketError(this.boundOnError);
151
+ my.onSocketClose(this.boundOnClose);
152
+ // Connect
153
+ my.connectSocket({
154
+ url: url,
155
+ success: () => {
156
+ console.log('Alipay WebSocket connecting...');
157
+ },
158
+ fail: (err) => {
159
+ console.error('Alipay WebSocket connection failed:', err);
160
+ this._readyState = WS_CLOSED;
161
+ this.cleanup();
162
+ if (this.onerror) {
163
+ this.onerror({ message: err.errorMessage || 'Connection failed' });
164
+ }
165
+ }
166
+ });
167
+ }
168
+ get readyState() {
169
+ return this._readyState;
170
+ }
171
+ send(data) {
172
+ if (typeof my === 'undefined') {
173
+ throw new Error('Alipay Mini Program environment not detected');
174
+ }
175
+ if (this._readyState !== WS_OPEN) {
176
+ throw new Error('WebSocket is not open');
177
+ }
178
+ my.sendSocketMessage({
179
+ data: data,
180
+ fail: (err) => {
181
+ console.error('Alipay WebSocket send failed:', err);
182
+ if (this.onerror) {
183
+ this.onerror({ message: 'Send failed' });
184
+ }
185
+ }
186
+ });
187
+ }
188
+ close(code, reason) {
189
+ if (typeof my === 'undefined') {
190
+ return;
191
+ }
192
+ if (this._readyState === WS_CLOSED || this._readyState === WS_CLOSING) {
193
+ return;
194
+ }
195
+ this._readyState = WS_CLOSING;
196
+ my.closeSocket({
197
+ code: code || 1000,
198
+ reason: reason || '',
199
+ fail: (err) => {
200
+ console.error('Alipay WebSocket close failed:', err);
201
+ }
202
+ });
203
+ }
204
+ cleanup() {
205
+ if (typeof my !== 'undefined') {
206
+ my.offSocketOpen(this.boundOnOpen);
207
+ my.offSocketMessage(this.boundOnMessage);
208
+ my.offSocketError(this.boundOnError);
209
+ my.offSocketClose(this.boundOnClose);
210
+ this.boundOnOpen = null;
211
+ this.boundOnMessage = null;
212
+ this.boundOnError = null;
213
+ this.boundOnClose = null;
214
+ }
215
+ }
216
+ }
217
+ // --- UniApp WebSocket Adapter ---
218
+ class UniAppWebSocketAdapter {
219
+ constructor(url) {
220
+ this.socketTask = null;
221
+ this._readyState = WS_CONNECTING;
222
+ this.onopen = null;
223
+ this.onmessage = null;
224
+ this.onerror = null;
225
+ this.onclose = null;
226
+ if (typeof uni === 'undefined') {
227
+ throw new Error('UniApp environment not detected');
228
+ }
229
+ this.socketTask = uni.connectSocket({
230
+ url: url,
231
+ success: () => {
232
+ console.log('UniApp WebSocket connecting...');
233
+ },
234
+ fail: (err) => {
235
+ console.error('UniApp WebSocket connection failed:', err);
236
+ this._readyState = WS_CLOSED;
237
+ if (this.onerror) {
238
+ this.onerror({ message: err.errMsg || 'Connection failed' });
239
+ }
240
+ }
241
+ });
242
+ this.socketTask.onOpen((res) => {
243
+ this._readyState = WS_OPEN;
244
+ if (this.onopen) {
245
+ this.onopen(res);
246
+ }
247
+ });
248
+ this.socketTask.onMessage((res) => {
249
+ if (this.onmessage) {
250
+ const data = res.data instanceof ArrayBuffer
251
+ ? new TextDecoder().decode(res.data)
252
+ : res.data;
253
+ this.onmessage({ data });
254
+ }
255
+ });
256
+ this.socketTask.onError((res) => {
257
+ console.error('UniApp WebSocket error:', res);
258
+ if (this.onerror) {
259
+ this.onerror({ message: res.errMsg || 'WebSocket error' });
260
+ }
261
+ });
262
+ this.socketTask.onClose((res) => {
263
+ this._readyState = WS_CLOSED;
264
+ if (this.onclose) {
265
+ this.onclose({ code: res.code || 1000, reason: res.reason || '' });
266
+ }
267
+ });
268
+ }
269
+ get readyState() {
270
+ return this._readyState;
271
+ }
272
+ send(data) {
273
+ if (this._readyState !== WS_OPEN || !this.socketTask) {
274
+ throw new Error('WebSocket is not open');
275
+ }
276
+ this.socketTask.send({
277
+ data: data,
278
+ fail: (err) => {
279
+ console.error('UniApp WebSocket send failed:', err);
280
+ if (this.onerror) {
281
+ this.onerror({ message: 'Send failed' });
282
+ }
283
+ }
284
+ });
285
+ }
286
+ close(code, reason) {
287
+ if (this._readyState === WS_CLOSED || this._readyState === WS_CLOSING) {
288
+ return;
289
+ }
290
+ this._readyState = WS_CLOSING;
291
+ if (this.socketTask) {
292
+ this.socketTask.close({
293
+ code: code || 1000,
294
+ reason: reason || '',
295
+ fail: (err) => {
296
+ console.error('UniApp WebSocket close failed:', err);
297
+ }
298
+ });
299
+ }
300
+ }
301
+ }
302
+ // --- Platform Detection and WebSocket Factory ---
303
+ function detectPlatform() {
304
+ // Check for UniApp first (it may also have wx defined in WeChat mini program mode)
305
+ if (typeof uni !== 'undefined' && typeof uni.connectSocket === 'function') {
306
+ return PlatformType.UniApp;
307
+ }
308
+ // Check for WeChat Mini Program
309
+ if (typeof wx !== 'undefined' && typeof wx.connectSocket === 'function') {
310
+ return PlatformType.WeChat;
311
+ }
312
+ // Check for Alipay Mini Program
313
+ if (typeof my !== 'undefined' && typeof my.connectSocket === 'function') {
314
+ return PlatformType.Alipay;
315
+ }
316
+ // Check for browser WebSocket
317
+ if (typeof WebSocket !== 'undefined') {
318
+ return PlatformType.Browser;
319
+ }
320
+ // Fallback to Node.js
321
+ return PlatformType.NodeJS;
322
+ }
323
+ const currentPlatform = detectPlatform();
324
+ exports.currentPlatform = currentPlatform;
325
+ // Factory function to create platform-appropriate WebSocket
326
+ function createWebSocket(url) {
327
+ switch (currentPlatform) {
328
+ case PlatformType.UniApp:
329
+ return new UniAppWebSocketAdapter(url);
330
+ case PlatformType.WeChat:
331
+ return new WeChatWebSocketAdapter(url);
332
+ case PlatformType.Alipay:
333
+ return new AlipayWebSocketAdapter(url);
334
+ case PlatformType.Browser:
335
+ return new WebSocket(url);
336
+ case PlatformType.NodeJS:
337
+ default:
338
+ try {
339
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
340
+ const dynamicRequire = new Function('mod', 'return require(mod)');
341
+ const Ws = dynamicRequire('ws');
342
+ const WsImpl = Ws.WebSocket || Ws;
343
+ return new WsImpl(url);
344
+ }
345
+ catch (e) {
346
+ throw new Error('WebSocket is not available in this environment. Install \'ws\' package for Node.js.');
347
+ }
348
+ }
349
+ }
350
+ // --- Enums and Types ---
351
+ /**
352
+ * Channel Type Enum based on WuKongIM protocol
353
+ */
354
+ var ChannelType;
355
+ (function (ChannelType) {
356
+ /** Person channel */
357
+ ChannelType[ChannelType["Person"] = 1] = "Person";
358
+ /** Group channel */
359
+ ChannelType[ChannelType["Group"] = 2] = "Group";
360
+ /** Customer Service channel (Consider using Visitors channel instead) */
361
+ ChannelType[ChannelType["CustomerService"] = 3] = "CustomerService";
362
+ /** Community channel */
363
+ ChannelType[ChannelType["Community"] = 4] = "Community";
364
+ /** Community Topic channel */
365
+ ChannelType[ChannelType["CommunityTopic"] = 5] = "CommunityTopic";
366
+ /** Info channel (with concept of temporary subscribers) */
367
+ ChannelType[ChannelType["Info"] = 6] = "Info";
368
+ /** Data channel */
369
+ ChannelType[ChannelType["Data"] = 7] = "Data";
370
+ /** Temporary channel */
371
+ ChannelType[ChannelType["Temp"] = 8] = "Temp";
372
+ /** Live channel (does not save recent session data) */
373
+ ChannelType[ChannelType["Live"] = 9] = "Live";
374
+ /** Visitors channel (replaces CustomerService for new implementations) */
375
+ ChannelType[ChannelType["Visitors"] = 10] = "Visitors";
376
+ })(ChannelType || (exports.WKIMChannelType = exports.ChannelType = ChannelType = {}));
377
+ /**
378
+ * SDK Event Names Enum
379
+ */
380
+ var Event;
381
+ (function (Event) {
382
+ /** Connection successfully established and authenticated */
383
+ Event["Connect"] = "connect";
384
+ /** Disconnected from server */
385
+ Event["Disconnect"] = "disconnect";
386
+ /** Received a message */
387
+ Event["Message"] = "message";
388
+ /** An error occurred (WebSocket error, connection error, etc.) */
389
+ Event["Error"] = "error";
390
+ /** Received acknowledgment for a sent message */
391
+ Event["SendAck"] = "sendack";
392
+ /** The SDK is attempting to reconnect */
393
+ Event["Reconnecting"] = "reconnecting";
394
+ /** Received a custom event notification from the server */
395
+ Event["CustomEvent"] = "customevent";
396
+ })(Event || (exports.WKIMEvent = exports.Event = Event = {}));
397
+ /**
398
+ * Reason codes for operation results and disconnect causes.
399
+ * Mirrors the server-side ReasonCode (Go) values; do not reorder to preserve numeric mapping.
400
+ */
401
+ var ReasonCode;
402
+ (function (ReasonCode) {
403
+ /** Unknown error */
404
+ ReasonCode[ReasonCode["Unknown"] = 0] = "Unknown";
405
+ /** Success */
406
+ ReasonCode[ReasonCode["Success"] = 1] = "Success";
407
+ /** Authentication failed */
408
+ ReasonCode[ReasonCode["AuthFail"] = 2] = "AuthFail";
409
+ /** Subscriber does not exist in the channel */
410
+ ReasonCode[ReasonCode["SubscriberNotExist"] = 3] = "SubscriberNotExist";
411
+ /** In blacklist */
412
+ ReasonCode[ReasonCode["InBlacklist"] = 4] = "InBlacklist";
413
+ /** Channel does not exist */
414
+ ReasonCode[ReasonCode["ChannelNotExist"] = 5] = "ChannelNotExist";
415
+ /** User not on node */
416
+ ReasonCode[ReasonCode["UserNotOnNode"] = 6] = "UserNotOnNode";
417
+ /** Sender is offline; message cannot be delivered */
418
+ ReasonCode[ReasonCode["SenderOffline"] = 7] = "SenderOffline";
419
+ /** Message key error; the message is invalid */
420
+ ReasonCode[ReasonCode["MsgKeyError"] = 8] = "MsgKeyError";
421
+ /** Payload decode failed */
422
+ ReasonCode[ReasonCode["PayloadDecodeError"] = 9] = "PayloadDecodeError";
423
+ /** Forwarding send packet failed */
424
+ ReasonCode[ReasonCode["ForwardSendPacketError"] = 10] = "ForwardSendPacketError";
425
+ /** Not allowed to send */
426
+ ReasonCode[ReasonCode["NotAllowSend"] = 11] = "NotAllowSend";
427
+ /** Connection kicked */
428
+ ReasonCode[ReasonCode["ConnectKick"] = 12] = "ConnectKick";
429
+ /** Not in whitelist */
430
+ ReasonCode[ReasonCode["NotInWhitelist"] = 13] = "NotInWhitelist";
431
+ /** Failed to query user token */
432
+ ReasonCode[ReasonCode["QueryTokenError"] = 14] = "QueryTokenError";
433
+ /** System error */
434
+ ReasonCode[ReasonCode["SystemError"] = 15] = "SystemError";
435
+ /** Invalid channel ID */
436
+ ReasonCode[ReasonCode["ChannelIDError"] = 16] = "ChannelIDError";
437
+ /** Node matching error */
438
+ ReasonCode[ReasonCode["NodeMatchError"] = 17] = "NodeMatchError";
439
+ /** Node not matched */
440
+ ReasonCode[ReasonCode["NodeNotMatch"] = 18] = "NodeNotMatch";
441
+ /** Channel is banned */
442
+ ReasonCode[ReasonCode["Ban"] = 19] = "Ban";
443
+ /** Unsupported header */
444
+ ReasonCode[ReasonCode["NotSupportHeader"] = 20] = "NotSupportHeader";
445
+ /** clientKey is empty */
446
+ ReasonCode[ReasonCode["ClientKeyIsEmpty"] = 21] = "ClientKeyIsEmpty";
447
+ /** Rate limit */
448
+ ReasonCode[ReasonCode["RateLimit"] = 22] = "RateLimit";
449
+ /** Unsupported channel type */
450
+ ReasonCode[ReasonCode["NotSupportChannelType"] = 23] = "NotSupportChannelType";
451
+ /** Channel disbanded */
452
+ ReasonCode[ReasonCode["Disband"] = 24] = "Disband";
453
+ /** Sending is banned */
454
+ ReasonCode[ReasonCode["SendBan"] = 25] = "SendBan";
455
+ })(ReasonCode || (exports.ReasonCode = ReasonCode = {}));
456
+ /**
457
+ * Device Flag Enum based on WuKongIM protocol
458
+ */
459
+ var DeviceFlag;
460
+ (function (DeviceFlag) {
461
+ /** Mobile APP */
462
+ DeviceFlag[DeviceFlag["App"] = 0] = "App";
463
+ /** Web Browser */
464
+ DeviceFlag[DeviceFlag["Web"] = 1] = "Web";
465
+ /** Desktop App */
466
+ DeviceFlag[DeviceFlag["Desktop"] = 2] = "Desktop";
467
+ })(DeviceFlag || (exports.WKIMDeviceFlag = exports.DeviceFlag = DeviceFlag = {}));
468
+ // --- WKIM Class ---
469
+ class WKIM {
470
+ constructor(url, auth) {
471
+ this.ws = null;
472
+ this.isConnected = false;
473
+ this.connectionPromise = null;
474
+ this.pingInterval = null;
475
+ this.pingTimeout = null;
476
+ this.PING_INTERVAL_MS = 25 * 1000; // Send ping every 25 seconds
477
+ this.PONG_TIMEOUT_MS = 10 * 1000; // Expect pong within 10 seconds
478
+ this.pendingRequests = new Map();
479
+ this.eventListeners = new Map();
480
+ // Reconnection properties
481
+ this.reconnectAttempts = 0;
482
+ this.maxReconnectAttempts = 5;
483
+ this.initialReconnectDelay = 1000;
484
+ this.isReconnecting = false;
485
+ this.manualDisconnect = false;
486
+ this.beforeUnloadHandler = null;
487
+ this.url = url;
488
+ this.auth = auth || {};
489
+ this.sessionId = this.generateUUID(); // Unique session identifier
490
+ // Ensure unique deviceId for each session
491
+ if (!this.auth.deviceId || this.auth.deviceId === '') {
492
+ this.auth.deviceId = `web_${this.sessionId.slice(0, 8)}_${Date.now()}`;
493
+ }
494
+ // Ensure Event enum values are used for internal map keys
495
+ Object.values(Event).forEach(event => this.eventListeners.set(event, []));
496
+ // Setup beforeunload handler to cleanup connection on page refresh/close
497
+ this.setupBeforeUnloadHandler();
498
+ }
499
+ /**
500
+ * Initializes the WKIM instance.
501
+ * @param url WebSocket server URL (e.g., "ws://localhost:5100")
502
+ * @param auth Authentication options { uid, token, ... }
503
+ * @param options Configuration options { singleton: boolean }
504
+ * @returns A WKIM instance
505
+ */
506
+ static init(url, auth, options = {}) {
507
+ if (!url || !auth || !auth.uid || !auth.token) {
508
+ throw new Error("URL, uid, and token are required for initialization.");
509
+ }
510
+ // If singleton mode is enabled and there's an existing instance, disconnect it first
511
+ if (options.singleton && WKIM.globalInstance) {
512
+ console.log("Destroying previous global instance...");
513
+ WKIM.globalInstance.destroy();
514
+ }
515
+ const instance = new WKIM(url, auth);
516
+ if (options.singleton !== false) {
517
+ WKIM.globalInstance = instance;
518
+ }
519
+ return instance;
520
+ }
521
+ /**
522
+ * Establishes connection and authenticates with the server.
523
+ * Returns a Promise that resolves on successful connection/authentication,
524
+ * or rejects on failure.
525
+ */
526
+ connect() {
527
+ return new Promise((resolve, reject) => {
528
+ var _a;
529
+ if (this.isConnected || ((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WS_CONNECTING) {
530
+ console.warn("Connection already established or in progress.");
531
+ // If already connected, resolve immediately. If connecting, wait for existing promise.
532
+ if (this.isConnected) {
533
+ resolve();
534
+ }
535
+ else if (this.connectionPromise) {
536
+ this.connectionPromise.resolve = resolve; // Chain the promises
537
+ this.connectionPromise.reject = reject;
538
+ }
539
+ else {
540
+ reject(new Error("Already connecting, but no connection promise found."));
541
+ }
542
+ return;
543
+ }
544
+ // On a new connect call, reset manual disconnect flag
545
+ this.manualDisconnect = false;
546
+ this.connectionPromise = { resolve, reject };
547
+ try {
548
+ console.log(`Connecting to ${this.url}... (Platform: ${currentPlatform})`);
549
+ this.ws = createWebSocket(this.url);
550
+ this.ws.onopen = () => {
551
+ console.log("WebSocket connection opened. Authenticating...");
552
+ this.sendConnectRequest();
553
+ };
554
+ this.ws.onmessage = (event) => {
555
+ this.handleMessage(event.data);
556
+ };
557
+ this.ws.onerror = (event) => {
558
+ const errorMessage = event.message || (event.error ? event.error.message : 'WebSocket error');
559
+ console.error("WebSocket error:", errorMessage, event);
560
+ this.emit(Event.Error, event.error || new Error(errorMessage));
561
+ // The 'onclose' event will be fired next, which will handle cleanup and reconnection logic.
562
+ };
563
+ this.ws.onclose = (event) => {
564
+ const wasConnected = this.isConnected;
565
+ console.log(`WebSocket connection closed. Code: ${event.code}, Reason: ${event.reason}`);
566
+ if (this.connectionPromise && !this.isConnected) { // Reject connect promise if closed before connect ack
567
+ this.connectionPromise.reject(new Error(`Connection closed before authentication (Code: ${event.code})`));
568
+ }
569
+ this.cleanupConnection(); // Clean up state like intervals, pending requests.
570
+ this.emit(Event.Disconnect, { code: event.code, reason: event.reason });
571
+ // Only try to reconnect if we were previously connected and it wasn't a manual disconnect.
572
+ if (wasConnected && !this.manualDisconnect) {
573
+ this.tryReconnect();
574
+ }
575
+ };
576
+ }
577
+ catch (error) {
578
+ console.error("Failed to create WebSocket:", error);
579
+ this.emit(Event.Error, error);
580
+ if (this.connectionPromise) {
581
+ this.connectionPromise.reject(error);
582
+ this.connectionPromise = null;
583
+ }
584
+ this.cleanupConnection();
585
+ }
586
+ });
587
+ }
588
+ /**
589
+ * Disconnects from the server.
590
+ */
591
+ disconnect() {
592
+ console.log("Manual disconnect initiated.");
593
+ this.manualDisconnect = true;
594
+ this.isReconnecting = false; // Stop any ongoing reconnection attempts
595
+ this.cleanupBeforeUnloadHandler(); // Remove page unload listeners
596
+ this.handleDisconnect(true, "Manual disconnection");
597
+ }
598
+ /**
599
+ * Completely destroys the SDK instance, cleaning up all resources.
600
+ * Call this when you no longer need the SDK instance.
601
+ */
602
+ destroy() {
603
+ console.log("Destroying SDK instance...");
604
+ this.disconnect();
605
+ this.eventListeners.clear();
606
+ this.pendingRequests.clear();
607
+ // Clear global instance if this is it
608
+ if (WKIM.globalInstance === this) {
609
+ WKIM.globalInstance = null;
610
+ }
611
+ }
612
+ /**
613
+ * Sends a message to a specific channel.
614
+ * @param channelId Target channel ID
615
+ * @param channelType Target channel type (e.g., WKIM.ChannelType.Person)
616
+ * @param payload Message payload (must be a JSON-serializable object)
617
+ * @param options Optional: { clientMsgNo, header, setting, msgKey, expire, topic }
618
+ * @returns Promise resolving with { messageId, messageSeq } on server ack, or rejecting on error.
619
+ */
620
+ send(channelId, channelType, payload, options = {}) {
621
+ if (!this.isConnected || !this.ws || this.ws.readyState !== WS_OPEN) {
622
+ return Promise.reject(new Error("Not connected. Call connect() first."));
623
+ }
624
+ if (typeof payload !== 'object' || payload === null) {
625
+ return Promise.reject(new Error("Payload must be a non-null object."));
626
+ }
627
+ const header = options.header || {};
628
+ header.redDot = true;
629
+ const clientMsgNo = options.clientMsgNo || this.generateUUID(); // Generate a unique message ID if not provided
630
+ const params = {
631
+ clientMsgNo: clientMsgNo,
632
+ channelId: channelId,
633
+ channelType: channelType,
634
+ payload: payload,
635
+ header: header,
636
+ topic: options.topic,
637
+ setting: options.setting,
638
+ };
639
+ return this.sendRequest('send', params);
640
+ }
641
+ /**
642
+ * Registers an event listener.
643
+ * @param eventName The event to listen for (e.g., WKIM.Event.Message)
644
+ * @param callback The function to call when the event occurs
645
+ */
646
+ on(eventName, callback) {
647
+ var _a;
648
+ if (this.eventListeners.has(eventName)) {
649
+ (_a = this.eventListeners.get(eventName)) === null || _a === void 0 ? void 0 : _a.push(callback);
650
+ }
651
+ else {
652
+ console.warn(`Attempted to register listener for unknown event: ${eventName}`);
653
+ }
654
+ }
655
+ /**
656
+ * Removes an event listener.
657
+ * @param eventName The event to stop listening for
658
+ * @param callback The specific callback function to remove
659
+ */
660
+ off(eventName, callback) {
661
+ if (this.eventListeners.has(eventName)) {
662
+ const listeners = this.eventListeners.get(eventName);
663
+ if (listeners) {
664
+ const index = listeners.indexOf(callback);
665
+ if (index > -1) {
666
+ listeners.splice(index, 1);
667
+ }
668
+ }
669
+ }
670
+ }
671
+ // --- Private Methods ---
672
+ emit(eventName, ...args) {
673
+ const listeners = this.eventListeners.get(eventName);
674
+ if (listeners) {
675
+ listeners.forEach(callback => {
676
+ try {
677
+ callback(...args);
678
+ }
679
+ catch (error) {
680
+ console.error(`Error in event listener for ${eventName}:`, error);
681
+ }
682
+ });
683
+ }
684
+ }
685
+ generateUUID() {
686
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
687
+ const r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
688
+ return v.toString(16);
689
+ });
690
+ }
691
+ sendConnectRequest() {
692
+ var _a;
693
+ const params = {
694
+ uid: this.auth.uid,
695
+ token: this.auth.token,
696
+ deviceId: this.auth.deviceId,
697
+ deviceFlag: (_a = this.auth.deviceFlag) !== null && _a !== void 0 ? _a : DeviceFlag.Web, // Default to WEB
698
+ clientTimestamp: Date.now(),
699
+ // Add version, clientKey if needed
700
+ };
701
+ this.sendRequest('connect', params, 5000) // 5s timeout for connect
702
+ .then(result => {
703
+ console.log("Authentication successful:", result);
704
+ this.isConnected = true;
705
+ // Reset reconnection state on successful connect
706
+ this.reconnectAttempts = 0;
707
+ this.isReconnecting = false;
708
+ this.manualDisconnect = false;
709
+ this.startPing();
710
+ this.emit(Event.Connect, result);
711
+ if (this.connectionPromise) {
712
+ this.connectionPromise.resolve();
713
+ this.connectionPromise = null;
714
+ }
715
+ })
716
+ .catch(error => {
717
+ console.error("Authentication failed:", error);
718
+ this.emit(Event.Error, new Error(`Authentication failed: ${error.message || JSON.stringify(error)}`));
719
+ if (this.connectionPromise) {
720
+ this.connectionPromise.reject(error);
721
+ this.connectionPromise = null;
722
+ }
723
+ // Don't start reconnection on auth failure, it's a permanent error.
724
+ this.handleDisconnect(false, "Authentication failed"); // Close connection on auth failure
725
+ this.cleanupConnection(); // Ensure ws is closed
726
+ });
727
+ }
728
+ sendRequest(method, params, timeoutMs = 15000) {
729
+ return new Promise((resolve, reject) => {
730
+ if (!this.ws || this.ws.readyState !== WS_OPEN) {
731
+ return reject(new Error("WebSocket is not open."));
732
+ }
733
+ const requestId = this.generateUUID(); // Generate a unique request ID
734
+ const request = {
735
+ method: method,
736
+ params: params,
737
+ id: requestId
738
+ };
739
+ const timeoutTimer = setTimeout(() => {
740
+ this.pendingRequests.delete(requestId);
741
+ reject(new Error(`Request timeout for method ${method} (id: ${requestId})`));
742
+ }, timeoutMs);
743
+ this.pendingRequests.set(requestId, { resolve, reject, timeoutTimer });
744
+ try {
745
+ console.debug(`--> Sending request (id: ${requestId}):`, JSON.stringify(request));
746
+ this.ws.send(JSON.stringify(request));
747
+ }
748
+ catch (error) {
749
+ clearTimeout(timeoutTimer);
750
+ this.pendingRequests.delete(requestId);
751
+ console.error(`Error sending request (id: ${requestId}):`, error);
752
+ reject(error);
753
+ }
754
+ });
755
+ }
756
+ sendNotification(method, params) {
757
+ if (!this.ws || this.ws.readyState !== WS_OPEN) {
758
+ console.error("Cannot send notification, WebSocket is not open.");
759
+ return;
760
+ }
761
+ const notification = {
762
+ method: method,
763
+ params: params
764
+ };
765
+ console.debug(`--> Sending notification:`, JSON.stringify(notification));
766
+ try {
767
+ this.ws.send(JSON.stringify(notification));
768
+ }
769
+ catch (error) {
770
+ console.error(`Error sending notification (${method}):`, error);
771
+ this.emit(Event.Error, new Error(`Failed to send notification ${method}: ${error}`));
772
+ }
773
+ }
774
+ handleMessage(data) {
775
+ console.debug("<-- Received raw:", data);
776
+ let message;
777
+ try {
778
+ message = JSON.parse(data.toString());
779
+ }
780
+ catch (error) {
781
+ console.error("Failed to parse incoming message:", error, data);
782
+ this.emit(Event.Error, new Error(`Failed to parse message: ${error}`));
783
+ return;
784
+ }
785
+ if ('id' in message) { // It's a Response
786
+ this.handleResponse(message);
787
+ }
788
+ else if ('method' in message) { // It's a Notification
789
+ this.handleNotification(message);
790
+ }
791
+ else {
792
+ console.warn("Received unknown message format:", message);
793
+ }
794
+ }
795
+ handleResponse(response) {
796
+ console.debug(`<-- Handling response (id: ${response.id}):`, response);
797
+ const pending = this.pendingRequests.get(response.id);
798
+ if (pending) {
799
+ clearTimeout(pending.timeoutTimer);
800
+ this.pendingRequests.delete(response.id);
801
+ if (response.error) {
802
+ pending.reject(response.error);
803
+ }
804
+ else {
805
+ pending.resolve(response.result);
806
+ }
807
+ }
808
+ else {
809
+ console.warn(`Received response for unknown request ID: ${response.id}`);
810
+ }
811
+ }
812
+ handleNotification(notification) {
813
+ var _a, _b;
814
+ console.debug(`<-- Handling notification (${notification.method}):`, notification.params);
815
+ switch (notification.method) {
816
+ case 'recv':
817
+ const messageData = notification.params;
818
+ this.emit(Event.Message, messageData);
819
+ // Automatically acknowledge receipt
820
+ this.sendRecvAck(messageData.header, messageData.messageId, messageData.messageSeq);
821
+ break;
822
+ case 'pong':
823
+ this.handlePong();
824
+ break;
825
+ case 'disconnect':
826
+ console.warn('Server initiated disconnect:', notification.params);
827
+ this.emit(Event.Disconnect, notification.params); // Emit server reason
828
+ this.handleDisconnect(false, `Server disconnected: ${((_a = notification.params) === null || _a === void 0 ? void 0 : _a.reason) || ((_b = notification.params) === null || _b === void 0 ? void 0 : _b.reasonCode)}`); // Close locally
829
+ break;
830
+ case 'event':
831
+ this.handleEventNotification(notification.params);
832
+ break;
833
+ // Handle other notifications if needed
834
+ default:
835
+ console.warn(`Received unhandled notification method: ${notification.method}`);
836
+ }
837
+ }
838
+ sendRecvAck(header, messageId, messageSeq) {
839
+ // Per protocol, recvack is a request, but usually doesn't need a response processed
840
+ // Sending as a notification might be simpler if the server allows it,
841
+ // but sticking to the doc: send as request, ignore response.
842
+ const params = { header, messageId, messageSeq };
843
+ this.sendNotification('recvack', params);
844
+ // Alternative: Send as notification if server supports it (non-standard)
845
+ // this.sendNotification('recvack', params);
846
+ }
847
+ /**
848
+ * Handles incoming event notifications from the server
849
+ * @param params Event notification parameters
850
+ */
851
+ handleEventNotification(params) {
852
+ try {
853
+ const eventData = {
854
+ header: params.header,
855
+ id: params.id,
856
+ type: params.type,
857
+ timestamp: params.timestamp,
858
+ data: params.data
859
+ };
860
+ // Validate required fields
861
+ if (!eventData.id || !eventData.type) {
862
+ console.error('Invalid event notification: missing required fields', params);
863
+ this.emit(Event.Error, new Error('Invalid event notification: missing required fields'));
864
+ return;
865
+ }
866
+ // Try to parse data if it's a JSON string
867
+ if (typeof eventData.data === 'string') {
868
+ try {
869
+ eventData.data = JSON.parse(eventData.data);
870
+ }
871
+ catch (e) {
872
+ // Keep as string if not valid JSON
873
+ console.debug('Event data is not JSON, keeping as string');
874
+ }
875
+ }
876
+ console.log(`Event notification received: type=${eventData.type}, id=${eventData.id}`);
877
+ // Emit the custom event to registered listeners
878
+ this.emit(Event.CustomEvent, eventData);
879
+ }
880
+ catch (error) {
881
+ console.error('Error handling event notification:', error);
882
+ this.emit(Event.Error, new Error(`Failed to handle event notification: ${error}`));
883
+ }
884
+ }
885
+ startPing() {
886
+ this.stopPing(); // Clear existing timers
887
+ this.pingInterval = setInterval(() => {
888
+ if (this.ws && this.ws.readyState === WS_OPEN) {
889
+ this.sendRequest('ping', {}, this.PONG_TIMEOUT_MS)
890
+ .then(this.handlePong.bind(this)) // Technically pong is a notification, but use req/res for timeout
891
+ .catch(err => {
892
+ console.error("Ping failed or timed out:", err);
893
+ this.emit(Event.Error, new Error(`Ping timeout: ${(err === null || err === void 0 ? void 0 : err.message) || err}`));
894
+ // Treat ping timeout as an unhealthy connection: close and reconnect
895
+ if (!this.manualDisconnect) {
896
+ this.handleDisconnect(false, "Ping timeout");
897
+ this.tryReconnect();
898
+ }
899
+ });
900
+ }
901
+ else {
902
+ this.stopPing(); // Stop if WS is not open
903
+ }
904
+ }, this.PING_INTERVAL_MS);
905
+ console.log(`Ping interval started (${this.PING_INTERVAL_MS}ms).`);
906
+ }
907
+ stopPing() {
908
+ if (this.pingInterval) {
909
+ clearInterval(this.pingInterval);
910
+ this.pingInterval = null;
911
+ console.log("Ping interval stopped.");
912
+ }
913
+ if (this.pingTimeout) {
914
+ clearTimeout(this.pingTimeout);
915
+ this.pingTimeout = null;
916
+ }
917
+ }
918
+ handlePong() {
919
+ // console.debug("Pong received.");
920
+ // Reset pong timeout if using one (mainly handled by sendRequest timeout now)
921
+ }
922
+ handleDisconnect(graceful, reason) {
923
+ console.log(`Handling disconnect. Graceful: ${graceful}, Reason: ${reason}`);
924
+ if (this.ws) {
925
+ this.stopPing();
926
+ if (graceful && this.ws.readyState === WS_OPEN) {
927
+ // Use standard close codes: 1000 (normal), 3000-4999 (custom)
928
+ this.ws.close(1000, "Client disconnected"); // Normal closure
929
+ }
930
+ else if (this.ws.readyState === WS_CONNECTING || this.ws.readyState === WS_OPEN) {
931
+ // Force close with custom code for abnormal situations
932
+ this.ws.close(3001, reason.substring(0, 123)); // Custom code, limit reason length
933
+ }
934
+ }
935
+ this.cleanupConnection(); // Clean up state regardless of how close happened
936
+ }
937
+ cleanupConnection() {
938
+ console.log("Cleaning up connection resources.");
939
+ this.isConnected = false;
940
+ this.stopPing();
941
+ // Reject any pending requests
942
+ this.pendingRequests.forEach((pending) => {
943
+ clearTimeout(pending.timeoutTimer);
944
+ pending.reject(new Error("Connection closed"));
945
+ });
946
+ this.pendingRequests.clear();
947
+ // Clear connection promise if it exists and hasn't resolved/rejected
948
+ if (this.connectionPromise) {
949
+ // Only reject if we're not in a reconnection loop that will try again.
950
+ if (!this.isReconnecting && !this.isConnected) {
951
+ this.connectionPromise.reject(new Error("Connection closed during operation"));
952
+ }
953
+ this.connectionPromise = null;
954
+ }
955
+ // Don't nullify ws immediately if onclose handler needs it, but ensure no further ops
956
+ if (this.ws) {
957
+ // Remove listeners to prevent potential memory leaks and duplicate handling
958
+ this.ws.onopen = null;
959
+ this.ws.onmessage = null;
960
+ this.ws.onerror = null;
961
+ this.ws.onclose = null;
962
+ // Consider setting this.ws = null here or after a short delay if needed
963
+ }
964
+ // Do NOT clear eventListeners here, user might want to reconnect.
965
+ }
966
+ // --- Reconnection Methods ---
967
+ setupBeforeUnloadHandler() {
968
+ // Only setup in browser environment
969
+ if (typeof window !== 'undefined') {
970
+ this.beforeUnloadHandler = () => {
971
+ console.log('Page unloading, closing WebSocket connection...');
972
+ this.manualDisconnect = true;
973
+ this.isReconnecting = false;
974
+ if (this.ws && this.ws.readyState === WS_OPEN) {
975
+ this.ws.close(1000, 'Page unloaded');
976
+ }
977
+ };
978
+ window.addEventListener('beforeunload', this.beforeUnloadHandler);
979
+ window.addEventListener('pagehide', this.beforeUnloadHandler);
980
+ }
981
+ }
982
+ cleanupBeforeUnloadHandler() {
983
+ if (typeof window !== 'undefined' && this.beforeUnloadHandler) {
984
+ window.removeEventListener('beforeunload', this.beforeUnloadHandler);
985
+ window.removeEventListener('pagehide', this.beforeUnloadHandler);
986
+ this.beforeUnloadHandler = null;
987
+ }
988
+ }
989
+ tryReconnect() {
990
+ if (this.isReconnecting || this.manualDisconnect) {
991
+ return;
992
+ }
993
+ // The onclose event handler should have already called cleanupConnection.
994
+ this.isReconnecting = true;
995
+ this.scheduleReconnect();
996
+ }
997
+ scheduleReconnect() {
998
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
999
+ console.error("Max reconnect attempts reached. Giving up.");
1000
+ this.isReconnecting = false;
1001
+ this.reconnectAttempts = 0;
1002
+ this.emit(Event.Error, new Error("Reconnection failed."));
1003
+ return;
1004
+ }
1005
+ const delay = this.initialReconnectDelay * Math.pow(2, this.reconnectAttempts);
1006
+ this.reconnectAttempts++;
1007
+ console.log(`Will attempt to reconnect in ${delay / 1000}s (Attempt ${this.reconnectAttempts}).`);
1008
+ this.emit(Event.Reconnecting, { attempt: this.reconnectAttempts, delay });
1009
+ setTimeout(() => {
1010
+ // Check if a manual disconnect happened while waiting
1011
+ if (!this.isReconnecting) {
1012
+ console.log("Reconnection aborted.");
1013
+ return;
1014
+ }
1015
+ this.connect().catch(() => {
1016
+ // connect() rejects if it fails. Schedule the next attempt.
1017
+ if (this.isReconnecting) {
1018
+ this.scheduleReconnect();
1019
+ }
1020
+ });
1021
+ }, delay);
1022
+ }
1023
+ }
1024
+ exports.WKIM = WKIM;
1025
+ WKIM.globalInstance = null;