agentgui 1.0.211 → 1.0.213
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/database.js +18 -0
- package/package.json +1 -1
- package/server.js +128 -22
- package/static/index.html +79 -0
- package/static/js/agent-auth.js +47 -30
- package/static/js/client.js +299 -44
- package/static/js/websocket-manager.js +220 -216
|
@@ -1,34 +1,40 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* WebSocket Manager
|
|
3
|
-
* Handles WebSocket connection, auto-reconnect, message buffering,
|
|
4
|
-
* and event distribution for streaming events
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
1
|
class WebSocketManager {
|
|
8
2
|
constructor(config = {}) {
|
|
9
|
-
// Configuration
|
|
10
3
|
this.config = {
|
|
11
4
|
url: config.url || this.getWebSocketURL(),
|
|
12
|
-
reconnectDelays: config.reconnectDelays || [1000, 2000, 4000, 8000,
|
|
5
|
+
reconnectDelays: config.reconnectDelays || [500, 1000, 2000, 4000, 8000, 15000, 30000],
|
|
13
6
|
maxReconnectDelay: config.maxReconnectDelay || 30000,
|
|
14
|
-
heartbeatInterval: config.heartbeatInterval ||
|
|
7
|
+
heartbeatInterval: config.heartbeatInterval || 15000,
|
|
15
8
|
messageTimeout: config.messageTimeout || 60000,
|
|
16
9
|
maxBufferedMessages: config.maxBufferedMessages || 1000,
|
|
10
|
+
pongTimeout: config.pongTimeout || 5000,
|
|
11
|
+
latencyWindowSize: config.latencyWindowSize || 10,
|
|
17
12
|
...config
|
|
18
13
|
};
|
|
19
14
|
|
|
20
|
-
// State
|
|
21
15
|
this.ws = null;
|
|
22
16
|
this.isConnected = false;
|
|
23
17
|
this.isConnecting = false;
|
|
18
|
+
this.isManuallyDisconnected = false;
|
|
24
19
|
this.reconnectCount = 0;
|
|
20
|
+
this.reconnectTimer = null;
|
|
25
21
|
this.messageBuffer = [];
|
|
26
22
|
this.requestMap = new Map();
|
|
27
23
|
this.heartbeatTimer = null;
|
|
28
24
|
this.connectionState = 'disconnected';
|
|
29
25
|
this.activeSubscriptions = new Set();
|
|
26
|
+
this.connectionEstablishedAt = 0;
|
|
27
|
+
|
|
28
|
+
this.latency = {
|
|
29
|
+
samples: [],
|
|
30
|
+
current: 0,
|
|
31
|
+
avg: 0,
|
|
32
|
+
jitter: 0,
|
|
33
|
+
quality: 'unknown',
|
|
34
|
+
missedPongs: 0,
|
|
35
|
+
pingCounter: 0
|
|
36
|
+
};
|
|
30
37
|
|
|
31
|
-
// Statistics
|
|
32
38
|
this.stats = {
|
|
33
39
|
totalConnections: 0,
|
|
34
40
|
totalReconnects: 0,
|
|
@@ -41,100 +47,77 @@ class WebSocketManager {
|
|
|
41
47
|
connectionDuration: 0
|
|
42
48
|
};
|
|
43
49
|
|
|
44
|
-
|
|
50
|
+
this.lastSeqBySession = {};
|
|
45
51
|
this.listeners = {};
|
|
52
|
+
|
|
53
|
+
this._onVisibilityChange = this._handleVisibilityChange.bind(this);
|
|
54
|
+
this._onOnline = this._handleOnline.bind(this);
|
|
55
|
+
if (typeof document !== 'undefined') {
|
|
56
|
+
document.addEventListener('visibilitychange', this._onVisibilityChange);
|
|
57
|
+
}
|
|
58
|
+
if (typeof window !== 'undefined') {
|
|
59
|
+
window.addEventListener('online', this._onOnline);
|
|
60
|
+
}
|
|
46
61
|
}
|
|
47
62
|
|
|
48
|
-
/**
|
|
49
|
-
* Get WebSocket URL from current window location
|
|
50
|
-
*/
|
|
51
63
|
getWebSocketURL() {
|
|
52
64
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
53
65
|
const baseURL = window.__BASE_URL || '/gm';
|
|
54
66
|
return `${protocol}//${window.location.host}${baseURL}/sync`;
|
|
55
67
|
}
|
|
56
68
|
|
|
57
|
-
/**
|
|
58
|
-
* Connect to WebSocket server
|
|
59
|
-
*/
|
|
60
69
|
async connect() {
|
|
61
|
-
if (this.isConnected || this.isConnecting)
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
70
|
+
if (this.isConnected || this.isConnecting) return this.ws;
|
|
71
|
+
this.isManuallyDisconnected = false;
|
|
65
72
|
this.isConnecting = true;
|
|
66
73
|
this.setConnectionState('connecting');
|
|
67
74
|
|
|
68
75
|
try {
|
|
69
|
-
console.log('WebSocket connecting to:', this.config.url);
|
|
70
|
-
|
|
71
76
|
this.ws = new WebSocket(this.config.url);
|
|
72
|
-
|
|
73
77
|
this.ws.onopen = () => this.onOpen();
|
|
74
78
|
this.ws.onmessage = (event) => this.onMessage(event);
|
|
75
79
|
this.ws.onerror = (error) => this.onError(error);
|
|
76
80
|
this.ws.onclose = () => this.onClose();
|
|
77
|
-
|
|
78
|
-
// Wait for connection with timeout
|
|
79
81
|
return await this.waitForConnection(this.config.messageTimeout);
|
|
80
82
|
} catch (error) {
|
|
81
|
-
console.error('WebSocket connection error:', error);
|
|
82
83
|
this.isConnecting = false;
|
|
83
84
|
this.stats.totalErrors++;
|
|
84
|
-
|
|
85
|
+
this.scheduleReconnect();
|
|
85
86
|
throw error;
|
|
86
87
|
}
|
|
87
88
|
}
|
|
88
89
|
|
|
89
|
-
/**
|
|
90
|
-
* Wait for connection to establish
|
|
91
|
-
*/
|
|
92
90
|
waitForConnection(timeout = 5000) {
|
|
93
91
|
return new Promise((resolve, reject) => {
|
|
94
|
-
const timer = setTimeout(() =>
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const checkConnection = () => {
|
|
99
|
-
if (this.isConnected) {
|
|
100
|
-
clearTimeout(timer);
|
|
101
|
-
resolve(this.ws);
|
|
102
|
-
} else if (this.ws?.readyState === WebSocket.OPEN) {
|
|
92
|
+
const timer = setTimeout(() => reject(new Error('WebSocket connection timeout')), timeout);
|
|
93
|
+
const check = () => {
|
|
94
|
+
if (this.isConnected || this.ws?.readyState === WebSocket.OPEN) {
|
|
103
95
|
clearTimeout(timer);
|
|
104
96
|
resolve(this.ws);
|
|
105
97
|
} else {
|
|
106
|
-
setTimeout(
|
|
98
|
+
setTimeout(check, 50);
|
|
107
99
|
}
|
|
108
100
|
};
|
|
109
|
-
|
|
110
|
-
checkConnection();
|
|
101
|
+
check();
|
|
111
102
|
});
|
|
112
103
|
}
|
|
113
104
|
|
|
114
|
-
/**
|
|
115
|
-
* Handle WebSocket open
|
|
116
|
-
*/
|
|
117
105
|
onOpen() {
|
|
118
|
-
console.log('WebSocket connected');
|
|
119
106
|
this.isConnected = true;
|
|
120
107
|
this.isConnecting = false;
|
|
121
|
-
this.
|
|
108
|
+
this.connectionEstablishedAt = Date.now();
|
|
122
109
|
this.stats.totalConnections++;
|
|
123
110
|
this.stats.lastConnectedTime = Date.now();
|
|
111
|
+
this.latency.missedPongs = 0;
|
|
124
112
|
this.setConnectionState('connected');
|
|
125
113
|
|
|
126
|
-
// Flush buffered messages
|
|
127
114
|
this.flushMessageBuffer();
|
|
128
115
|
this.resubscribeAll();
|
|
129
|
-
|
|
130
116
|
this.startHeartbeat();
|
|
131
117
|
|
|
132
118
|
this.emit('connected', { timestamp: Date.now() });
|
|
133
119
|
}
|
|
134
120
|
|
|
135
|
-
/**
|
|
136
|
-
* Handle WebSocket message
|
|
137
|
-
*/
|
|
138
121
|
onMessage(event) {
|
|
139
122
|
try {
|
|
140
123
|
const parsed = JSON.parse(event.data);
|
|
@@ -143,139 +126,221 @@ class WebSocketManager {
|
|
|
143
126
|
|
|
144
127
|
for (const data of messages) {
|
|
145
128
|
if (data.type === 'pong') {
|
|
146
|
-
|
|
147
|
-
if (requestId && this.requestMap.has(requestId)) {
|
|
148
|
-
const request = this.requestMap.get(requestId);
|
|
149
|
-
request.resolve({ latency: Date.now() - request.sentTime });
|
|
150
|
-
this.requestMap.delete(requestId);
|
|
151
|
-
}
|
|
129
|
+
this._handlePong(data);
|
|
152
130
|
continue;
|
|
153
131
|
}
|
|
154
132
|
|
|
133
|
+
if (data.seq !== undefined && data.sessionId) {
|
|
134
|
+
this.lastSeqBySession[data.sessionId] = Math.max(
|
|
135
|
+
this.lastSeqBySession[data.sessionId] || -1, data.seq
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
155
139
|
this.emit('message', data);
|
|
156
|
-
if (data.type) this.emit(
|
|
140
|
+
if (data.type) this.emit('message:' + data.type, data);
|
|
157
141
|
}
|
|
158
142
|
} catch (error) {
|
|
159
|
-
console.error('WebSocket message parse error:', error);
|
|
160
143
|
this.stats.totalErrors++;
|
|
161
144
|
}
|
|
162
145
|
}
|
|
163
146
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
147
|
+
_handlePong(data) {
|
|
148
|
+
this.latency.missedPongs = 0;
|
|
149
|
+
const requestId = data.requestId;
|
|
150
|
+
if (requestId && this.requestMap.has(requestId)) {
|
|
151
|
+
const request = this.requestMap.get(requestId);
|
|
152
|
+
const rtt = Date.now() - request.sentTime;
|
|
153
|
+
this.requestMap.delete(requestId);
|
|
154
|
+
this._recordLatency(rtt);
|
|
155
|
+
if (request.resolve) request.resolve({ latency: rtt });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
_recordLatency(rtt) {
|
|
160
|
+
const samples = this.latency.samples;
|
|
161
|
+
samples.push(rtt);
|
|
162
|
+
if (samples.length > this.config.latencyWindowSize) samples.shift();
|
|
163
|
+
|
|
164
|
+
this.latency.current = rtt;
|
|
165
|
+
this.latency.avg = samples.reduce((a, b) => a + b, 0) / samples.length;
|
|
166
|
+
|
|
167
|
+
if (samples.length > 1) {
|
|
168
|
+
const mean = this.latency.avg;
|
|
169
|
+
const variance = samples.reduce((sum, s) => sum + Math.pow(s - mean, 2), 0) / samples.length;
|
|
170
|
+
this.latency.jitter = Math.sqrt(variance);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const prev = this.latency.quality;
|
|
174
|
+
this.latency.quality = this._qualityTier(this.latency.avg);
|
|
175
|
+
this.stats.avgLatency = this.latency.avg;
|
|
176
|
+
|
|
177
|
+
this.emit('latency_update', {
|
|
178
|
+
latency: rtt,
|
|
179
|
+
avg: this.latency.avg,
|
|
180
|
+
jitter: this.latency.jitter,
|
|
181
|
+
quality: this.latency.quality
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
if (rtt > this.latency.avg * 3 && samples.length >= 3) {
|
|
185
|
+
this.emit('latency_spike', { latency: rtt, avg: this.latency.avg });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
_qualityTier(avg) {
|
|
190
|
+
if (avg < 50) return 'excellent';
|
|
191
|
+
if (avg < 150) return 'good';
|
|
192
|
+
if (avg < 300) return 'fair';
|
|
193
|
+
if (avg < 500) return 'poor';
|
|
194
|
+
return 'bad';
|
|
195
|
+
}
|
|
196
|
+
|
|
167
197
|
onError(error) {
|
|
168
|
-
console.error('WebSocket error:', error);
|
|
169
198
|
this.stats.totalErrors++;
|
|
170
199
|
this.emit('error', { error, timestamp: Date.now() });
|
|
171
200
|
}
|
|
172
201
|
|
|
173
|
-
/**
|
|
174
|
-
* Handle WebSocket close
|
|
175
|
-
*/
|
|
176
202
|
onClose() {
|
|
177
|
-
console.log('WebSocket disconnected');
|
|
178
203
|
this.isConnected = false;
|
|
179
204
|
this.isConnecting = false;
|
|
180
205
|
this.setConnectionState('disconnected');
|
|
206
|
+
this.stopHeartbeat();
|
|
181
207
|
|
|
182
|
-
// Stop heartbeat
|
|
183
|
-
if (this.heartbeatTimer) {
|
|
184
|
-
clearTimeout(this.heartbeatTimer);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Update connection duration
|
|
188
208
|
if (this.stats.lastConnectedTime) {
|
|
189
209
|
this.stats.connectionDuration = Date.now() - this.stats.lastConnectedTime;
|
|
190
210
|
}
|
|
191
211
|
|
|
192
212
|
this.emit('disconnected', { timestamp: Date.now() });
|
|
193
213
|
|
|
194
|
-
// Attempt reconnect
|
|
195
214
|
if (!this.isManuallyDisconnected) {
|
|
196
215
|
this.scheduleReconnect();
|
|
197
216
|
}
|
|
198
217
|
}
|
|
199
218
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
219
|
+
scheduleReconnect() {
|
|
220
|
+
if (this.isManuallyDisconnected) return;
|
|
221
|
+
if (this.reconnectTimer) return;
|
|
222
|
+
|
|
223
|
+
const delays = this.config.reconnectDelays;
|
|
224
|
+
const baseDelay = this.reconnectCount < delays.length
|
|
225
|
+
? delays[this.reconnectCount]
|
|
226
|
+
: this.config.maxReconnectDelay;
|
|
227
|
+
|
|
228
|
+
const jitter = Math.random() * 0.3 * baseDelay;
|
|
229
|
+
const delay = Math.round(baseDelay + jitter);
|
|
210
230
|
|
|
211
|
-
const delay = this.config.reconnectDelays[this.reconnectCount];
|
|
212
231
|
this.reconnectCount++;
|
|
213
232
|
this.stats.totalReconnects++;
|
|
214
233
|
this.setConnectionState('reconnecting');
|
|
215
234
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
return new Promise((resolve) => {
|
|
221
|
-
setTimeout(() => {
|
|
222
|
-
this.connect().catch((error) => {
|
|
223
|
-
console.error('Reconnection attempt failed:', error);
|
|
224
|
-
});
|
|
225
|
-
resolve();
|
|
226
|
-
}, delay);
|
|
235
|
+
this.emit('reconnecting', {
|
|
236
|
+
delay,
|
|
237
|
+
attempt: this.reconnectCount,
|
|
238
|
+
nextAttemptAt: Date.now() + delay
|
|
227
239
|
});
|
|
240
|
+
|
|
241
|
+
this.reconnectTimer = setTimeout(() => {
|
|
242
|
+
this.reconnectTimer = null;
|
|
243
|
+
this.connect().catch(() => {});
|
|
244
|
+
}, delay);
|
|
228
245
|
}
|
|
229
246
|
|
|
230
|
-
/**
|
|
231
|
-
* Start heartbeat/keepalive
|
|
232
|
-
*/
|
|
233
247
|
startHeartbeat() {
|
|
234
|
-
|
|
248
|
+
this.stopHeartbeat();
|
|
249
|
+
const tick = () => {
|
|
250
|
+
if (!this.isConnected) return;
|
|
251
|
+
if (typeof document !== 'undefined' && document.hidden) {
|
|
252
|
+
this.heartbeatTimer = setTimeout(tick, this.config.heartbeatInterval);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
this.latency.pingCounter++;
|
|
256
|
+
this.ping().catch(() => {
|
|
257
|
+
this.latency.missedPongs++;
|
|
258
|
+
if (this.latency.missedPongs >= 3) {
|
|
259
|
+
this.latency.missedPongs = 0;
|
|
260
|
+
if (this.ws) {
|
|
261
|
+
try { this.ws.close(); } catch (_) {}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
if (this.latency.pingCounter % 10 === 0) {
|
|
266
|
+
this._reportLatency();
|
|
267
|
+
}
|
|
268
|
+
this.heartbeatTimer = setTimeout(tick, this.config.heartbeatInterval);
|
|
269
|
+
};
|
|
270
|
+
this.heartbeatTimer = setTimeout(tick, this.config.heartbeatInterval);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
stopHeartbeat() {
|
|
274
|
+
if (this.heartbeatTimer) {
|
|
275
|
+
clearTimeout(this.heartbeatTimer);
|
|
276
|
+
this.heartbeatTimer = null;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
_reportLatency() {
|
|
281
|
+
if (this.latency.avg > 0) {
|
|
282
|
+
this.sendMessage({
|
|
283
|
+
type: 'latency_report',
|
|
284
|
+
avg: Math.round(this.latency.avg),
|
|
285
|
+
jitter: Math.round(this.latency.jitter),
|
|
286
|
+
quality: this.latency.quality
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
_handleVisibilityChange() {
|
|
292
|
+
if (typeof document !== 'undefined' && document.hidden) return;
|
|
293
|
+
if (!this.isConnected && !this.isConnecting && !this.isManuallyDisconnected) {
|
|
294
|
+
if (this.reconnectTimer) {
|
|
295
|
+
clearTimeout(this.reconnectTimer);
|
|
296
|
+
this.reconnectTimer = null;
|
|
297
|
+
}
|
|
298
|
+
this.connect().catch(() => {});
|
|
299
|
+
}
|
|
300
|
+
if (this.isConnected) {
|
|
301
|
+
const stableFor = Date.now() - this.connectionEstablishedAt;
|
|
302
|
+
if (stableFor > 10000) this.reconnectCount = 0;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
_handleOnline() {
|
|
307
|
+
if (!this.isConnected && !this.isConnecting && !this.isManuallyDisconnected) {
|
|
308
|
+
if (this.reconnectTimer) {
|
|
309
|
+
clearTimeout(this.reconnectTimer);
|
|
310
|
+
this.reconnectTimer = null;
|
|
311
|
+
}
|
|
312
|
+
this.connect().catch(() => {});
|
|
313
|
+
}
|
|
235
314
|
}
|
|
236
315
|
|
|
237
|
-
/**
|
|
238
|
-
* Send ping message
|
|
239
|
-
*/
|
|
240
316
|
ping() {
|
|
241
|
-
const requestId =
|
|
242
|
-
const request = {
|
|
243
|
-
sentTime: Date.now(),
|
|
244
|
-
resolve: null
|
|
245
|
-
};
|
|
317
|
+
const requestId = 'ping-' + Date.now() + '-' + Math.random();
|
|
318
|
+
const request = { sentTime: Date.now(), resolve: null };
|
|
246
319
|
|
|
247
|
-
const promise = new Promise((resolve) => {
|
|
320
|
+
const promise = new Promise((resolve, reject) => {
|
|
248
321
|
request.resolve = resolve;
|
|
322
|
+
setTimeout(() => {
|
|
323
|
+
if (this.requestMap.has(requestId)) {
|
|
324
|
+
this.stats.totalTimeouts++;
|
|
325
|
+
this.requestMap.delete(requestId);
|
|
326
|
+
reject(new Error('ping timeout'));
|
|
327
|
+
}
|
|
328
|
+
}, this.config.pongTimeout);
|
|
249
329
|
});
|
|
250
330
|
|
|
251
331
|
this.requestMap.set(requestId, request);
|
|
252
|
-
|
|
253
|
-
// Timeout if no response
|
|
254
|
-
setTimeout(() => {
|
|
255
|
-
if (this.requestMap.has(requestId)) {
|
|
256
|
-
this.stats.totalTimeouts++;
|
|
257
|
-
this.requestMap.delete(requestId);
|
|
258
|
-
this.emit('ping_timeout', { requestId });
|
|
259
|
-
}
|
|
260
|
-
}, 5000);
|
|
261
|
-
|
|
262
332
|
this.sendMessage({ type: 'ping', requestId });
|
|
263
333
|
return promise;
|
|
264
334
|
}
|
|
265
335
|
|
|
266
|
-
/**
|
|
267
|
-
* Send message through WebSocket
|
|
268
|
-
*/
|
|
269
336
|
sendMessage(data) {
|
|
270
|
-
if (!data || typeof data !== 'object')
|
|
271
|
-
throw new Error('Invalid message data');
|
|
272
|
-
}
|
|
337
|
+
if (!data || typeof data !== 'object') throw new Error('Invalid message data');
|
|
273
338
|
|
|
274
339
|
if (data.type === 'subscribe') {
|
|
275
|
-
const key = data.sessionId ?
|
|
340
|
+
const key = data.sessionId ? 'session:' + data.sessionId : 'conv:' + data.conversationId;
|
|
276
341
|
this.activeSubscriptions.add(key);
|
|
277
342
|
} else if (data.type === 'unsubscribe') {
|
|
278
|
-
const key = data.sessionId ?
|
|
343
|
+
const key = data.sessionId ? 'session:' + data.sessionId : 'conv:' + data.conversationId;
|
|
279
344
|
this.activeSubscriptions.delete(key);
|
|
280
345
|
}
|
|
281
346
|
|
|
@@ -289,57 +354,37 @@ class WebSocketManager {
|
|
|
289
354
|
this.stats.totalMessagesSent++;
|
|
290
355
|
return true;
|
|
291
356
|
} catch (error) {
|
|
292
|
-
console.error('WebSocket send error:', error);
|
|
293
357
|
this.stats.totalErrors++;
|
|
294
358
|
this.bufferMessage(data);
|
|
295
359
|
return false;
|
|
296
360
|
}
|
|
297
361
|
}
|
|
298
362
|
|
|
299
|
-
/**
|
|
300
|
-
* Buffer message for sending when connected
|
|
301
|
-
*/
|
|
302
363
|
bufferMessage(data) {
|
|
303
364
|
if (this.messageBuffer.length >= this.config.maxBufferedMessages) {
|
|
304
|
-
console.warn('Message buffer full, dropping oldest message');
|
|
305
365
|
this.messageBuffer.shift();
|
|
306
366
|
}
|
|
307
367
|
this.messageBuffer.push(data);
|
|
308
368
|
this.emit('message_buffered', { bufferLength: this.messageBuffer.length });
|
|
309
369
|
}
|
|
310
370
|
|
|
311
|
-
/**
|
|
312
|
-
* Flush buffered messages
|
|
313
|
-
*/
|
|
314
371
|
flushMessageBuffer() {
|
|
315
372
|
if (this.messageBuffer.length === 0) return;
|
|
316
|
-
|
|
317
|
-
console.log(`Flushing ${this.messageBuffer.length} buffered messages`);
|
|
318
373
|
const messages = [...this.messageBuffer];
|
|
319
374
|
this.messageBuffer = [];
|
|
320
|
-
|
|
321
375
|
for (const message of messages) {
|
|
322
376
|
try {
|
|
323
377
|
this.ws.send(JSON.stringify(message));
|
|
324
378
|
this.stats.totalMessagesSent++;
|
|
325
379
|
} catch (error) {
|
|
326
|
-
console.error('Error sending buffered message:', error);
|
|
327
380
|
this.bufferMessage(message);
|
|
328
381
|
}
|
|
329
382
|
}
|
|
330
|
-
|
|
331
383
|
this.emit('buffer_flushed', { count: messages.length });
|
|
332
384
|
}
|
|
333
385
|
|
|
334
|
-
/**
|
|
335
|
-
* Subscribe to streaming session
|
|
336
|
-
*/
|
|
337
386
|
subscribeToSession(sessionId) {
|
|
338
|
-
return this.sendMessage({
|
|
339
|
-
type: 'subscribe',
|
|
340
|
-
sessionId,
|
|
341
|
-
timestamp: Date.now()
|
|
342
|
-
});
|
|
387
|
+
return this.sendMessage({ type: 'subscribe', sessionId, timestamp: Date.now() });
|
|
343
388
|
}
|
|
344
389
|
|
|
345
390
|
resubscribeAll() {
|
|
@@ -355,24 +400,13 @@ class WebSocketManager {
|
|
|
355
400
|
}
|
|
356
401
|
}
|
|
357
402
|
|
|
358
|
-
/**
|
|
359
|
-
* Unsubscribe from streaming session
|
|
360
|
-
*/
|
|
361
403
|
unsubscribeFromSession(sessionId) {
|
|
362
|
-
return this.sendMessage({
|
|
363
|
-
type: 'unsubscribe',
|
|
364
|
-
sessionId,
|
|
365
|
-
timestamp: Date.now()
|
|
366
|
-
});
|
|
404
|
+
return this.sendMessage({ type: 'unsubscribe', sessionId, timestamp: Date.now() });
|
|
367
405
|
}
|
|
368
406
|
|
|
369
|
-
/**
|
|
370
|
-
* Request session history
|
|
371
|
-
*/
|
|
372
407
|
requestSessionHistory(sessionId, limit = 1000, offset = 0) {
|
|
373
408
|
return new Promise((resolve, reject) => {
|
|
374
|
-
const requestId =
|
|
375
|
-
|
|
409
|
+
const requestId = 'history-' + Date.now() + '-' + Math.random();
|
|
376
410
|
const timeout = setTimeout(() => {
|
|
377
411
|
this.requestMap.delete(requestId);
|
|
378
412
|
this.stats.totalTimeouts++;
|
|
@@ -381,55 +415,39 @@ class WebSocketManager {
|
|
|
381
415
|
|
|
382
416
|
this.requestMap.set(requestId, {
|
|
383
417
|
type: 'history',
|
|
384
|
-
resolve: (
|
|
385
|
-
clearTimeout(timeout);
|
|
386
|
-
resolve(data);
|
|
387
|
-
},
|
|
418
|
+
resolve: (d) => { clearTimeout(timeout); resolve(d); },
|
|
388
419
|
reject
|
|
389
420
|
});
|
|
390
421
|
|
|
391
422
|
this.sendMessage({
|
|
392
|
-
type: 'request_history',
|
|
393
|
-
requestId,
|
|
394
|
-
sessionId,
|
|
395
|
-
limit,
|
|
396
|
-
offset,
|
|
397
|
-
timestamp: Date.now()
|
|
423
|
+
type: 'request_history', requestId, sessionId, limit, offset, timestamp: Date.now()
|
|
398
424
|
});
|
|
399
425
|
});
|
|
400
426
|
}
|
|
401
427
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
428
|
+
getLastSeq(sessionId) {
|
|
429
|
+
return this.lastSeqBySession[sessionId] || -1;
|
|
430
|
+
}
|
|
431
|
+
|
|
405
432
|
setConnectionState(state) {
|
|
406
433
|
this.connectionState = state;
|
|
407
434
|
this.emit('state_change', { state, timestamp: Date.now() });
|
|
408
435
|
}
|
|
409
436
|
|
|
410
|
-
/**
|
|
411
|
-
* Disconnect manually
|
|
412
|
-
*/
|
|
413
437
|
disconnect() {
|
|
414
438
|
this.isManuallyDisconnected = true;
|
|
415
439
|
this.reconnectCount = 0;
|
|
416
|
-
|
|
417
|
-
if (this.
|
|
418
|
-
clearTimeout(this.
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
if (this.ws) {
|
|
422
|
-
this.ws.close();
|
|
440
|
+
this.stopHeartbeat();
|
|
441
|
+
if (this.reconnectTimer) {
|
|
442
|
+
clearTimeout(this.reconnectTimer);
|
|
443
|
+
this.reconnectTimer = null;
|
|
423
444
|
}
|
|
424
|
-
|
|
445
|
+
if (this.ws) this.ws.close();
|
|
425
446
|
this.messageBuffer = [];
|
|
426
447
|
this.requestMap.clear();
|
|
427
448
|
this.setConnectionState('disconnected');
|
|
428
449
|
}
|
|
429
450
|
|
|
430
|
-
/**
|
|
431
|
-
* Get connection status
|
|
432
|
-
*/
|
|
433
451
|
getStatus() {
|
|
434
452
|
return {
|
|
435
453
|
isConnected: this.isConnected,
|
|
@@ -437,55 +455,41 @@ class WebSocketManager {
|
|
|
437
455
|
connectionState: this.connectionState,
|
|
438
456
|
reconnectCount: this.reconnectCount,
|
|
439
457
|
bufferLength: this.messageBuffer.length,
|
|
458
|
+
latency: { ...this.latency, samples: undefined },
|
|
440
459
|
stats: { ...this.stats }
|
|
441
460
|
};
|
|
442
461
|
}
|
|
443
462
|
|
|
444
|
-
/**
|
|
445
|
-
* Add event listener
|
|
446
|
-
*/
|
|
447
463
|
on(event, callback) {
|
|
448
|
-
if (!this.listeners[event])
|
|
449
|
-
this.listeners[event] = [];
|
|
450
|
-
}
|
|
464
|
+
if (!this.listeners[event]) this.listeners[event] = [];
|
|
451
465
|
this.listeners[event].push(callback);
|
|
452
466
|
}
|
|
453
467
|
|
|
454
|
-
/**
|
|
455
|
-
* Remove event listener
|
|
456
|
-
*/
|
|
457
468
|
off(event, callback) {
|
|
458
469
|
if (!this.listeners[event]) return;
|
|
459
470
|
const index = this.listeners[event].indexOf(callback);
|
|
460
|
-
if (index > -1)
|
|
461
|
-
this.listeners[event].splice(index, 1);
|
|
462
|
-
}
|
|
471
|
+
if (index > -1) this.listeners[event].splice(index, 1);
|
|
463
472
|
}
|
|
464
473
|
|
|
465
|
-
/**
|
|
466
|
-
* Emit event
|
|
467
|
-
*/
|
|
468
474
|
emit(event, data) {
|
|
469
475
|
if (!this.listeners[event]) return;
|
|
470
|
-
this.listeners[event].forEach((
|
|
471
|
-
try {
|
|
472
|
-
callback(data);
|
|
473
|
-
} catch (error) {
|
|
474
|
-
console.error(`Listener error for event ${event}:`, error);
|
|
475
|
-
}
|
|
476
|
+
this.listeners[event].forEach((cb) => {
|
|
477
|
+
try { cb(data); } catch (error) {}
|
|
476
478
|
});
|
|
477
479
|
}
|
|
478
480
|
|
|
479
|
-
/**
|
|
480
|
-
* Cleanup resources
|
|
481
|
-
*/
|
|
482
481
|
destroy() {
|
|
482
|
+
if (typeof document !== 'undefined') {
|
|
483
|
+
document.removeEventListener('visibilitychange', this._onVisibilityChange);
|
|
484
|
+
}
|
|
485
|
+
if (typeof window !== 'undefined') {
|
|
486
|
+
window.removeEventListener('online', this._onOnline);
|
|
487
|
+
}
|
|
483
488
|
this.disconnect();
|
|
484
489
|
this.listeners = {};
|
|
485
490
|
}
|
|
486
491
|
}
|
|
487
492
|
|
|
488
|
-
// Export for use in browser
|
|
489
493
|
if (typeof module !== 'undefined' && module.exports) {
|
|
490
494
|
module.exports = WebSocketManager;
|
|
491
495
|
}
|