agentgui 1.0.820 → 1.0.822

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.
@@ -1,642 +1,107 @@
1
-
2
- class WebSocketManager {
3
- constructor(config = {}) {
4
- this.config = {
5
- url: config.url || this.getWebSocketURL(),
6
- reconnectDelays: config.reconnectDelays || [500, 1000, 2000, 4000, 8000, 15000, 30000],
7
- maxReconnectDelay: config.maxReconnectDelay || 30000,
8
- heartbeatInterval: config.heartbeatInterval || 15000,
9
- messageTimeout: config.messageTimeout || 60000,
10
- maxBufferedMessages: config.maxBufferedMessages || 1000,
11
- pongTimeout: config.pongTimeout || 5000,
12
- latencyWindowSize: config.latencyWindowSize || 10,
13
- ...config
14
- };
15
-
16
- this.ws = null;
17
- this._isConnected = false;
18
- this._isConnecting = false;
19
- this._isManuallyDisconnected = false;
20
- this._reconnectCount = 0;
21
- this.isManuallyDisconnected = false;
22
- this.reconnectCount = 0;
23
- this.reconnectTimer = null;
24
- this.messageBuffer = [];
25
- this.requestMap = new Map();
26
- this.heartbeatTimer = null;
27
- this._connectionState = 'disconnected';
28
- this.activeSubscriptions = new Set();
29
- this._wsActor = typeof createWsActor === 'function' ? createWsActor() : null;
30
- this.connectionEstablishedAt = 0;
31
- this.cachedVoiceList = null;
32
- this.voiceListListeners = [];
33
-
34
- this.latency = {
35
- samples: [],
36
- current: 0,
37
- avg: 0,
38
- jitter: 0,
39
- quality: 'unknown',
40
- predicted: 0,
41
- predictedNext: 0,
42
- trend: 'stable',
43
- missedPongs: 0,
44
- pingCounter: 0
45
- };
46
-
47
- this._latencyEma = null;
48
- this._trendHistory = [];
49
- this._trendCount = 0;
50
- this._reconnectedAt = 0;
51
-
52
- this.stats = {
53
- totalConnections: 0,
54
- totalReconnects: 0,
55
- totalMessagesSent: 0,
56
- totalMessagesReceived: 0,
57
- totalErrors: 0,
58
- totalTimeouts: 0,
59
- avgLatency: 0,
60
- lastConnectedTime: null,
61
- connectionDuration: 0
62
- };
63
-
64
- this.lastSeqBySession = {};
65
- this.listeners = {};
66
-
67
- this._onVisibilityChange = this._handleVisibilityChange.bind(this);
68
- this._onOnline = this._handleOnline.bind(this);
69
- if (typeof document !== 'undefined') {
70
- document.addEventListener('visibilitychange', this._onVisibilityChange);
71
- }
72
- if (typeof window !== 'undefined') {
73
- window.addEventListener('online', this._onOnline);
74
- }
75
- }
76
-
77
- get isConnected() {
78
- if (this._wsActor) return this._wsActor.getSnapshot().value === 'connected';
79
- return this._isConnected;
80
- }
81
- set isConnected(v) { this._isConnected = v; }
82
-
83
- get isConnecting() {
84
- if (this._wsActor) return this._wsActor.getSnapshot().value === 'connecting';
85
- return this._isConnecting;
86
- }
87
- set isConnecting(v) { this._isConnecting = v; }
88
-
89
- get connectionState() {
90
- if (this._wsActor) return this._wsActor.getSnapshot().value;
91
- return this._connectionState;
92
- }
93
- set connectionState(v) { this._connectionState = v; }
94
-
95
- get isManuallyDisconnected() {
96
- if (this._wsActor) return !!this._wsActor.getSnapshot().context.manualDisconnect;
97
- return this._isManuallyDisconnected;
98
- }
99
- set isManuallyDisconnected(v) {
100
- this._isManuallyDisconnected = v;
101
- if (this._wsActor && v) this._wsActor.send({ type: 'MANUAL_DISCONNECT' });
102
- }
103
-
104
- get reconnectCount() {
105
- if (this._wsActor) return this._wsActor.getSnapshot().context.reconnectCount;
106
- return this._reconnectCount;
107
- }
108
- set reconnectCount(v) { this._reconnectCount = v; }
109
-
110
- getWebSocketURL() {
111
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
112
- const baseURL = window.__BASE_URL || '/gm';
113
- let url = `${protocol}//${window.location.host}${baseURL}/sync`;
114
- if (window.__WS_TOKEN) url += `?token=${encodeURIComponent(window.__WS_TOKEN)}`;
115
- return url;
116
- }
117
-
118
- async connect() {
119
- if (this.isConnected || this.isConnecting) return this.ws;
120
- this.isManuallyDisconnected = false;
121
- this._isConnecting = true;
122
- if (this._wsActor) this._wsActor.send({ type: 'CONNECT' });
123
- this.setConnectionState('connecting');
124
-
125
- try {
126
- this.ws = new WebSocket(this.config.url);
127
- this.ws.binaryType = 'arraybuffer';
128
- this.ws.onopen = () => this.onOpen();
129
- this.ws.onmessage = (event) => this.onMessage(event);
130
- this.ws.onerror = (error) => this.onError(error);
131
- this.ws.onclose = () => this.onClose();
132
- return await this.waitForConnection(this.config.messageTimeout);
133
- } catch (error) {
134
- this.isConnecting = false;
135
- this.stats.totalErrors++;
136
- this.scheduleReconnect();
137
- throw error;
138
- }
139
- }
140
-
141
- waitForConnection(timeout = 5000) {
142
- return new Promise((resolve, reject) => {
143
- const timer = setTimeout(() => reject(new Error('WebSocket connection timeout')), timeout);
144
- const check = () => {
145
- if (this.isConnected || this.ws?.readyState === WebSocket.OPEN) {
146
- clearTimeout(timer);
147
- resolve(this.ws);
148
- } else {
149
- setTimeout(check, 50);
150
- }
151
- };
152
- check();
153
- });
154
- }
155
-
156
- onOpen() {
157
- this._isConnected = true;
158
- this._isConnecting = false;
159
- if (this._wsActor) this._wsActor.send({ type: 'OPEN' });
160
- this.connectionEstablishedAt = Date.now();
161
- this._reconnectedAt = this.stats.totalConnections > 0 ? Date.now() : 0;
162
- this.stats.totalConnections++;
163
- this.stats.lastConnectedTime = Date.now();
164
- this.latency.missedPongs = 0;
165
- this.setConnectionState('connected');
166
-
167
- this.flushMessageBuffer();
168
- this.resubscribeAll();
169
- this.startHeartbeat();
170
-
171
- this.emit('connected', { timestamp: Date.now() });
172
- }
173
-
174
- async onMessage(event) {
175
- try {
176
- let parsed;
177
- const buf = event.data instanceof Blob ? await event.data.arrayBuffer() : event.data;
178
- parsed = window._codec ? window._codec.decode(buf) : msgpackr.unpack(new Uint8Array(buf));
179
- const messages = Array.isArray(parsed) ? parsed : [parsed];
180
- this.stats.totalMessagesReceived += messages.length;
181
-
182
- for (const data of messages) {
183
- if (data.type === 'pong') {
184
- this._handlePong(data);
185
- continue;
186
- }
187
-
188
- if (data.type === 'voice_list') {
189
- this.cachedVoiceList = data.voices || [];
190
- for (const listener of this.voiceListListeners) {
191
- try { listener(this.cachedVoiceList); } catch (_) {}
192
- }
193
- }
194
-
195
- if (data.seq !== undefined && data.sessionId) {
196
- this.lastSeqBySession[data.sessionId] = Math.max(
197
- this.lastSeqBySession[data.sessionId] || -1, data.seq
198
- );
199
- }
200
-
201
- if (data.r !== undefined && !data.type) {
202
- this.emit('message', data);
203
- continue;
204
- }
205
- this.emit('message', data);
206
- if (data.type) this.emit('message:' + data.type, data);
207
- }
208
- } catch (error) {
209
- this.stats.totalErrors++;
210
- }
211
- }
212
-
213
- _handlePong(data) {
214
- this.latency.missedPongs = 0;
215
- const requestId = data.requestId;
216
- if (requestId && this.requestMap.has(requestId)) {
217
- const request = this.requestMap.get(requestId);
218
- const rtt = Date.now() - request.sentTime;
219
- this.requestMap.delete(requestId);
220
- this._recordLatency(rtt);
221
- if (request.resolve) request.resolve({ latency: rtt });
222
- }
223
- }
224
-
225
- _recordLatency(rtt) {
226
- const samples = this.latency.samples;
227
- samples.push(rtt);
228
- if (samples.length > this.config.latencyWindowSize) samples.shift();
229
-
230
- this.latency.current = rtt;
231
-
232
- const alpha = 0.2;
233
- this._latencyEma = this._latencyEma === null ? rtt : alpha * rtt + (1 - alpha) * this._latencyEma;
234
- this.latency.avg = this._latencyEma;
235
- this.latency.predicted = this._latencyEma;
236
- this.latency.predictedNext = this._latencyEma;
237
-
238
- if (samples.length > 1) {
239
- const mean = samples.reduce((a, b) => a + b, 0) / samples.length;
240
- const variance = samples.reduce((sum, s) => sum + Math.pow(s - mean, 2), 0) / samples.length;
241
- this.latency.jitter = Math.sqrt(variance);
242
- }
243
-
244
- this._trendHistory.push(this.latency.predicted);
245
- if (this._trendHistory.length > 3) this._trendHistory.shift();
246
- if (this._trendHistory.length >= 3) {
247
- const [a, b, c] = this._trendHistory;
248
- const rising = b > a * 1.05 && c > b * 1.05;
249
- const falling = b < a * 0.95 && c < b * 0.95;
250
- this.latency.trend = rising ? 'rising' : falling ? 'falling' : 'stable';
251
- }
252
-
253
- this.latency.quality = this._qualityTier(this.latency.avg);
254
- this.stats.avgLatency = this.latency.avg;
255
-
256
- this.emit('latency_update', {
257
- latency: rtt,
258
- avg: this.latency.avg,
259
- predicted: this.latency.predicted,
260
- predictedNext: this.latency.predictedNext,
261
- trend: this.latency.trend,
262
- jitter: this.latency.jitter,
263
- quality: this.latency.quality
264
- });
265
-
266
- if (rtt > this.latency.avg * 3 && samples.length >= 3) {
267
- this.emit('latency_spike', { latency: rtt, avg: this.latency.avg });
268
- }
269
-
270
- this.emit('latency_prediction', {
271
- predicted: this.latency.predicted,
272
- predictedNext: this.latency.predictedNext,
273
- trend: this.latency.trend,
274
- gain: 0
275
- });
276
-
277
- this._checkDegradation();
278
- }
279
-
280
- _checkDegradation() {
281
- if (this.latency.trend === 'rising') {
282
- this._trendCount = (this._trendCount || 0) + 1;
283
- } else {
284
- if (this._trendCount >= 5 && (this.latency.trend === 'stable' || this.latency.trend === 'falling')) {
285
- this.emit('connection_recovering', { currentTier: this.latency.quality });
286
- }
287
- this._trendCount = 0;
288
- return;
289
- }
290
- if (this._trendCount < 5) return;
291
- const currentTier = this.latency.quality;
292
- const predictedTier = this._qualityTier(this.latency.predictedNext);
293
- if (predictedTier === currentTier) return;
294
- const thresholds = { excellent: 50, good: 150, fair: 300, poor: 500 };
295
- const threshold = thresholds[currentTier];
296
- if (!threshold) return;
297
- const rate = this._trendHistory.length >= 2 ? this._trendHistory[this._trendHistory.length - 1] - this._trendHistory[0] : 0;
298
- const timeToChange = rate > 0 ? Math.round((threshold - this.latency.predicted) / rate * 1000) : Infinity;
299
- this.emit('connection_degrading', { currentTier, predictedTier, predictedLatency: this.latency.predictedNext, timeToChange });
300
- }
301
-
302
- _qualityTier(avg) {
303
- if (avg < 50) return 'excellent';
304
- if (avg < 150) return 'good';
305
- if (avg < 300) return 'fair';
306
- if (avg < 500) return 'poor';
307
- return 'bad';
308
- }
309
-
310
- onError(error) {
311
- this.stats.totalErrors++;
312
- if (this._wsActor) this._wsActor.send({ type: 'ERROR' });
313
- this.emit('error', { error, timestamp: Date.now() });
314
- }
315
-
316
- onClose() {
317
- this._isConnected = false;
318
- this._isConnecting = false;
319
- if (this._wsActor) this._wsActor.send({ type: 'CLOSE' });
320
- this.setConnectionState('disconnected');
321
- this.stopHeartbeat();
322
-
323
- if (this.stats.lastConnectedTime) {
324
- this.stats.connectionDuration = Date.now() - this.stats.lastConnectedTime;
325
- }
326
-
327
- this.emit('disconnected', { timestamp: Date.now() });
328
-
329
- if (!this.isManuallyDisconnected) {
330
- this.scheduleReconnect();
331
- }
332
- }
333
-
334
- scheduleReconnect() {
335
- if (this.isManuallyDisconnected) return;
336
- if (this.reconnectTimer) return;
337
-
338
- const delays = this.config.reconnectDelays;
339
- const baseDelay = this.reconnectCount < delays.length
340
- ? delays[this.reconnectCount]
341
- : this.config.maxReconnectDelay;
342
-
343
- const jitter = Math.random() * 0.3 * baseDelay;
344
- const delay = Math.round(baseDelay + jitter);
345
-
346
- this.reconnectCount++;
347
- this.stats.totalReconnects++;
348
- this.setConnectionState('reconnecting');
349
-
350
- this.emit('reconnecting', {
351
- delay,
352
- attempt: this.reconnectCount,
353
- nextAttemptAt: Date.now() + delay
354
- });
355
-
356
- this.reconnectTimer = setTimeout(() => {
357
- this.reconnectTimer = null;
358
- this.connect().catch(() => {});
359
- }, delay);
360
- }
361
-
362
- startHeartbeat() {
363
- this.stopHeartbeat();
364
- const tick = () => {
365
- if (!this.isConnected) return;
366
- if (typeof document !== 'undefined' && document.hidden) {
367
- this.heartbeatTimer = setTimeout(tick, this.config.heartbeatInterval);
368
- return;
369
- }
370
- this.latency.pingCounter++;
371
- this.ping().catch(() => {
372
- this.latency.missedPongs++;
373
- if (this.latency.missedPongs >= 3) {
374
- this.latency.missedPongs = 0;
375
- if (this.ws) {
376
- try { this.ws.close(); } catch (_) {}
377
- }
378
- }
379
- });
380
- if (this.latency.pingCounter % 10 === 0) {
381
- this._reportLatency();
382
- }
383
- this.heartbeatTimer = setTimeout(tick, this.config.heartbeatInterval);
384
- };
385
- this.heartbeatTimer = setTimeout(tick, this.config.heartbeatInterval);
386
- }
387
-
388
- stopHeartbeat() {
389
- if (this.heartbeatTimer) {
390
- clearTimeout(this.heartbeatTimer);
391
- this.heartbeatTimer = null;
392
- }
393
- }
394
-
395
- _reportLatency() {
396
- if (this.latency.avg > 0) {
397
- this.sendMessage({
398
- type: 'latency_report',
399
- avg: Math.round(this.latency.avg),
400
- jitter: Math.round(this.latency.jitter),
401
- quality: this.latency.quality,
402
- trend: this.latency.trend,
403
- predictedNext: Math.round(this.latency.predictedNext)
404
- });
405
- }
406
- }
407
-
408
- _handleVisibilityChange() {
409
- if (typeof document !== 'undefined' && document.hidden) {
410
- this._hiddenAt = Date.now();
411
- return;
412
- }
413
- if (this._hiddenAt && Date.now() - this._hiddenAt > 30000) {
414
- this._latencyEma = null;
415
- this._trendHistory = [];
416
- this.latency.trend = 'stable';
417
- }
418
- this._hiddenAt = 0;
419
- if (!this.isConnected && !this.isConnecting && !this.isManuallyDisconnected) {
420
- if (this.reconnectTimer) {
421
- clearTimeout(this.reconnectTimer);
422
- this.reconnectTimer = null;
423
- }
424
- this.connect().catch(() => {});
425
- }
426
- if (this.isConnected) {
427
- const stableFor = Date.now() - this.connectionEstablishedAt;
428
- if (stableFor > 10000) this.reconnectCount = 0;
429
- }
430
- }
431
-
432
- _handleOnline() {
433
- if (!this.isConnected && !this.isConnecting && !this.isManuallyDisconnected) {
434
- if (this.reconnectTimer) {
435
- clearTimeout(this.reconnectTimer);
436
- this.reconnectTimer = null;
437
- }
438
- this.connect().catch(() => {});
439
- }
440
- }
441
-
442
- ping() {
443
- const requestId = 'ping-' + Date.now() + '-' + Math.random();
444
- const request = { sentTime: Date.now(), resolve: null };
445
-
446
- const promise = new Promise((resolve, reject) => {
447
- request.resolve = resolve;
448
- setTimeout(() => {
449
- if (this.requestMap.has(requestId)) {
450
- this.stats.totalTimeouts++;
451
- this.requestMap.delete(requestId);
452
- reject(new Error('ping timeout'));
453
- }
454
- }, this.config.pongTimeout);
455
- });
456
-
457
- this.requestMap.set(requestId, request);
458
- this.sendMessage({ type: 'ping', requestId });
459
- return promise;
460
- }
461
-
462
- sendMessage(data) {
463
- if (!data || typeof data !== 'object') throw new Error('Invalid message data');
464
-
465
- if (data.type === 'subscribe') {
466
- const key = data.sessionId ? 'session:' + data.sessionId : 'conv:' + data.conversationId;
467
- this.activeSubscriptions.add(key);
468
- } else if (data.type === 'unsubscribe') {
469
- const key = data.sessionId ? 'session:' + data.sessionId : 'conv:' + data.conversationId;
470
- this.activeSubscriptions.delete(key);
471
- }
472
-
473
- if (!this.isConnected) {
474
- this.bufferMessage(data);
475
- return false;
476
- }
477
-
478
- try {
479
- this.ws.send(window._codec ? window._codec.encode(data) : msgpackr.pack(data));
480
- this.stats.totalMessagesSent++;
481
- return true;
482
- } catch (error) {
483
- this.stats.totalErrors++;
484
- this.bufferMessage(data);
485
- return false;
486
- }
487
- }
488
-
489
- bufferMessage(data) {
490
- if (this.messageBuffer.length >= this.config.maxBufferedMessages) {
491
- this.messageBuffer.shift();
492
- }
493
- this.messageBuffer.push(data);
494
- this.emit('message_buffered', { bufferLength: this.messageBuffer.length });
495
- }
496
-
497
- flushMessageBuffer() {
498
- if (this.messageBuffer.length === 0) return;
499
- const messages = [...this.messageBuffer];
500
- this.messageBuffer = [];
501
- for (const message of messages) {
502
- try {
503
- this.ws.send(window._codec ? window._codec.encode(message) : msgpackr.pack(message));
504
- this.stats.totalMessagesSent++;
505
- } catch (error) {
506
- this.bufferMessage(message);
507
- }
508
- }
509
- this.emit('buffer_flushed', { count: messages.length });
510
- }
511
-
512
- subscribeToSession(sessionId) {
513
- return this.sendMessage({ type: 'subscribe', sessionId, timestamp: Date.now() });
514
- }
515
-
516
- subscribeToConversation(conversationId) {
517
- return this.sendMessage({ type: 'subscribe', conversationId, timestamp: Date.now() });
518
- }
519
-
520
- resubscribeAll() {
521
- for (const key of this.activeSubscriptions) {
522
- const colonIdx = key.indexOf(':');
523
- const type = key.substring(0, colonIdx);
524
- const id = key.substring(colonIdx + 1);
525
- const msg = { type: 'subscribe', timestamp: Date.now() };
526
- if (type === 'session') msg.sessionId = id;
527
- else msg.conversationId = id;
528
- try {
529
- this.ws.send(window._codec ? window._codec.encode(msg) : msgpackr.pack(msg));
530
- this.stats.totalMessagesSent++;
531
- } catch (_) {}
532
- }
533
- }
534
-
535
- unsubscribeFromSession(sessionId) {
536
- return this.sendMessage({ type: 'unsubscribe', sessionId, timestamp: Date.now() });
537
- }
538
-
539
- requestSessionHistory(sessionId, limit = 1000, offset = 0) {
540
- return new Promise((resolve, reject) => {
541
- const requestId = 'history-' + Date.now() + '-' + Math.random();
542
- const timeout = setTimeout(() => {
543
- this.requestMap.delete(requestId);
544
- this.stats.totalTimeouts++;
545
- reject(new Error('History request timeout'));
546
- }, this.config.messageTimeout);
547
-
548
- this.requestMap.set(requestId, {
549
- type: 'history',
550
- resolve: (d) => { clearTimeout(timeout); resolve(d); },
551
- reject
552
- });
553
-
554
- this.sendMessage({
555
- type: 'request_history', requestId, sessionId, limit, offset, timestamp: Date.now()
556
- });
557
- });
558
- }
559
-
560
- getLastSeq(sessionId) {
561
- return this.lastSeqBySession[sessionId] || -1;
562
- }
563
-
564
- setConnectionState(state) {
565
- this.connectionState = state;
566
- this.emit('state_change', { state, timestamp: Date.now() });
567
- }
568
-
569
- disconnect() {
570
- this.isManuallyDisconnected = true;
571
- this.reconnectCount = 0;
572
- this.stopHeartbeat();
573
- if (this.reconnectTimer) {
574
- clearTimeout(this.reconnectTimer);
575
- this.reconnectTimer = null;
576
- }
577
- if (this.ws) this.ws.close();
578
- this.messageBuffer = [];
579
- this.requestMap.clear();
580
- this.setConnectionState('disconnected');
581
- }
582
-
583
- getStatus() {
584
- return {
585
- isConnected: this.isConnected,
586
- isConnecting: this.isConnecting,
587
- connectionState: this.connectionState,
588
- reconnectCount: this.reconnectCount,
589
- bufferLength: this.messageBuffer.length,
590
- latency: { ...this.latency, samples: undefined },
591
- stats: { ...this.stats }
592
- };
593
- }
594
-
595
- on(event, callback) {
596
- if (!this.listeners[event]) this.listeners[event] = [];
597
- this.listeners[event].push(callback);
598
- }
599
-
600
- off(event, callback) {
601
- if (!this.listeners[event]) return;
602
- const index = this.listeners[event].indexOf(callback);
603
- if (index > -1) this.listeners[event].splice(index, 1);
604
- }
605
-
606
- emit(event, data) {
607
- if (!this.listeners[event]) return;
608
- this.listeners[event].forEach((cb) => {
609
- try { cb(data); } catch (error) {}
610
- });
611
- }
612
-
613
- destroy() {
614
- if (typeof document !== 'undefined') {
615
- document.removeEventListener('visibilitychange', this._onVisibilityChange);
616
- }
617
- if (typeof window !== 'undefined') {
618
- window.removeEventListener('online', this._onOnline);
619
- }
620
- this.disconnect();
621
- this.listeners = {};
622
- }
623
- subscribeToVoiceList(callback) {
624
- if (!this.voiceListListeners.includes(callback)) {
625
- this.voiceListListeners.push(callback);
626
- }
627
- if (this.cachedVoiceList !== null) {
628
- callback(this.cachedVoiceList);
629
- }
630
- }
631
-
632
- unsubscribeFromVoiceList(callback) {
633
- const idx = this.voiceListListeners.indexOf(callback);
634
- if (idx > -1) {
635
- this.voiceListListeners.splice(idx, 1);
636
- }
637
- }
638
- }
639
-
640
- if (typeof module !== 'undefined' && module.exports) {
641
- module.exports = WebSocketManager;
642
- }
1
+ Object.assign(WebSocketManager.prototype, {
2
+ sendMessage(data) {
3
+ if (!data || typeof data !== 'object') throw new Error('Invalid message data');
4
+ if (data.type === 'subscribe') {
5
+ const key = data.sessionId ? 'session:' + data.sessionId : 'conv:' + data.conversationId;
6
+ this.activeSubscriptions.add(key);
7
+ } else if (data.type === 'unsubscribe') {
8
+ const key = data.sessionId ? 'session:' + data.sessionId : 'conv:' + data.conversationId;
9
+ this.activeSubscriptions.delete(key);
10
+ }
11
+ if (!this.isConnected) { this.bufferMessage(data); return false; }
12
+ try {
13
+ this.ws.send(window._codec ? window._codec.encode(data) : msgpackr.pack(data));
14
+ this.stats.totalMessagesSent++;
15
+ return true;
16
+ } catch (error) { this.stats.totalErrors++; this.bufferMessage(data); return false; }
17
+ },
18
+
19
+ bufferMessage(data) {
20
+ if (this.messageBuffer.length >= this.config.maxBufferedMessages) this.messageBuffer.shift();
21
+ this.messageBuffer.push(data);
22
+ this.emit('message_buffered', { bufferLength: this.messageBuffer.length });
23
+ },
24
+
25
+ flushMessageBuffer() {
26
+ if (this.messageBuffer.length === 0) return;
27
+ const messages = [...this.messageBuffer];
28
+ this.messageBuffer = [];
29
+ for (const message of messages) {
30
+ try {
31
+ this.ws.send(window._codec ? window._codec.encode(message) : msgpackr.pack(message));
32
+ this.stats.totalMessagesSent++;
33
+ } catch (error) { this.bufferMessage(message); }
34
+ }
35
+ this.emit('buffer_flushed', { count: messages.length });
36
+ },
37
+
38
+ subscribeToSession(sessionId) { return this.sendMessage({ type: 'subscribe', sessionId, timestamp: Date.now() }); },
39
+ subscribeToConversation(conversationId) { return this.sendMessage({ type: 'subscribe', conversationId, timestamp: Date.now() }); },
40
+
41
+ resubscribeAll() {
42
+ for (const key of this.activeSubscriptions) {
43
+ const colonIdx = key.indexOf(':');
44
+ const type = key.substring(0, colonIdx);
45
+ const id = key.substring(colonIdx + 1);
46
+ const msg = { type: 'subscribe', timestamp: Date.now() };
47
+ if (type === 'session') msg.sessionId = id; else msg.conversationId = id;
48
+ try { this.ws.send(window._codec ? window._codec.encode(msg) : msgpackr.pack(msg)); this.stats.totalMessagesSent++; } catch (_) {}
49
+ }
50
+ },
51
+
52
+ unsubscribeFromSession(sessionId) { return this.sendMessage({ type: 'unsubscribe', sessionId, timestamp: Date.now() }); },
53
+
54
+ requestSessionHistory(sessionId, limit = 1000, offset = 0) {
55
+ return new Promise((resolve, reject) => {
56
+ const requestId = 'history-' + Date.now() + '-' + Math.random();
57
+ const timeout = setTimeout(() => { this.requestMap.delete(requestId); this.stats.totalTimeouts++; reject(new Error('History request timeout')); }, this.config.messageTimeout);
58
+ this.requestMap.set(requestId, { type: 'history', resolve: (d) => { clearTimeout(timeout); resolve(d); }, reject });
59
+ this.sendMessage({ type: 'request_history', requestId, sessionId, limit, offset, timestamp: Date.now() });
60
+ });
61
+ },
62
+
63
+ getLastSeq(sessionId) { return this.lastSeqBySession[sessionId] || -1; },
64
+
65
+ setConnectionState(state) {
66
+ this.connectionState = state;
67
+ this.emit('state_change', { state, timestamp: Date.now() });
68
+ },
69
+
70
+ disconnect() {
71
+ this.isManuallyDisconnected = true;
72
+ this.reconnectCount = 0;
73
+ this.stopHeartbeat();
74
+ if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; }
75
+ if (this.ws) this.ws.close();
76
+ this.messageBuffer = [];
77
+ this.requestMap.clear();
78
+ this.setConnectionState('disconnected');
79
+ },
80
+
81
+ getStatus() {
82
+ return { isConnected: this.isConnected, isConnecting: this.isConnecting, connectionState: this.connectionState, reconnectCount: this.reconnectCount, bufferLength: this.messageBuffer.length, latency: { ...this.latency, samples: undefined }, stats: { ...this.stats } };
83
+ },
84
+
85
+ on(event, callback) { if (!this.listeners[event]) this.listeners[event] = []; this.listeners[event].push(callback); },
86
+ off(event, callback) { if (!this.listeners[event]) return; const index = this.listeners[event].indexOf(callback); if (index > -1) this.listeners[event].splice(index, 1); },
87
+ emit(event, data) { if (!this.listeners[event]) return; this.listeners[event].forEach((cb) => { try { cb(data); } catch (error) {} }); },
88
+
89
+ destroy() {
90
+ if (typeof document !== 'undefined') document.removeEventListener('visibilitychange', this._onVisibilityChange);
91
+ if (typeof window !== 'undefined') window.removeEventListener('online', this._onOnline);
92
+ this.disconnect();
93
+ this.listeners = {};
94
+ },
95
+
96
+ subscribeToVoiceList(callback) {
97
+ if (!this.voiceListListeners.includes(callback)) this.voiceListListeners.push(callback);
98
+ if (this.cachedVoiceList !== null) callback(this.cachedVoiceList);
99
+ },
100
+
101
+ unsubscribeFromVoiceList(callback) {
102
+ const idx = this.voiceListListeners.indexOf(callback);
103
+ if (idx > -1) this.voiceListListeners.splice(idx, 1);
104
+ }
105
+ });
106
+
107
+ if (typeof module !== 'undefined' && module.exports) module.exports = WebSocketManager;