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.
- package/README.md +66 -29
- package/dist/cjs/index.js +1025 -0
- package/dist/{index.js → esm/index.js} +2 -1
- package/dist/esm/index.js.map +1 -0
- package/package.json +17 -8
- package/dist/index.js.map +0 -1
- /package/dist/{index.d.ts → esm/index.d.ts} +0 -0
|
@@ -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;
|