agentgui 1.0.394 → 1.0.395
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/lib/ws-optimizer.js +30 -110
- package/package.json +1 -1
package/lib/ws-optimizer.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
// WebSocket Optimization Module
|
|
2
|
-
// Implements batching, rate limiting, compression, deduplication, priority queuing, and monitoring
|
|
3
|
-
|
|
4
1
|
import zlib from 'zlib';
|
|
5
2
|
|
|
6
3
|
const MESSAGE_PRIORITY = {
|
|
@@ -13,7 +10,20 @@ function getPriority(eventType) {
|
|
|
13
10
|
if (MESSAGE_PRIORITY.high.includes(eventType)) return 3;
|
|
14
11
|
if (MESSAGE_PRIORITY.normal.includes(eventType)) return 2;
|
|
15
12
|
if (MESSAGE_PRIORITY.low.includes(eventType)) return 1;
|
|
16
|
-
return 2;
|
|
13
|
+
return 2;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getBatchInterval(ws) {
|
|
17
|
+
const BATCH_BY_TIER = { excellent: 16, good: 32, fair: 50, poor: 100, bad: 200 };
|
|
18
|
+
const TIER_ORDER = ['excellent', 'good', 'fair', 'poor', 'bad'];
|
|
19
|
+
const tier = ws.latencyTier || 'good';
|
|
20
|
+
const trend = ws.latencyTrend;
|
|
21
|
+
if (trend === 'rising' || trend === 'falling') {
|
|
22
|
+
const idx = TIER_ORDER.indexOf(tier);
|
|
23
|
+
if (trend === 'rising' && idx < TIER_ORDER.length - 1) return BATCH_BY_TIER[TIER_ORDER[idx + 1]] || 32;
|
|
24
|
+
if (trend === 'falling' && idx > 0) return BATCH_BY_TIER[TIER_ORDER[idx - 1]] || 32;
|
|
25
|
+
}
|
|
26
|
+
return BATCH_BY_TIER[tier] || 32;
|
|
17
27
|
}
|
|
18
28
|
|
|
19
29
|
class ClientQueue {
|
|
@@ -31,151 +41,76 @@ class ClientQueue {
|
|
|
31
41
|
}
|
|
32
42
|
|
|
33
43
|
add(data, priority) {
|
|
34
|
-
// Deduplication: skip if identical to last message
|
|
35
44
|
if (this.lastMessage === data) return;
|
|
36
45
|
this.lastMessage = data;
|
|
37
|
-
|
|
38
|
-
if (priority ===
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
} else {
|
|
43
|
-
this.lowPriority.push(data);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// High priority: flush immediately
|
|
47
|
-
if (priority === 3) {
|
|
48
|
-
this.flushImmediate();
|
|
49
|
-
} else if (!this.timer) {
|
|
50
|
-
this.scheduleFlush();
|
|
51
|
-
}
|
|
46
|
+
if (priority === 3) this.highPriority.push(data);
|
|
47
|
+
else if (priority === 2) this.normalPriority.push(data);
|
|
48
|
+
else this.lowPriority.push(data);
|
|
49
|
+
if (priority === 3) this.flushImmediate();
|
|
50
|
+
else if (!this.timer) this.scheduleFlush();
|
|
52
51
|
}
|
|
53
52
|
|
|
54
53
|
scheduleFlush() {
|
|
55
54
|
const interval = this.ws.latencyTier ? getBatchInterval(this.ws) : 100;
|
|
56
|
-
this.timer = setTimeout(() => {
|
|
57
|
-
this.timer = null;
|
|
58
|
-
this.flush();
|
|
59
|
-
}, interval);
|
|
55
|
+
this.timer = setTimeout(() => { this.timer = null; this.flush(); }, interval);
|
|
60
56
|
}
|
|
61
57
|
|
|
62
58
|
flushImmediate() {
|
|
63
|
-
if (this.timer) {
|
|
64
|
-
clearTimeout(this.timer);
|
|
65
|
-
this.timer = null;
|
|
66
|
-
}
|
|
59
|
+
if (this.timer) { clearTimeout(this.timer); this.timer = null; }
|
|
67
60
|
this.flush();
|
|
68
61
|
}
|
|
69
62
|
|
|
70
63
|
flush() {
|
|
71
64
|
if (this.ws.readyState !== 1) return;
|
|
72
|
-
|
|
73
65
|
const now = Date.now();
|
|
74
66
|
const windowDuration = now - this.windowStart;
|
|
75
|
-
|
|
76
|
-
// Reset rate limit window every second
|
|
77
67
|
if (windowDuration >= 1000) {
|
|
78
68
|
this.messageCount = 0;
|
|
79
69
|
this.bytesSent = 0;
|
|
80
70
|
this.windowStart = now;
|
|
81
71
|
this.rateLimitWarned = false;
|
|
82
72
|
}
|
|
83
|
-
|
|
84
|
-
// Collect messages from all priorities (high first)
|
|
85
|
-
const batch = [
|
|
86
|
-
...this.highPriority.splice(0),
|
|
87
|
-
...this.normalPriority.splice(0, 10),
|
|
88
|
-
...this.lowPriority.splice(0, 5)
|
|
89
|
-
];
|
|
90
|
-
|
|
73
|
+
const batch = [...this.highPriority.splice(0), ...this.normalPriority.splice(0, 10), ...this.lowPriority.splice(0, 5)];
|
|
91
74
|
if (batch.length === 0) return;
|
|
92
|
-
|
|
93
|
-
// Rate limiting: max 100 msg/sec per client
|
|
94
75
|
const messagesThisSecond = this.messageCount + batch.length;
|
|
95
76
|
if (messagesThisSecond > 100) {
|
|
96
77
|
if (!this.rateLimitWarned) {
|
|
97
78
|
console.warn(`[ws-optimizer] Client ${this.ws.clientId} rate limited: ${messagesThisSecond} msg/sec`);
|
|
98
79
|
this.rateLimitWarned = true;
|
|
99
80
|
}
|
|
100
|
-
// Keep high priority, drop some normal/low
|
|
101
81
|
const allowedCount = 100 - this.messageCount;
|
|
102
|
-
if (allowedCount <= 0) {
|
|
103
|
-
// Reschedule remaining
|
|
104
|
-
this.scheduleFlush();
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
82
|
+
if (allowedCount <= 0) { this.scheduleFlush(); return; }
|
|
107
83
|
batch.splice(allowedCount);
|
|
108
84
|
}
|
|
109
|
-
|
|
110
|
-
let payload;
|
|
111
|
-
if (batch.length === 1) {
|
|
112
|
-
payload = batch[0];
|
|
113
|
-
} else {
|
|
114
|
-
payload = '[' + batch.join(',') + ']';
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Compression for large payloads (>1KB)
|
|
85
|
+
let payload = batch.length === 1 ? batch[0] : '[' + batch.join(',') + ']';
|
|
118
86
|
if (payload.length > 1024) {
|
|
119
87
|
try {
|
|
120
88
|
const compressed = zlib.gzipSync(Buffer.from(payload), { level: 6 });
|
|
121
89
|
if (compressed.length < payload.length * 0.9) {
|
|
122
|
-
// Send compression hint as separate control message
|
|
123
90
|
this.ws.send(JSON.stringify({ type: '_compressed', encoding: 'gzip' }));
|
|
124
91
|
this.ws.send(compressed);
|
|
125
|
-
payload = null;
|
|
92
|
+
payload = null;
|
|
126
93
|
}
|
|
127
|
-
} catch (e) {
|
|
128
|
-
// Fall back to uncompressed
|
|
129
|
-
}
|
|
94
|
+
} catch (e) {}
|
|
130
95
|
}
|
|
131
|
-
|
|
132
|
-
if (payload) {
|
|
133
|
-
this.ws.send(payload);
|
|
134
|
-
}
|
|
135
|
-
|
|
96
|
+
if (payload) this.ws.send(payload);
|
|
136
97
|
this.messageCount += batch.length;
|
|
137
98
|
this.bytesSent += (payload ? payload.length : 0);
|
|
138
|
-
|
|
139
|
-
// Monitor: warn if >1MB/sec sustained for 3+ seconds
|
|
140
99
|
if (windowDuration >= 3000 && this.bytesSent > 3 * 1024 * 1024) {
|
|
141
100
|
const mbps = (this.bytesSent / windowDuration * 1000 / 1024 / 1024).toFixed(2);
|
|
142
101
|
console.warn(`[ws-optimizer] Client ${this.ws.clientId} high bandwidth: ${mbps} MB/sec`);
|
|
143
102
|
}
|
|
144
|
-
|
|
145
|
-
// If there are remaining low-priority messages, schedule next flush
|
|
146
103
|
if (this.normalPriority.length > 0 || this.lowPriority.length > 0) {
|
|
147
104
|
if (!this.timer) this.scheduleFlush();
|
|
148
105
|
}
|
|
149
106
|
}
|
|
150
107
|
|
|
151
108
|
drain() {
|
|
152
|
-
if (this.timer) {
|
|
153
|
-
clearTimeout(this.timer);
|
|
154
|
-
this.timer = null;
|
|
155
|
-
}
|
|
109
|
+
if (this.timer) { clearTimeout(this.timer); this.timer = null; }
|
|
156
110
|
this.flush();
|
|
157
111
|
}
|
|
158
112
|
}
|
|
159
113
|
|
|
160
|
-
function getBatchInterval(ws) {
|
|
161
|
-
const BATCH_BY_TIER = { excellent: 16, good: 32, fair: 50, poor: 100, bad: 200 };
|
|
162
|
-
const TIER_ORDER = ['excellent', 'good', 'fair', 'poor', 'bad'];
|
|
163
|
-
const tier = ws.latencyTier || 'good';
|
|
164
|
-
const trend = ws.latencyTrend;
|
|
165
|
-
|
|
166
|
-
if (trend === 'rising' || trend === 'falling') {
|
|
167
|
-
const idx = TIER_ORDER.indexOf(tier);
|
|
168
|
-
if (trend === 'rising' && idx < TIER_ORDER.length - 1) {
|
|
169
|
-
return BATCH_BY_TIER[TIER_ORDER[idx + 1]] || 32;
|
|
170
|
-
}
|
|
171
|
-
if (trend === 'falling' && idx > 0) {
|
|
172
|
-
return BATCH_BY_TIER[TIER_ORDER[idx - 1]] || 32;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return BATCH_BY_TIER[tier] || 32;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
114
|
class WSOptimizer {
|
|
180
115
|
constructor() {
|
|
181
116
|
this.clientQueues = new Map();
|
|
@@ -183,16 +118,13 @@ class WSOptimizer {
|
|
|
183
118
|
|
|
184
119
|
sendToClient(ws, event) {
|
|
185
120
|
if (ws.readyState !== 1) return;
|
|
186
|
-
|
|
187
121
|
let queue = this.clientQueues.get(ws);
|
|
188
122
|
if (!queue) {
|
|
189
123
|
queue = new ClientQueue(ws);
|
|
190
124
|
this.clientQueues.set(ws, queue);
|
|
191
125
|
}
|
|
192
|
-
|
|
193
126
|
const data = typeof event === 'string' ? event : JSON.stringify(event);
|
|
194
127
|
const priority = typeof event === 'object' ? getPriority(event.type) : 2;
|
|
195
|
-
|
|
196
128
|
queue.add(data, priority);
|
|
197
129
|
}
|
|
198
130
|
|
|
@@ -205,30 +137,18 @@ class WSOptimizer {
|
|
|
205
137
|
}
|
|
206
138
|
|
|
207
139
|
getStats() {
|
|
208
|
-
const stats = {
|
|
209
|
-
clients: this.clientQueues.size,
|
|
210
|
-
totalBytes: 0,
|
|
211
|
-
totalMessages: 0,
|
|
212
|
-
highBandwidthClients: []
|
|
213
|
-
};
|
|
214
|
-
|
|
140
|
+
const stats = { clients: this.clientQueues.size, totalBytes: 0, totalMessages: 0, highBandwidthClients: [] };
|
|
215
141
|
for (const [ws, queue] of this.clientQueues.entries()) {
|
|
216
142
|
stats.totalBytes += queue.bytesSent;
|
|
217
143
|
stats.totalMessages += queue.messageCount;
|
|
218
|
-
|
|
219
144
|
const windowDuration = Date.now() - queue.windowStart;
|
|
220
145
|
if (windowDuration > 0) {
|
|
221
146
|
const mbps = (queue.bytesSent / windowDuration * 1000 / 1024 / 1024);
|
|
222
147
|
if (mbps > 1) {
|
|
223
|
-
stats.highBandwidthClients.push({
|
|
224
|
-
clientId: ws.clientId,
|
|
225
|
-
mbps: mbps.toFixed(2),
|
|
226
|
-
messages: queue.messageCount
|
|
227
|
-
});
|
|
148
|
+
stats.highBandwidthClients.push({ clientId: ws.clientId, mbps: mbps.toFixed(2), messages: queue.messageCount });
|
|
228
149
|
}
|
|
229
150
|
}
|
|
230
151
|
}
|
|
231
|
-
|
|
232
152
|
return stats;
|
|
233
153
|
}
|
|
234
154
|
}
|