agentgui 1.0.274 → 1.0.275

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.
Files changed (69) hide show
  1. package/CLAUDE.md +280 -280
  2. package/IPFS_DOWNLOADER.md +277 -277
  3. package/TASK_2C_COMPLETION.md +334 -334
  4. package/bin/gmgui.cjs +54 -54
  5. package/build-portable.js +3 -42
  6. package/database.js +1422 -1406
  7. package/lib/claude-runner.js +1130 -1130
  8. package/lib/ipfs-downloader.js +459 -459
  9. package/lib/speech.js +152 -152
  10. package/package.json +1 -1
  11. package/readme.md +76 -76
  12. package/server.js +3787 -3794
  13. package/setup-npm-token.sh +68 -68
  14. package/static/app.js +773 -773
  15. package/static/event-rendering-showcase.html +708 -708
  16. package/static/index.html +3178 -3180
  17. package/static/js/agent-auth.js +298 -298
  18. package/static/js/audio-recorder-processor.js +18 -18
  19. package/static/js/client.js +2656 -2656
  20. package/static/js/conversations.js +583 -583
  21. package/static/js/dialogs.js +267 -267
  22. package/static/js/event-consolidator.js +101 -101
  23. package/static/js/event-filter.js +311 -311
  24. package/static/js/event-processor.js +452 -452
  25. package/static/js/features.js +413 -413
  26. package/static/js/kalman-filter.js +67 -67
  27. package/static/js/progress-dialog.js +130 -130
  28. package/static/js/script-runner.js +219 -219
  29. package/static/js/streaming-renderer.js +2123 -2120
  30. package/static/js/syntax-highlighter.js +269 -269
  31. package/static/js/tts-websocket-handler.js +152 -152
  32. package/static/js/ui-components.js +431 -431
  33. package/static/js/voice.js +849 -849
  34. package/static/js/websocket-manager.js +596 -596
  35. package/static/templates/INDEX.html +465 -465
  36. package/static/templates/README.md +190 -190
  37. package/static/templates/agent-capabilities.html +56 -56
  38. package/static/templates/agent-metadata-panel.html +44 -44
  39. package/static/templates/agent-status-badge.html +30 -30
  40. package/static/templates/code-annotation-panel.html +155 -155
  41. package/static/templates/code-suggestion-panel.html +184 -184
  42. package/static/templates/command-header.html +77 -77
  43. package/static/templates/command-output-scrollable.html +118 -118
  44. package/static/templates/elapsed-time.html +54 -54
  45. package/static/templates/error-alert.html +106 -106
  46. package/static/templates/error-history-timeline.html +160 -160
  47. package/static/templates/error-recovery-options.html +109 -109
  48. package/static/templates/error-stack-trace.html +95 -95
  49. package/static/templates/error-summary.html +80 -80
  50. package/static/templates/event-counter.html +48 -48
  51. package/static/templates/execution-actions.html +97 -97
  52. package/static/templates/execution-progress-bar.html +80 -80
  53. package/static/templates/execution-stepper.html +120 -120
  54. package/static/templates/file-breadcrumb.html +118 -118
  55. package/static/templates/file-diff-viewer.html +121 -121
  56. package/static/templates/file-metadata.html +133 -133
  57. package/static/templates/file-read-panel.html +66 -66
  58. package/static/templates/file-write-panel.html +120 -120
  59. package/static/templates/git-branch-remote.html +107 -107
  60. package/static/templates/git-diff-list.html +101 -101
  61. package/static/templates/git-log-visualization.html +153 -153
  62. package/static/templates/git-status-panel.html +115 -115
  63. package/static/templates/quality-metrics-display.html +170 -170
  64. package/static/templates/terminal-output-panel.html +87 -87
  65. package/static/templates/test-results-display.html +144 -144
  66. package/static/theme.js +72 -72
  67. package/test-download-progress.js +223 -223
  68. package/test-websocket-broadcast.js +147 -147
  69. package/tests/ipfs-downloader.test.js +370 -370
@@ -1,596 +1,596 @@
1
- class WebSocketManager {
2
- constructor(config = {}) {
3
- this.config = {
4
- url: config.url || this.getWebSocketURL(),
5
- reconnectDelays: config.reconnectDelays || [500, 1000, 2000, 4000, 8000, 15000, 30000],
6
- maxReconnectDelay: config.maxReconnectDelay || 30000,
7
- heartbeatInterval: config.heartbeatInterval || 15000,
8
- messageTimeout: config.messageTimeout || 60000,
9
- maxBufferedMessages: config.maxBufferedMessages || 1000,
10
- pongTimeout: config.pongTimeout || 5000,
11
- latencyWindowSize: config.latencyWindowSize || 10,
12
- ...config
13
- };
14
-
15
- this.ws = null;
16
- this.isConnected = false;
17
- this.isConnecting = false;
18
- this.isManuallyDisconnected = false;
19
- this.reconnectCount = 0;
20
- this.reconnectTimer = null;
21
- this.messageBuffer = [];
22
- this.requestMap = new Map();
23
- this.heartbeatTimer = null;
24
- this.connectionState = 'disconnected';
25
- this.activeSubscriptions = new Set();
26
- this.connectionEstablishedAt = 0;
27
- this.cachedVoiceList = null;
28
- this.voiceListListeners = [];
29
-
30
- this.latency = {
31
- samples: [],
32
- current: 0,
33
- avg: 0,
34
- jitter: 0,
35
- quality: 'unknown',
36
- predicted: 0,
37
- predictedNext: 0,
38
- trend: 'stable',
39
- missedPongs: 0,
40
- pingCounter: 0
41
- };
42
-
43
- this._latencyKalman = typeof KalmanFilter !== 'undefined' ? new KalmanFilter({ processNoise: 1, measurementNoise: 10 }) : null;
44
- this._trendHistory = [];
45
- this._trendCount = 0;
46
- this._reconnectedAt = 0;
47
-
48
- this.stats = {
49
- totalConnections: 0,
50
- totalReconnects: 0,
51
- totalMessagesSent: 0,
52
- totalMessagesReceived: 0,
53
- totalErrors: 0,
54
- totalTimeouts: 0,
55
- avgLatency: 0,
56
- lastConnectedTime: null,
57
- connectionDuration: 0
58
- };
59
-
60
- this.lastSeqBySession = {};
61
- this.listeners = {};
62
-
63
- this._onVisibilityChange = this._handleVisibilityChange.bind(this);
64
- this._onOnline = this._handleOnline.bind(this);
65
- if (typeof document !== 'undefined') {
66
- document.addEventListener('visibilitychange', this._onVisibilityChange);
67
- }
68
- if (typeof window !== 'undefined') {
69
- window.addEventListener('online', this._onOnline);
70
- }
71
- }
72
-
73
- getWebSocketURL() {
74
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
75
- const baseURL = window.__BASE_URL || '/gm';
76
- return `${protocol}//${window.location.host}${baseURL}/sync`;
77
- }
78
-
79
- async connect() {
80
- if (this.isConnected || this.isConnecting) return this.ws;
81
- this.isManuallyDisconnected = false;
82
- this.isConnecting = true;
83
- this.setConnectionState('connecting');
84
-
85
- try {
86
- this.ws = new WebSocket(this.config.url);
87
- this.ws.onopen = () => this.onOpen();
88
- this.ws.onmessage = (event) => this.onMessage(event);
89
- this.ws.onerror = (error) => this.onError(error);
90
- this.ws.onclose = () => this.onClose();
91
- return await this.waitForConnection(this.config.messageTimeout);
92
- } catch (error) {
93
- this.isConnecting = false;
94
- this.stats.totalErrors++;
95
- this.scheduleReconnect();
96
- throw error;
97
- }
98
- }
99
-
100
- waitForConnection(timeout = 5000) {
101
- return new Promise((resolve, reject) => {
102
- const timer = setTimeout(() => reject(new Error('WebSocket connection timeout')), timeout);
103
- const check = () => {
104
- if (this.isConnected || this.ws?.readyState === WebSocket.OPEN) {
105
- clearTimeout(timer);
106
- resolve(this.ws);
107
- } else {
108
- setTimeout(check, 50);
109
- }
110
- };
111
- check();
112
- });
113
- }
114
-
115
- onOpen() {
116
- this.isConnected = true;
117
- this.isConnecting = false;
118
- this.connectionEstablishedAt = Date.now();
119
- this._reconnectedAt = this.stats.totalConnections > 0 ? Date.now() : 0;
120
- this.stats.totalConnections++;
121
- this.stats.lastConnectedTime = Date.now();
122
- this.latency.missedPongs = 0;
123
- this.setConnectionState('connected');
124
-
125
- this.flushMessageBuffer();
126
- this.resubscribeAll();
127
- this.startHeartbeat();
128
-
129
- this.emit('connected', { timestamp: Date.now() });
130
- }
131
-
132
- onMessage(event) {
133
- try {
134
- const parsed = JSON.parse(event.data);
135
- const messages = Array.isArray(parsed) ? parsed : [parsed];
136
- this.stats.totalMessagesReceived += messages.length;
137
-
138
- for (const data of messages) {
139
- if (data.type === 'pong') {
140
- this._handlePong(data);
141
- continue;
142
- }
143
-
144
- if (data.type === 'voice_list') {
145
- this.cachedVoiceList = data.voices || [];
146
- for (const listener of this.voiceListListeners) {
147
- try { listener(this.cachedVoiceList); } catch (_) {}
148
- }
149
- }
150
-
151
- if (data.seq !== undefined && data.sessionId) {
152
- this.lastSeqBySession[data.sessionId] = Math.max(
153
- this.lastSeqBySession[data.sessionId] || -1, data.seq
154
- );
155
- }
156
-
157
- this.emit('message', data);
158
- if (data.type) this.emit('message:' + data.type, data);
159
- }
160
- } catch (error) {
161
- this.stats.totalErrors++;
162
- }
163
- }
164
-
165
- _handlePong(data) {
166
- this.latency.missedPongs = 0;
167
- const requestId = data.requestId;
168
- if (requestId && this.requestMap.has(requestId)) {
169
- const request = this.requestMap.get(requestId);
170
- const rtt = Date.now() - request.sentTime;
171
- this.requestMap.delete(requestId);
172
- this._recordLatency(rtt);
173
- if (request.resolve) request.resolve({ latency: rtt });
174
- }
175
- }
176
-
177
- _recordLatency(rtt) {
178
- const samples = this.latency.samples;
179
- samples.push(rtt);
180
- if (samples.length > this.config.latencyWindowSize) samples.shift();
181
-
182
- this.latency.current = rtt;
183
-
184
- if (this._latencyKalman && samples.length > 3) {
185
- if (this._reconnectedAt && Date.now() - this._reconnectedAt < 5000) {
186
- this._latencyKalman.setMeasurementNoise(50);
187
- } else {
188
- this._latencyKalman.setMeasurementNoise(10);
189
- }
190
- const result = this._latencyKalman.update(rtt);
191
- this.latency.predicted = result.estimate;
192
- this.latency.predictedNext = this._latencyKalman.predict();
193
- this.latency.avg = result.estimate;
194
- } else {
195
- this.latency.avg = samples.reduce((a, b) => a + b, 0) / samples.length;
196
- this.latency.predicted = this.latency.avg;
197
- this.latency.predictedNext = this.latency.avg;
198
- }
199
-
200
- if (samples.length > 1) {
201
- const mean = samples.reduce((a, b) => a + b, 0) / samples.length;
202
- const variance = samples.reduce((sum, s) => sum + Math.pow(s - mean, 2), 0) / samples.length;
203
- this.latency.jitter = Math.sqrt(variance);
204
- }
205
-
206
- this._trendHistory.push(this.latency.predicted);
207
- if (this._trendHistory.length > 3) this._trendHistory.shift();
208
- if (this._trendHistory.length >= 3) {
209
- const [a, b, c] = this._trendHistory;
210
- const rising = b > a * 1.05 && c > b * 1.05;
211
- const falling = b < a * 0.95 && c < b * 0.95;
212
- this.latency.trend = rising ? 'rising' : falling ? 'falling' : 'stable';
213
- }
214
-
215
- this.latency.quality = this._qualityTier(this.latency.avg);
216
- this.stats.avgLatency = this.latency.avg;
217
-
218
- this.emit('latency_update', {
219
- latency: rtt,
220
- avg: this.latency.avg,
221
- predicted: this.latency.predicted,
222
- predictedNext: this.latency.predictedNext,
223
- trend: this.latency.trend,
224
- jitter: this.latency.jitter,
225
- quality: this.latency.quality
226
- });
227
-
228
- if (rtt > this.latency.avg * 3 && samples.length >= 3) {
229
- this.emit('latency_spike', { latency: rtt, avg: this.latency.avg });
230
- }
231
-
232
- this.emit('latency_prediction', {
233
- predicted: this.latency.predicted,
234
- predictedNext: this.latency.predictedNext,
235
- trend: this.latency.trend,
236
- gain: this._latencyKalman ? this._latencyKalman.getState().gain : 0
237
- });
238
-
239
- this._checkDegradation();
240
- }
241
-
242
- _checkDegradation() {
243
- if (this.latency.trend === 'rising') {
244
- this._trendCount = (this._trendCount || 0) + 1;
245
- } else {
246
- if (this._trendCount >= 5 && (this.latency.trend === 'stable' || this.latency.trend === 'falling')) {
247
- this.emit('connection_recovering', { currentTier: this.latency.quality });
248
- }
249
- this._trendCount = 0;
250
- return;
251
- }
252
- if (this._trendCount < 5) return;
253
- const currentTier = this.latency.quality;
254
- const predictedTier = this._qualityTier(this.latency.predictedNext);
255
- if (predictedTier === currentTier) return;
256
- const thresholds = { excellent: 50, good: 150, fair: 300, poor: 500 };
257
- const threshold = thresholds[currentTier];
258
- if (!threshold) return;
259
- const rate = this._trendHistory.length >= 2 ? this._trendHistory[this._trendHistory.length - 1] - this._trendHistory[0] : 0;
260
- const timeToChange = rate > 0 ? Math.round((threshold - this.latency.predicted) / rate * 1000) : Infinity;
261
- this.emit('connection_degrading', { currentTier, predictedTier, predictedLatency: this.latency.predictedNext, timeToChange });
262
- }
263
-
264
- _qualityTier(avg) {
265
- if (avg < 50) return 'excellent';
266
- if (avg < 150) return 'good';
267
- if (avg < 300) return 'fair';
268
- if (avg < 500) return 'poor';
269
- return 'bad';
270
- }
271
-
272
- onError(error) {
273
- this.stats.totalErrors++;
274
- this.emit('error', { error, timestamp: Date.now() });
275
- }
276
-
277
- onClose() {
278
- this.isConnected = false;
279
- this.isConnecting = false;
280
- this.setConnectionState('disconnected');
281
- this.stopHeartbeat();
282
-
283
- if (this.stats.lastConnectedTime) {
284
- this.stats.connectionDuration = Date.now() - this.stats.lastConnectedTime;
285
- }
286
-
287
- this.emit('disconnected', { timestamp: Date.now() });
288
-
289
- if (!this.isManuallyDisconnected) {
290
- this.scheduleReconnect();
291
- }
292
- }
293
-
294
- scheduleReconnect() {
295
- if (this.isManuallyDisconnected) return;
296
- if (this.reconnectTimer) return;
297
-
298
- const delays = this.config.reconnectDelays;
299
- const baseDelay = this.reconnectCount < delays.length
300
- ? delays[this.reconnectCount]
301
- : this.config.maxReconnectDelay;
302
-
303
- const jitter = Math.random() * 0.3 * baseDelay;
304
- const delay = Math.round(baseDelay + jitter);
305
-
306
- this.reconnectCount++;
307
- this.stats.totalReconnects++;
308
- this.setConnectionState('reconnecting');
309
-
310
- this.emit('reconnecting', {
311
- delay,
312
- attempt: this.reconnectCount,
313
- nextAttemptAt: Date.now() + delay
314
- });
315
-
316
- this.reconnectTimer = setTimeout(() => {
317
- this.reconnectTimer = null;
318
- this.connect().catch(() => {});
319
- }, delay);
320
- }
321
-
322
- startHeartbeat() {
323
- this.stopHeartbeat();
324
- const tick = () => {
325
- if (!this.isConnected) return;
326
- if (typeof document !== 'undefined' && document.hidden) {
327
- this.heartbeatTimer = setTimeout(tick, this.config.heartbeatInterval);
328
- return;
329
- }
330
- this.latency.pingCounter++;
331
- this.ping().catch(() => {
332
- this.latency.missedPongs++;
333
- if (this.latency.missedPongs >= 3) {
334
- this.latency.missedPongs = 0;
335
- if (this.ws) {
336
- try { this.ws.close(); } catch (_) {}
337
- }
338
- }
339
- });
340
- if (this.latency.pingCounter % 10 === 0) {
341
- this._reportLatency();
342
- }
343
- this.heartbeatTimer = setTimeout(tick, this.config.heartbeatInterval);
344
- };
345
- this.heartbeatTimer = setTimeout(tick, this.config.heartbeatInterval);
346
- }
347
-
348
- stopHeartbeat() {
349
- if (this.heartbeatTimer) {
350
- clearTimeout(this.heartbeatTimer);
351
- this.heartbeatTimer = null;
352
- }
353
- }
354
-
355
- _reportLatency() {
356
- if (this.latency.avg > 0) {
357
- this.sendMessage({
358
- type: 'latency_report',
359
- avg: Math.round(this.latency.avg),
360
- jitter: Math.round(this.latency.jitter),
361
- quality: this.latency.quality,
362
- trend: this.latency.trend,
363
- predictedNext: Math.round(this.latency.predictedNext)
364
- });
365
- }
366
- }
367
-
368
- _handleVisibilityChange() {
369
- if (typeof document !== 'undefined' && document.hidden) {
370
- this._hiddenAt = Date.now();
371
- return;
372
- }
373
- if (this._hiddenAt && this._latencyKalman && Date.now() - this._hiddenAt > 30000) {
374
- this._latencyKalman.reset();
375
- this._trendHistory = [];
376
- this.latency.trend = 'stable';
377
- }
378
- this._hiddenAt = 0;
379
- if (!this.isConnected && !this.isConnecting && !this.isManuallyDisconnected) {
380
- if (this.reconnectTimer) {
381
- clearTimeout(this.reconnectTimer);
382
- this.reconnectTimer = null;
383
- }
384
- this.connect().catch(() => {});
385
- }
386
- if (this.isConnected) {
387
- const stableFor = Date.now() - this.connectionEstablishedAt;
388
- if (stableFor > 10000) this.reconnectCount = 0;
389
- }
390
- }
391
-
392
- _handleOnline() {
393
- if (!this.isConnected && !this.isConnecting && !this.isManuallyDisconnected) {
394
- if (this.reconnectTimer) {
395
- clearTimeout(this.reconnectTimer);
396
- this.reconnectTimer = null;
397
- }
398
- this.connect().catch(() => {});
399
- }
400
- }
401
-
402
- ping() {
403
- const requestId = 'ping-' + Date.now() + '-' + Math.random();
404
- const request = { sentTime: Date.now(), resolve: null };
405
-
406
- const promise = new Promise((resolve, reject) => {
407
- request.resolve = resolve;
408
- setTimeout(() => {
409
- if (this.requestMap.has(requestId)) {
410
- this.stats.totalTimeouts++;
411
- this.requestMap.delete(requestId);
412
- reject(new Error('ping timeout'));
413
- }
414
- }, this.config.pongTimeout);
415
- });
416
-
417
- this.requestMap.set(requestId, request);
418
- this.sendMessage({ type: 'ping', requestId });
419
- return promise;
420
- }
421
-
422
- sendMessage(data) {
423
- if (!data || typeof data !== 'object') throw new Error('Invalid message data');
424
-
425
- if (data.type === 'subscribe') {
426
- const key = data.sessionId ? 'session:' + data.sessionId : 'conv:' + data.conversationId;
427
- this.activeSubscriptions.add(key);
428
- } else if (data.type === 'unsubscribe') {
429
- const key = data.sessionId ? 'session:' + data.sessionId : 'conv:' + data.conversationId;
430
- this.activeSubscriptions.delete(key);
431
- }
432
-
433
- if (!this.isConnected) {
434
- this.bufferMessage(data);
435
- return false;
436
- }
437
-
438
- try {
439
- this.ws.send(JSON.stringify(data));
440
- this.stats.totalMessagesSent++;
441
- return true;
442
- } catch (error) {
443
- this.stats.totalErrors++;
444
- this.bufferMessage(data);
445
- return false;
446
- }
447
- }
448
-
449
- bufferMessage(data) {
450
- if (this.messageBuffer.length >= this.config.maxBufferedMessages) {
451
- this.messageBuffer.shift();
452
- }
453
- this.messageBuffer.push(data);
454
- this.emit('message_buffered', { bufferLength: this.messageBuffer.length });
455
- }
456
-
457
- flushMessageBuffer() {
458
- if (this.messageBuffer.length === 0) return;
459
- const messages = [...this.messageBuffer];
460
- this.messageBuffer = [];
461
- for (const message of messages) {
462
- try {
463
- this.ws.send(JSON.stringify(message));
464
- this.stats.totalMessagesSent++;
465
- } catch (error) {
466
- this.bufferMessage(message);
467
- }
468
- }
469
- this.emit('buffer_flushed', { count: messages.length });
470
- }
471
-
472
- subscribeToSession(sessionId) {
473
- return this.sendMessage({ type: 'subscribe', sessionId, timestamp: Date.now() });
474
- }
475
-
476
- resubscribeAll() {
477
- for (const key of this.activeSubscriptions) {
478
- const [type, id] = key.split(':');
479
- const msg = { type: 'subscribe', timestamp: Date.now() };
480
- if (type === 'session') msg.sessionId = id;
481
- else msg.conversationId = id;
482
- try {
483
- this.ws.send(JSON.stringify(msg));
484
- this.stats.totalMessagesSent++;
485
- } catch (_) {}
486
- }
487
- }
488
-
489
- unsubscribeFromSession(sessionId) {
490
- return this.sendMessage({ type: 'unsubscribe', sessionId, timestamp: Date.now() });
491
- }
492
-
493
- requestSessionHistory(sessionId, limit = 1000, offset = 0) {
494
- return new Promise((resolve, reject) => {
495
- const requestId = 'history-' + Date.now() + '-' + Math.random();
496
- const timeout = setTimeout(() => {
497
- this.requestMap.delete(requestId);
498
- this.stats.totalTimeouts++;
499
- reject(new Error('History request timeout'));
500
- }, this.config.messageTimeout);
501
-
502
- this.requestMap.set(requestId, {
503
- type: 'history',
504
- resolve: (d) => { clearTimeout(timeout); resolve(d); },
505
- reject
506
- });
507
-
508
- this.sendMessage({
509
- type: 'request_history', requestId, sessionId, limit, offset, timestamp: Date.now()
510
- });
511
- });
512
- }
513
-
514
- getLastSeq(sessionId) {
515
- return this.lastSeqBySession[sessionId] || -1;
516
- }
517
-
518
- setConnectionState(state) {
519
- this.connectionState = state;
520
- this.emit('state_change', { state, timestamp: Date.now() });
521
- }
522
-
523
- disconnect() {
524
- this.isManuallyDisconnected = true;
525
- this.reconnectCount = 0;
526
- this.stopHeartbeat();
527
- if (this.reconnectTimer) {
528
- clearTimeout(this.reconnectTimer);
529
- this.reconnectTimer = null;
530
- }
531
- if (this.ws) this.ws.close();
532
- this.messageBuffer = [];
533
- this.requestMap.clear();
534
- this.setConnectionState('disconnected');
535
- }
536
-
537
- getStatus() {
538
- return {
539
- isConnected: this.isConnected,
540
- isConnecting: this.isConnecting,
541
- connectionState: this.connectionState,
542
- reconnectCount: this.reconnectCount,
543
- bufferLength: this.messageBuffer.length,
544
- latency: { ...this.latency, samples: undefined },
545
- stats: { ...this.stats }
546
- };
547
- }
548
-
549
- on(event, callback) {
550
- if (!this.listeners[event]) this.listeners[event] = [];
551
- this.listeners[event].push(callback);
552
- }
553
-
554
- off(event, callback) {
555
- if (!this.listeners[event]) return;
556
- const index = this.listeners[event].indexOf(callback);
557
- if (index > -1) this.listeners[event].splice(index, 1);
558
- }
559
-
560
- emit(event, data) {
561
- if (!this.listeners[event]) return;
562
- this.listeners[event].forEach((cb) => {
563
- try { cb(data); } catch (error) {}
564
- });
565
- }
566
-
567
- destroy() {
568
- if (typeof document !== 'undefined') {
569
- document.removeEventListener('visibilitychange', this._onVisibilityChange);
570
- }
571
- if (typeof window !== 'undefined') {
572
- window.removeEventListener('online', this._onOnline);
573
- }
574
- this.disconnect();
575
- this.listeners = {};
576
- }
577
- subscribeToVoiceList(callback) {
578
- if (!this.voiceListListeners.includes(callback)) {
579
- this.voiceListListeners.push(callback);
580
- }
581
- if (this.cachedVoiceList !== null) {
582
- callback(this.cachedVoiceList);
583
- }
584
- }
585
-
586
- unsubscribeFromVoiceList(callback) {
587
- const idx = this.voiceListListeners.indexOf(callback);
588
- if (idx > -1) {
589
- this.voiceListListeners.splice(idx, 1);
590
- }
591
- }
592
- }
593
-
594
- if (typeof module !== 'undefined' && module.exports) {
595
- module.exports = WebSocketManager;
596
- }
1
+ class WebSocketManager {
2
+ constructor(config = {}) {
3
+ this.config = {
4
+ url: config.url || this.getWebSocketURL(),
5
+ reconnectDelays: config.reconnectDelays || [500, 1000, 2000, 4000, 8000, 15000, 30000],
6
+ maxReconnectDelay: config.maxReconnectDelay || 30000,
7
+ heartbeatInterval: config.heartbeatInterval || 15000,
8
+ messageTimeout: config.messageTimeout || 60000,
9
+ maxBufferedMessages: config.maxBufferedMessages || 1000,
10
+ pongTimeout: config.pongTimeout || 5000,
11
+ latencyWindowSize: config.latencyWindowSize || 10,
12
+ ...config
13
+ };
14
+
15
+ this.ws = null;
16
+ this.isConnected = false;
17
+ this.isConnecting = false;
18
+ this.isManuallyDisconnected = false;
19
+ this.reconnectCount = 0;
20
+ this.reconnectTimer = null;
21
+ this.messageBuffer = [];
22
+ this.requestMap = new Map();
23
+ this.heartbeatTimer = null;
24
+ this.connectionState = 'disconnected';
25
+ this.activeSubscriptions = new Set();
26
+ this.connectionEstablishedAt = 0;
27
+ this.cachedVoiceList = null;
28
+ this.voiceListListeners = [];
29
+
30
+ this.latency = {
31
+ samples: [],
32
+ current: 0,
33
+ avg: 0,
34
+ jitter: 0,
35
+ quality: 'unknown',
36
+ predicted: 0,
37
+ predictedNext: 0,
38
+ trend: 'stable',
39
+ missedPongs: 0,
40
+ pingCounter: 0
41
+ };
42
+
43
+ this._latencyKalman = typeof KalmanFilter !== 'undefined' ? new KalmanFilter({ processNoise: 1, measurementNoise: 10 }) : null;
44
+ this._trendHistory = [];
45
+ this._trendCount = 0;
46
+ this._reconnectedAt = 0;
47
+
48
+ this.stats = {
49
+ totalConnections: 0,
50
+ totalReconnects: 0,
51
+ totalMessagesSent: 0,
52
+ totalMessagesReceived: 0,
53
+ totalErrors: 0,
54
+ totalTimeouts: 0,
55
+ avgLatency: 0,
56
+ lastConnectedTime: null,
57
+ connectionDuration: 0
58
+ };
59
+
60
+ this.lastSeqBySession = {};
61
+ this.listeners = {};
62
+
63
+ this._onVisibilityChange = this._handleVisibilityChange.bind(this);
64
+ this._onOnline = this._handleOnline.bind(this);
65
+ if (typeof document !== 'undefined') {
66
+ document.addEventListener('visibilitychange', this._onVisibilityChange);
67
+ }
68
+ if (typeof window !== 'undefined') {
69
+ window.addEventListener('online', this._onOnline);
70
+ }
71
+ }
72
+
73
+ getWebSocketURL() {
74
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
75
+ const baseURL = window.__BASE_URL || '/gm';
76
+ return `${protocol}//${window.location.host}${baseURL}/sync`;
77
+ }
78
+
79
+ async connect() {
80
+ if (this.isConnected || this.isConnecting) return this.ws;
81
+ this.isManuallyDisconnected = false;
82
+ this.isConnecting = true;
83
+ this.setConnectionState('connecting');
84
+
85
+ try {
86
+ this.ws = new WebSocket(this.config.url);
87
+ this.ws.onopen = () => this.onOpen();
88
+ this.ws.onmessage = (event) => this.onMessage(event);
89
+ this.ws.onerror = (error) => this.onError(error);
90
+ this.ws.onclose = () => this.onClose();
91
+ return await this.waitForConnection(this.config.messageTimeout);
92
+ } catch (error) {
93
+ this.isConnecting = false;
94
+ this.stats.totalErrors++;
95
+ this.scheduleReconnect();
96
+ throw error;
97
+ }
98
+ }
99
+
100
+ waitForConnection(timeout = 5000) {
101
+ return new Promise((resolve, reject) => {
102
+ const timer = setTimeout(() => reject(new Error('WebSocket connection timeout')), timeout);
103
+ const check = () => {
104
+ if (this.isConnected || this.ws?.readyState === WebSocket.OPEN) {
105
+ clearTimeout(timer);
106
+ resolve(this.ws);
107
+ } else {
108
+ setTimeout(check, 50);
109
+ }
110
+ };
111
+ check();
112
+ });
113
+ }
114
+
115
+ onOpen() {
116
+ this.isConnected = true;
117
+ this.isConnecting = false;
118
+ this.connectionEstablishedAt = Date.now();
119
+ this._reconnectedAt = this.stats.totalConnections > 0 ? Date.now() : 0;
120
+ this.stats.totalConnections++;
121
+ this.stats.lastConnectedTime = Date.now();
122
+ this.latency.missedPongs = 0;
123
+ this.setConnectionState('connected');
124
+
125
+ this.flushMessageBuffer();
126
+ this.resubscribeAll();
127
+ this.startHeartbeat();
128
+
129
+ this.emit('connected', { timestamp: Date.now() });
130
+ }
131
+
132
+ onMessage(event) {
133
+ try {
134
+ const parsed = JSON.parse(event.data);
135
+ const messages = Array.isArray(parsed) ? parsed : [parsed];
136
+ this.stats.totalMessagesReceived += messages.length;
137
+
138
+ for (const data of messages) {
139
+ if (data.type === 'pong') {
140
+ this._handlePong(data);
141
+ continue;
142
+ }
143
+
144
+ if (data.type === 'voice_list') {
145
+ this.cachedVoiceList = data.voices || [];
146
+ for (const listener of this.voiceListListeners) {
147
+ try { listener(this.cachedVoiceList); } catch (_) {}
148
+ }
149
+ }
150
+
151
+ if (data.seq !== undefined && data.sessionId) {
152
+ this.lastSeqBySession[data.sessionId] = Math.max(
153
+ this.lastSeqBySession[data.sessionId] || -1, data.seq
154
+ );
155
+ }
156
+
157
+ this.emit('message', data);
158
+ if (data.type) this.emit('message:' + data.type, data);
159
+ }
160
+ } catch (error) {
161
+ this.stats.totalErrors++;
162
+ }
163
+ }
164
+
165
+ _handlePong(data) {
166
+ this.latency.missedPongs = 0;
167
+ const requestId = data.requestId;
168
+ if (requestId && this.requestMap.has(requestId)) {
169
+ const request = this.requestMap.get(requestId);
170
+ const rtt = Date.now() - request.sentTime;
171
+ this.requestMap.delete(requestId);
172
+ this._recordLatency(rtt);
173
+ if (request.resolve) request.resolve({ latency: rtt });
174
+ }
175
+ }
176
+
177
+ _recordLatency(rtt) {
178
+ const samples = this.latency.samples;
179
+ samples.push(rtt);
180
+ if (samples.length > this.config.latencyWindowSize) samples.shift();
181
+
182
+ this.latency.current = rtt;
183
+
184
+ if (this._latencyKalman && samples.length > 3) {
185
+ if (this._reconnectedAt && Date.now() - this._reconnectedAt < 5000) {
186
+ this._latencyKalman.setMeasurementNoise(50);
187
+ } else {
188
+ this._latencyKalman.setMeasurementNoise(10);
189
+ }
190
+ const result = this._latencyKalman.update(rtt);
191
+ this.latency.predicted = result.estimate;
192
+ this.latency.predictedNext = this._latencyKalman.predict();
193
+ this.latency.avg = result.estimate;
194
+ } else {
195
+ this.latency.avg = samples.reduce((a, b) => a + b, 0) / samples.length;
196
+ this.latency.predicted = this.latency.avg;
197
+ this.latency.predictedNext = this.latency.avg;
198
+ }
199
+
200
+ if (samples.length > 1) {
201
+ const mean = samples.reduce((a, b) => a + b, 0) / samples.length;
202
+ const variance = samples.reduce((sum, s) => sum + Math.pow(s - mean, 2), 0) / samples.length;
203
+ this.latency.jitter = Math.sqrt(variance);
204
+ }
205
+
206
+ this._trendHistory.push(this.latency.predicted);
207
+ if (this._trendHistory.length > 3) this._trendHistory.shift();
208
+ if (this._trendHistory.length >= 3) {
209
+ const [a, b, c] = this._trendHistory;
210
+ const rising = b > a * 1.05 && c > b * 1.05;
211
+ const falling = b < a * 0.95 && c < b * 0.95;
212
+ this.latency.trend = rising ? 'rising' : falling ? 'falling' : 'stable';
213
+ }
214
+
215
+ this.latency.quality = this._qualityTier(this.latency.avg);
216
+ this.stats.avgLatency = this.latency.avg;
217
+
218
+ this.emit('latency_update', {
219
+ latency: rtt,
220
+ avg: this.latency.avg,
221
+ predicted: this.latency.predicted,
222
+ predictedNext: this.latency.predictedNext,
223
+ trend: this.latency.trend,
224
+ jitter: this.latency.jitter,
225
+ quality: this.latency.quality
226
+ });
227
+
228
+ if (rtt > this.latency.avg * 3 && samples.length >= 3) {
229
+ this.emit('latency_spike', { latency: rtt, avg: this.latency.avg });
230
+ }
231
+
232
+ this.emit('latency_prediction', {
233
+ predicted: this.latency.predicted,
234
+ predictedNext: this.latency.predictedNext,
235
+ trend: this.latency.trend,
236
+ gain: this._latencyKalman ? this._latencyKalman.getState().gain : 0
237
+ });
238
+
239
+ this._checkDegradation();
240
+ }
241
+
242
+ _checkDegradation() {
243
+ if (this.latency.trend === 'rising') {
244
+ this._trendCount = (this._trendCount || 0) + 1;
245
+ } else {
246
+ if (this._trendCount >= 5 && (this.latency.trend === 'stable' || this.latency.trend === 'falling')) {
247
+ this.emit('connection_recovering', { currentTier: this.latency.quality });
248
+ }
249
+ this._trendCount = 0;
250
+ return;
251
+ }
252
+ if (this._trendCount < 5) return;
253
+ const currentTier = this.latency.quality;
254
+ const predictedTier = this._qualityTier(this.latency.predictedNext);
255
+ if (predictedTier === currentTier) return;
256
+ const thresholds = { excellent: 50, good: 150, fair: 300, poor: 500 };
257
+ const threshold = thresholds[currentTier];
258
+ if (!threshold) return;
259
+ const rate = this._trendHistory.length >= 2 ? this._trendHistory[this._trendHistory.length - 1] - this._trendHistory[0] : 0;
260
+ const timeToChange = rate > 0 ? Math.round((threshold - this.latency.predicted) / rate * 1000) : Infinity;
261
+ this.emit('connection_degrading', { currentTier, predictedTier, predictedLatency: this.latency.predictedNext, timeToChange });
262
+ }
263
+
264
+ _qualityTier(avg) {
265
+ if (avg < 50) return 'excellent';
266
+ if (avg < 150) return 'good';
267
+ if (avg < 300) return 'fair';
268
+ if (avg < 500) return 'poor';
269
+ return 'bad';
270
+ }
271
+
272
+ onError(error) {
273
+ this.stats.totalErrors++;
274
+ this.emit('error', { error, timestamp: Date.now() });
275
+ }
276
+
277
+ onClose() {
278
+ this.isConnected = false;
279
+ this.isConnecting = false;
280
+ this.setConnectionState('disconnected');
281
+ this.stopHeartbeat();
282
+
283
+ if (this.stats.lastConnectedTime) {
284
+ this.stats.connectionDuration = Date.now() - this.stats.lastConnectedTime;
285
+ }
286
+
287
+ this.emit('disconnected', { timestamp: Date.now() });
288
+
289
+ if (!this.isManuallyDisconnected) {
290
+ this.scheduleReconnect();
291
+ }
292
+ }
293
+
294
+ scheduleReconnect() {
295
+ if (this.isManuallyDisconnected) return;
296
+ if (this.reconnectTimer) return;
297
+
298
+ const delays = this.config.reconnectDelays;
299
+ const baseDelay = this.reconnectCount < delays.length
300
+ ? delays[this.reconnectCount]
301
+ : this.config.maxReconnectDelay;
302
+
303
+ const jitter = Math.random() * 0.3 * baseDelay;
304
+ const delay = Math.round(baseDelay + jitter);
305
+
306
+ this.reconnectCount++;
307
+ this.stats.totalReconnects++;
308
+ this.setConnectionState('reconnecting');
309
+
310
+ this.emit('reconnecting', {
311
+ delay,
312
+ attempt: this.reconnectCount,
313
+ nextAttemptAt: Date.now() + delay
314
+ });
315
+
316
+ this.reconnectTimer = setTimeout(() => {
317
+ this.reconnectTimer = null;
318
+ this.connect().catch(() => {});
319
+ }, delay);
320
+ }
321
+
322
+ startHeartbeat() {
323
+ this.stopHeartbeat();
324
+ const tick = () => {
325
+ if (!this.isConnected) return;
326
+ if (typeof document !== 'undefined' && document.hidden) {
327
+ this.heartbeatTimer = setTimeout(tick, this.config.heartbeatInterval);
328
+ return;
329
+ }
330
+ this.latency.pingCounter++;
331
+ this.ping().catch(() => {
332
+ this.latency.missedPongs++;
333
+ if (this.latency.missedPongs >= 3) {
334
+ this.latency.missedPongs = 0;
335
+ if (this.ws) {
336
+ try { this.ws.close(); } catch (_) {}
337
+ }
338
+ }
339
+ });
340
+ if (this.latency.pingCounter % 10 === 0) {
341
+ this._reportLatency();
342
+ }
343
+ this.heartbeatTimer = setTimeout(tick, this.config.heartbeatInterval);
344
+ };
345
+ this.heartbeatTimer = setTimeout(tick, this.config.heartbeatInterval);
346
+ }
347
+
348
+ stopHeartbeat() {
349
+ if (this.heartbeatTimer) {
350
+ clearTimeout(this.heartbeatTimer);
351
+ this.heartbeatTimer = null;
352
+ }
353
+ }
354
+
355
+ _reportLatency() {
356
+ if (this.latency.avg > 0) {
357
+ this.sendMessage({
358
+ type: 'latency_report',
359
+ avg: Math.round(this.latency.avg),
360
+ jitter: Math.round(this.latency.jitter),
361
+ quality: this.latency.quality,
362
+ trend: this.latency.trend,
363
+ predictedNext: Math.round(this.latency.predictedNext)
364
+ });
365
+ }
366
+ }
367
+
368
+ _handleVisibilityChange() {
369
+ if (typeof document !== 'undefined' && document.hidden) {
370
+ this._hiddenAt = Date.now();
371
+ return;
372
+ }
373
+ if (this._hiddenAt && this._latencyKalman && Date.now() - this._hiddenAt > 30000) {
374
+ this._latencyKalman.reset();
375
+ this._trendHistory = [];
376
+ this.latency.trend = 'stable';
377
+ }
378
+ this._hiddenAt = 0;
379
+ if (!this.isConnected && !this.isConnecting && !this.isManuallyDisconnected) {
380
+ if (this.reconnectTimer) {
381
+ clearTimeout(this.reconnectTimer);
382
+ this.reconnectTimer = null;
383
+ }
384
+ this.connect().catch(() => {});
385
+ }
386
+ if (this.isConnected) {
387
+ const stableFor = Date.now() - this.connectionEstablishedAt;
388
+ if (stableFor > 10000) this.reconnectCount = 0;
389
+ }
390
+ }
391
+
392
+ _handleOnline() {
393
+ if (!this.isConnected && !this.isConnecting && !this.isManuallyDisconnected) {
394
+ if (this.reconnectTimer) {
395
+ clearTimeout(this.reconnectTimer);
396
+ this.reconnectTimer = null;
397
+ }
398
+ this.connect().catch(() => {});
399
+ }
400
+ }
401
+
402
+ ping() {
403
+ const requestId = 'ping-' + Date.now() + '-' + Math.random();
404
+ const request = { sentTime: Date.now(), resolve: null };
405
+
406
+ const promise = new Promise((resolve, reject) => {
407
+ request.resolve = resolve;
408
+ setTimeout(() => {
409
+ if (this.requestMap.has(requestId)) {
410
+ this.stats.totalTimeouts++;
411
+ this.requestMap.delete(requestId);
412
+ reject(new Error('ping timeout'));
413
+ }
414
+ }, this.config.pongTimeout);
415
+ });
416
+
417
+ this.requestMap.set(requestId, request);
418
+ this.sendMessage({ type: 'ping', requestId });
419
+ return promise;
420
+ }
421
+
422
+ sendMessage(data) {
423
+ if (!data || typeof data !== 'object') throw new Error('Invalid message data');
424
+
425
+ if (data.type === 'subscribe') {
426
+ const key = data.sessionId ? 'session:' + data.sessionId : 'conv:' + data.conversationId;
427
+ this.activeSubscriptions.add(key);
428
+ } else if (data.type === 'unsubscribe') {
429
+ const key = data.sessionId ? 'session:' + data.sessionId : 'conv:' + data.conversationId;
430
+ this.activeSubscriptions.delete(key);
431
+ }
432
+
433
+ if (!this.isConnected) {
434
+ this.bufferMessage(data);
435
+ return false;
436
+ }
437
+
438
+ try {
439
+ this.ws.send(JSON.stringify(data));
440
+ this.stats.totalMessagesSent++;
441
+ return true;
442
+ } catch (error) {
443
+ this.stats.totalErrors++;
444
+ this.bufferMessage(data);
445
+ return false;
446
+ }
447
+ }
448
+
449
+ bufferMessage(data) {
450
+ if (this.messageBuffer.length >= this.config.maxBufferedMessages) {
451
+ this.messageBuffer.shift();
452
+ }
453
+ this.messageBuffer.push(data);
454
+ this.emit('message_buffered', { bufferLength: this.messageBuffer.length });
455
+ }
456
+
457
+ flushMessageBuffer() {
458
+ if (this.messageBuffer.length === 0) return;
459
+ const messages = [...this.messageBuffer];
460
+ this.messageBuffer = [];
461
+ for (const message of messages) {
462
+ try {
463
+ this.ws.send(JSON.stringify(message));
464
+ this.stats.totalMessagesSent++;
465
+ } catch (error) {
466
+ this.bufferMessage(message);
467
+ }
468
+ }
469
+ this.emit('buffer_flushed', { count: messages.length });
470
+ }
471
+
472
+ subscribeToSession(sessionId) {
473
+ return this.sendMessage({ type: 'subscribe', sessionId, timestamp: Date.now() });
474
+ }
475
+
476
+ resubscribeAll() {
477
+ for (const key of this.activeSubscriptions) {
478
+ const [type, id] = key.split(':');
479
+ const msg = { type: 'subscribe', timestamp: Date.now() };
480
+ if (type === 'session') msg.sessionId = id;
481
+ else msg.conversationId = id;
482
+ try {
483
+ this.ws.send(JSON.stringify(msg));
484
+ this.stats.totalMessagesSent++;
485
+ } catch (_) {}
486
+ }
487
+ }
488
+
489
+ unsubscribeFromSession(sessionId) {
490
+ return this.sendMessage({ type: 'unsubscribe', sessionId, timestamp: Date.now() });
491
+ }
492
+
493
+ requestSessionHistory(sessionId, limit = 1000, offset = 0) {
494
+ return new Promise((resolve, reject) => {
495
+ const requestId = 'history-' + Date.now() + '-' + Math.random();
496
+ const timeout = setTimeout(() => {
497
+ this.requestMap.delete(requestId);
498
+ this.stats.totalTimeouts++;
499
+ reject(new Error('History request timeout'));
500
+ }, this.config.messageTimeout);
501
+
502
+ this.requestMap.set(requestId, {
503
+ type: 'history',
504
+ resolve: (d) => { clearTimeout(timeout); resolve(d); },
505
+ reject
506
+ });
507
+
508
+ this.sendMessage({
509
+ type: 'request_history', requestId, sessionId, limit, offset, timestamp: Date.now()
510
+ });
511
+ });
512
+ }
513
+
514
+ getLastSeq(sessionId) {
515
+ return this.lastSeqBySession[sessionId] || -1;
516
+ }
517
+
518
+ setConnectionState(state) {
519
+ this.connectionState = state;
520
+ this.emit('state_change', { state, timestamp: Date.now() });
521
+ }
522
+
523
+ disconnect() {
524
+ this.isManuallyDisconnected = true;
525
+ this.reconnectCount = 0;
526
+ this.stopHeartbeat();
527
+ if (this.reconnectTimer) {
528
+ clearTimeout(this.reconnectTimer);
529
+ this.reconnectTimer = null;
530
+ }
531
+ if (this.ws) this.ws.close();
532
+ this.messageBuffer = [];
533
+ this.requestMap.clear();
534
+ this.setConnectionState('disconnected');
535
+ }
536
+
537
+ getStatus() {
538
+ return {
539
+ isConnected: this.isConnected,
540
+ isConnecting: this.isConnecting,
541
+ connectionState: this.connectionState,
542
+ reconnectCount: this.reconnectCount,
543
+ bufferLength: this.messageBuffer.length,
544
+ latency: { ...this.latency, samples: undefined },
545
+ stats: { ...this.stats }
546
+ };
547
+ }
548
+
549
+ on(event, callback) {
550
+ if (!this.listeners[event]) this.listeners[event] = [];
551
+ this.listeners[event].push(callback);
552
+ }
553
+
554
+ off(event, callback) {
555
+ if (!this.listeners[event]) return;
556
+ const index = this.listeners[event].indexOf(callback);
557
+ if (index > -1) this.listeners[event].splice(index, 1);
558
+ }
559
+
560
+ emit(event, data) {
561
+ if (!this.listeners[event]) return;
562
+ this.listeners[event].forEach((cb) => {
563
+ try { cb(data); } catch (error) {}
564
+ });
565
+ }
566
+
567
+ destroy() {
568
+ if (typeof document !== 'undefined') {
569
+ document.removeEventListener('visibilitychange', this._onVisibilityChange);
570
+ }
571
+ if (typeof window !== 'undefined') {
572
+ window.removeEventListener('online', this._onOnline);
573
+ }
574
+ this.disconnect();
575
+ this.listeners = {};
576
+ }
577
+ subscribeToVoiceList(callback) {
578
+ if (!this.voiceListListeners.includes(callback)) {
579
+ this.voiceListListeners.push(callback);
580
+ }
581
+ if (this.cachedVoiceList !== null) {
582
+ callback(this.cachedVoiceList);
583
+ }
584
+ }
585
+
586
+ unsubscribeFromVoiceList(callback) {
587
+ const idx = this.voiceListListeners.indexOf(callback);
588
+ if (idx > -1) {
589
+ this.voiceListListeners.splice(idx, 1);
590
+ }
591
+ }
592
+ }
593
+
594
+ if (typeof module !== 'undefined' && module.exports) {
595
+ module.exports = WebSocketManager;
596
+ }