agentic-flow 1.10.0 → 1.10.1
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/dist/utils/adaptive-pool-sizing.js +414 -0
- package/dist/utils/circular-rate-limiter.js +391 -0
- package/dist/utils/dynamic-compression.js +298 -0
- package/dist/utils/http2-multiplexing.js +319 -0
- package/dist/utils/lazy-auth.js +311 -0
- package/dist/utils/server-push.js +251 -0
- package/dist/utils/zero-copy-buffer.js +286 -0
- package/docs/DOCKER-VERIFICATION.md +207 -0
- package/docs/ISSUE-55-VALIDATION.md +25 -6
- package/docs/NPX_AGENTDB_SETUP.md +175 -0
- package/docs/PHASE2-IMPLEMENTATION-SUMMARY.md +275 -0
- package/docs/PHASE2-PHASE3-COMPLETE-SUMMARY.md +453 -0
- package/docs/PHASE3-IMPLEMENTATION-SUMMARY.md +357 -0
- package/docs/PUBLISH_GUIDE.md +438 -0
- package/docs/RELEASE-v1.10.0-COMPLETE.md +382 -0
- package/docs/archive/.agentdb-instructions.md +66 -0
- package/docs/archive/AGENT-BOOSTER-STATUS.md +292 -0
- package/docs/archive/CHANGELOG-v1.3.0.md +120 -0
- package/docs/archive/COMPLETION_REPORT_v1.7.1.md +335 -0
- package/docs/archive/IMPLEMENTATION_SUMMARY_v1.7.1.md +241 -0
- package/docs/archive/SUPABASE-INTEGRATION-COMPLETE.md +357 -0
- package/docs/archive/TESTING_QUICK_START.md +223 -0
- package/docs/archive/TOOL-EMULATION-INTEGRATION-ISSUE.md +669 -0
- package/docs/archive/VALIDATION_v1.7.1.md +234 -0
- package/docs/releases/PUBLISH_CHECKLIST_v1.10.0.md +396 -0
- package/docs/releases/PUBLISH_SUMMARY_v1.7.1.md +198 -0
- package/docs/releases/RELEASE_NOTES_v1.10.0.md +464 -0
- package/docs/releases/RELEASE_NOTES_v1.7.0.md +297 -0
- package/docs/releases/RELEASE_v1.7.1.md +327 -0
- package/package.json +1 -1
- package/validation/docker-npm-validation.sh +170 -0
- package/validation/simple-npm-validation.sh +131 -0
- package/validation/test-gemini-models.ts +200 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optimized Rate Limiter with Circular Buffers
|
|
3
|
+
* 2-5% CPU reduction through efficient circular buffer implementation
|
|
4
|
+
* Phase 3 Optimization
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Circular Buffer for efficient timestamp storage
|
|
8
|
+
* Avoids array shifts and allocations
|
|
9
|
+
*/
|
|
10
|
+
class CircularBuffer {
|
|
11
|
+
buffer;
|
|
12
|
+
head = 0;
|
|
13
|
+
tail = 0;
|
|
14
|
+
count = 0;
|
|
15
|
+
size;
|
|
16
|
+
constructor(size) {
|
|
17
|
+
this.size = size;
|
|
18
|
+
this.buffer = new Array(size);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Add timestamp to buffer
|
|
22
|
+
*/
|
|
23
|
+
add(timestamp) {
|
|
24
|
+
this.buffer[this.tail] = timestamp;
|
|
25
|
+
this.tail = (this.tail + 1) % this.size;
|
|
26
|
+
if (this.count < this.size) {
|
|
27
|
+
this.count++;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
// Buffer is full, move head forward
|
|
31
|
+
this.head = (this.head + 1) % this.size;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Remove timestamps older than cutoff
|
|
36
|
+
*/
|
|
37
|
+
removeOlderThan(cutoff) {
|
|
38
|
+
let removed = 0;
|
|
39
|
+
while (this.count > 0) {
|
|
40
|
+
const timestamp = this.buffer[this.head];
|
|
41
|
+
if (timestamp >= cutoff) {
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
this.head = (this.head + 1) % this.size;
|
|
45
|
+
this.count--;
|
|
46
|
+
removed++;
|
|
47
|
+
}
|
|
48
|
+
return removed;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Get current count
|
|
52
|
+
*/
|
|
53
|
+
getCount() {
|
|
54
|
+
return this.count;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Get oldest timestamp
|
|
58
|
+
*/
|
|
59
|
+
getOldest() {
|
|
60
|
+
return this.count > 0 ? this.buffer[this.head] : null;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Clear buffer
|
|
64
|
+
*/
|
|
65
|
+
clear() {
|
|
66
|
+
this.head = 0;
|
|
67
|
+
this.tail = 0;
|
|
68
|
+
this.count = 0;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Check if buffer is full
|
|
72
|
+
*/
|
|
73
|
+
isFull() {
|
|
74
|
+
return this.count === this.size;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Get buffer utilization
|
|
78
|
+
*/
|
|
79
|
+
getUtilization() {
|
|
80
|
+
return (this.count / this.size) * 100;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Circular Rate Limiter
|
|
85
|
+
* Uses circular buffers for efficient rate limiting with minimal CPU overhead
|
|
86
|
+
*/
|
|
87
|
+
export class CircularRateLimiter {
|
|
88
|
+
config;
|
|
89
|
+
clients = new Map();
|
|
90
|
+
stats;
|
|
91
|
+
constructor(config) {
|
|
92
|
+
this.config = {
|
|
93
|
+
enabled: config.enabled,
|
|
94
|
+
windowMs: config.windowMs || 60000, // 1 minute default
|
|
95
|
+
maxRequests: config.maxRequests || 100,
|
|
96
|
+
bufferSize: config.bufferSize || 200 // 2x maxRequests for safety
|
|
97
|
+
};
|
|
98
|
+
this.stats = {
|
|
99
|
+
totalRequests: 0,
|
|
100
|
+
allowedRequests: 0,
|
|
101
|
+
blockedRequests: 0,
|
|
102
|
+
uniqueClients: 0,
|
|
103
|
+
avgCheckTime: 0
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Check if request is allowed
|
|
108
|
+
*/
|
|
109
|
+
checkLimit(clientId) {
|
|
110
|
+
if (!this.config.enabled) {
|
|
111
|
+
return {
|
|
112
|
+
allowed: true,
|
|
113
|
+
remaining: this.config.maxRequests,
|
|
114
|
+
resetAt: Date.now() + this.config.windowMs
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const startTime = performance.now();
|
|
118
|
+
const now = Date.now();
|
|
119
|
+
const windowStart = now - this.config.windowMs;
|
|
120
|
+
// Get or create buffer for client
|
|
121
|
+
let buffer = this.clients.get(clientId);
|
|
122
|
+
if (!buffer) {
|
|
123
|
+
buffer = new CircularBuffer(this.config.bufferSize);
|
|
124
|
+
this.clients.set(clientId, buffer);
|
|
125
|
+
this.stats.uniqueClients = this.clients.size;
|
|
126
|
+
}
|
|
127
|
+
// Remove expired timestamps
|
|
128
|
+
buffer.removeOlderThan(windowStart);
|
|
129
|
+
const currentCount = buffer.getCount();
|
|
130
|
+
const allowed = currentCount < this.config.maxRequests;
|
|
131
|
+
if (allowed) {
|
|
132
|
+
buffer.add(now);
|
|
133
|
+
this.stats.allowedRequests++;
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
this.stats.blockedRequests++;
|
|
137
|
+
}
|
|
138
|
+
this.stats.totalRequests++;
|
|
139
|
+
// Update average check time
|
|
140
|
+
const checkTime = performance.now() - startTime;
|
|
141
|
+
this.updateAvgCheckTime(checkTime);
|
|
142
|
+
const oldest = buffer.getOldest();
|
|
143
|
+
const resetAt = oldest ? oldest + this.config.windowMs : now + this.config.windowMs;
|
|
144
|
+
const retryAfter = allowed ? undefined : resetAt - now;
|
|
145
|
+
return {
|
|
146
|
+
allowed,
|
|
147
|
+
remaining: Math.max(0, this.config.maxRequests - currentCount - (allowed ? 1 : 0)),
|
|
148
|
+
resetAt,
|
|
149
|
+
retryAfter
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Update average check time
|
|
154
|
+
*/
|
|
155
|
+
updateAvgCheckTime(newTime) {
|
|
156
|
+
const total = this.stats.totalRequests;
|
|
157
|
+
const currentAvg = this.stats.avgCheckTime;
|
|
158
|
+
this.stats.avgCheckTime = (currentAvg * (total - 1) + newTime) / total;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Reset limit for a client
|
|
162
|
+
*/
|
|
163
|
+
reset(clientId) {
|
|
164
|
+
const buffer = this.clients.get(clientId);
|
|
165
|
+
if (buffer) {
|
|
166
|
+
buffer.clear();
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Remove a client
|
|
173
|
+
*/
|
|
174
|
+
removeClient(clientId) {
|
|
175
|
+
const removed = this.clients.delete(clientId);
|
|
176
|
+
if (removed) {
|
|
177
|
+
this.stats.uniqueClients = this.clients.size;
|
|
178
|
+
}
|
|
179
|
+
return removed;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Clean up expired clients
|
|
183
|
+
*/
|
|
184
|
+
cleanup() {
|
|
185
|
+
const now = Date.now();
|
|
186
|
+
const windowStart = now - this.config.windowMs;
|
|
187
|
+
let cleaned = 0;
|
|
188
|
+
for (const [clientId, buffer] of this.clients) {
|
|
189
|
+
buffer.removeOlderThan(windowStart);
|
|
190
|
+
// Remove clients with no recent activity
|
|
191
|
+
if (buffer.getCount() === 0) {
|
|
192
|
+
this.clients.delete(clientId);
|
|
193
|
+
cleaned++;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
this.stats.uniqueClients = this.clients.size;
|
|
197
|
+
return cleaned;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Get statistics
|
|
201
|
+
*/
|
|
202
|
+
getStats() {
|
|
203
|
+
return { ...this.stats };
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Get block rate
|
|
207
|
+
*/
|
|
208
|
+
getBlockRate() {
|
|
209
|
+
return this.stats.totalRequests > 0
|
|
210
|
+
? (this.stats.blockedRequests / this.stats.totalRequests) * 100
|
|
211
|
+
: 0;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Get client info
|
|
215
|
+
*/
|
|
216
|
+
getClientInfo(clientId) {
|
|
217
|
+
const buffer = this.clients.get(clientId);
|
|
218
|
+
if (!buffer) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
count: buffer.getCount(),
|
|
223
|
+
utilization: buffer.getUtilization(),
|
|
224
|
+
oldest: buffer.getOldest()
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Reset statistics
|
|
229
|
+
*/
|
|
230
|
+
resetStats() {
|
|
231
|
+
this.stats = {
|
|
232
|
+
totalRequests: 0,
|
|
233
|
+
allowedRequests: 0,
|
|
234
|
+
blockedRequests: 0,
|
|
235
|
+
uniqueClients: this.clients.size,
|
|
236
|
+
avgCheckTime: 0
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Clear all clients
|
|
241
|
+
*/
|
|
242
|
+
clear() {
|
|
243
|
+
this.clients.clear();
|
|
244
|
+
this.stats.uniqueClients = 0;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Get configuration (for subclasses)
|
|
248
|
+
*/
|
|
249
|
+
getConfig() {
|
|
250
|
+
return this.config;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Sliding Window Rate Limiter
|
|
255
|
+
* More accurate than fixed window, uses circular buffers
|
|
256
|
+
*/
|
|
257
|
+
export class SlidingWindowRateLimiter extends CircularRateLimiter {
|
|
258
|
+
/**
|
|
259
|
+
* Check with sliding window algorithm
|
|
260
|
+
*/
|
|
261
|
+
checkLimitSliding(clientId) {
|
|
262
|
+
const result = this.checkLimit(clientId);
|
|
263
|
+
// Sliding window provides more accurate rate limiting
|
|
264
|
+
// by continuously cleaning up old timestamps
|
|
265
|
+
const clientInfo = this.getClientInfo(clientId);
|
|
266
|
+
if (clientInfo && clientInfo.oldest) {
|
|
267
|
+
const now = Date.now();
|
|
268
|
+
const config = this.getConfig();
|
|
269
|
+
const windowStart = now - config.windowMs;
|
|
270
|
+
// More accurate remaining calculation
|
|
271
|
+
const timeInWindow = now - Math.max(clientInfo.oldest, windowStart);
|
|
272
|
+
const weightedCount = (clientInfo.count * timeInWindow) / config.windowMs;
|
|
273
|
+
result.remaining = Math.max(0, Math.floor(config.maxRequests - weightedCount));
|
|
274
|
+
}
|
|
275
|
+
return result;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Token Bucket Rate Limiter
|
|
280
|
+
* Allows burst traffic while maintaining average rate
|
|
281
|
+
*/
|
|
282
|
+
export class TokenBucketRateLimiter {
|
|
283
|
+
config;
|
|
284
|
+
buckets = new Map();
|
|
285
|
+
stats;
|
|
286
|
+
constructor(config) {
|
|
287
|
+
this.config = {
|
|
288
|
+
capacity: config.capacity,
|
|
289
|
+
refillRate: config.refillRate,
|
|
290
|
+
refillInterval: config.refillInterval || 1000
|
|
291
|
+
};
|
|
292
|
+
this.stats = {
|
|
293
|
+
totalRequests: 0,
|
|
294
|
+
allowedRequests: 0,
|
|
295
|
+
blockedRequests: 0,
|
|
296
|
+
uniqueClients: 0,
|
|
297
|
+
avgCheckTime: 0
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Check if tokens available
|
|
302
|
+
*/
|
|
303
|
+
checkLimit(clientId, tokens = 1) {
|
|
304
|
+
let bucket = this.buckets.get(clientId);
|
|
305
|
+
if (!bucket) {
|
|
306
|
+
bucket = new TokenBucket(this.config.capacity, this.config.refillRate, this.config.refillInterval);
|
|
307
|
+
this.buckets.set(clientId, bucket);
|
|
308
|
+
this.stats.uniqueClients = this.buckets.size;
|
|
309
|
+
}
|
|
310
|
+
const allowed = bucket.consume(tokens);
|
|
311
|
+
this.stats.totalRequests++;
|
|
312
|
+
if (allowed) {
|
|
313
|
+
this.stats.allowedRequests++;
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
this.stats.blockedRequests++;
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
allowed,
|
|
320
|
+
remaining: Math.floor(bucket.getTokens()),
|
|
321
|
+
resetAt: Date.now() + this.config.refillInterval
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Get statistics
|
|
326
|
+
*/
|
|
327
|
+
getStats() {
|
|
328
|
+
return { ...this.stats };
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Token Bucket implementation
|
|
333
|
+
*/
|
|
334
|
+
class TokenBucket {
|
|
335
|
+
tokens;
|
|
336
|
+
capacity;
|
|
337
|
+
refillRate;
|
|
338
|
+
lastRefill;
|
|
339
|
+
refillInterval;
|
|
340
|
+
constructor(capacity, refillRate, refillInterval) {
|
|
341
|
+
this.capacity = capacity;
|
|
342
|
+
this.tokens = capacity;
|
|
343
|
+
this.refillRate = refillRate;
|
|
344
|
+
this.refillInterval = refillInterval;
|
|
345
|
+
this.lastRefill = Date.now();
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Consume tokens
|
|
349
|
+
*/
|
|
350
|
+
consume(tokens) {
|
|
351
|
+
this.refill();
|
|
352
|
+
if (this.tokens >= tokens) {
|
|
353
|
+
this.tokens -= tokens;
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Refill tokens
|
|
360
|
+
*/
|
|
361
|
+
refill() {
|
|
362
|
+
const now = Date.now();
|
|
363
|
+
const elapsed = now - this.lastRefill;
|
|
364
|
+
const intervals = Math.floor(elapsed / this.refillInterval);
|
|
365
|
+
if (intervals > 0) {
|
|
366
|
+
this.tokens = Math.min(this.capacity, this.tokens + intervals * this.refillRate);
|
|
367
|
+
this.lastRefill = now;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Get current tokens
|
|
372
|
+
*/
|
|
373
|
+
getTokens() {
|
|
374
|
+
this.refill();
|
|
375
|
+
return this.tokens;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Calculate CPU savings from circular buffer optimization
|
|
380
|
+
*/
|
|
381
|
+
export function calculateRateLimiterSavings(oldAvgTime, newAvgTime, totalRequests) {
|
|
382
|
+
const savings = oldAvgTime - newAvgTime;
|
|
383
|
+
const savingsPercentage = (savings / oldAvgTime) * 100;
|
|
384
|
+
const totalTimeSaved = savings * totalRequests;
|
|
385
|
+
const cpuReduction = savingsPercentage;
|
|
386
|
+
return {
|
|
387
|
+
savingsPercentage,
|
|
388
|
+
totalTimeSaved,
|
|
389
|
+
cpuReduction
|
|
390
|
+
};
|
|
391
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dynamic Compression based on CPU
|
|
3
|
+
* Adaptive compression levels based on CPU availability
|
|
4
|
+
* Phase 3 Optimization
|
|
5
|
+
*/
|
|
6
|
+
import * as zlib from 'zlib';
|
|
7
|
+
import { performance } from 'perf_hooks';
|
|
8
|
+
/**
|
|
9
|
+
* Compression levels with CPU costs
|
|
10
|
+
*/
|
|
11
|
+
export const COMPRESSION_LEVELS = {
|
|
12
|
+
gzip: [
|
|
13
|
+
{ level: 1, name: 'fastest', cpuCost: 1, compressionRatio: 3 },
|
|
14
|
+
{ level: 3, name: 'fast', cpuCost: 3, compressionRatio: 5 },
|
|
15
|
+
{ level: 6, name: 'default', cpuCost: 6, compressionRatio: 7 },
|
|
16
|
+
{ level: 9, name: 'best', cpuCost: 10, compressionRatio: 9 }
|
|
17
|
+
],
|
|
18
|
+
brotli: [
|
|
19
|
+
{ level: 1, name: 'fastest', cpuCost: 2, compressionRatio: 4 },
|
|
20
|
+
{ level: 4, name: 'fast', cpuCost: 4, compressionRatio: 6 },
|
|
21
|
+
{ level: 6, name: 'default', cpuCost: 7, compressionRatio: 8 },
|
|
22
|
+
{ level: 11, name: 'best', cpuCost: 10, compressionRatio: 10 }
|
|
23
|
+
]
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Dynamic Compression Manager
|
|
27
|
+
* Adjusts compression levels based on CPU load
|
|
28
|
+
*/
|
|
29
|
+
export class DynamicCompressionManager {
|
|
30
|
+
config;
|
|
31
|
+
stats;
|
|
32
|
+
currentLevelIndex = 1; // Start with 'fast'
|
|
33
|
+
cpuSamples = [];
|
|
34
|
+
monitorInterval;
|
|
35
|
+
constructor(config) {
|
|
36
|
+
this.config = {
|
|
37
|
+
enabled: config.enabled,
|
|
38
|
+
minSize: config.minSize || 1024,
|
|
39
|
+
algorithm: config.algorithm || 'gzip',
|
|
40
|
+
adaptive: config.adaptive !== false,
|
|
41
|
+
cpuThresholdHigh: config.cpuThresholdHigh || 70,
|
|
42
|
+
cpuThresholdLow: config.cpuThresholdLow || 30,
|
|
43
|
+
checkInterval: config.checkInterval || 5000
|
|
44
|
+
};
|
|
45
|
+
this.stats = {
|
|
46
|
+
totalBytes: 0,
|
|
47
|
+
compressedBytes: 0,
|
|
48
|
+
compressionRatio: 1,
|
|
49
|
+
avgCompressionTime: 0,
|
|
50
|
+
currentLevel: this.getCurrentLevel().level,
|
|
51
|
+
levelChanges: 0,
|
|
52
|
+
cpuAdjustments: 0
|
|
53
|
+
};
|
|
54
|
+
if (this.config.adaptive) {
|
|
55
|
+
this.startCPUMonitoring();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Compress data with adaptive level
|
|
60
|
+
*/
|
|
61
|
+
async compress(data) {
|
|
62
|
+
if (!this.config.enabled || data.length < this.config.minSize) {
|
|
63
|
+
return data;
|
|
64
|
+
}
|
|
65
|
+
const startTime = performance.now();
|
|
66
|
+
const level = this.getCurrentLevel();
|
|
67
|
+
let compressed;
|
|
68
|
+
try {
|
|
69
|
+
if (this.config.algorithm === 'brotli') {
|
|
70
|
+
compressed = await this.compressBrotli(data, level.level);
|
|
71
|
+
}
|
|
72
|
+
else if (this.config.algorithm === 'deflate') {
|
|
73
|
+
compressed = await this.compressDeflate(data, level.level);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
compressed = await this.compressGzip(data, level.level);
|
|
77
|
+
}
|
|
78
|
+
// Update statistics
|
|
79
|
+
const compressionTime = performance.now() - startTime;
|
|
80
|
+
this.updateStats(data.length, compressed.length, compressionTime);
|
|
81
|
+
return compressed;
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
// Fallback to uncompressed
|
|
85
|
+
return data;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Compress with gzip
|
|
90
|
+
*/
|
|
91
|
+
compressGzip(data, level) {
|
|
92
|
+
return new Promise((resolve, reject) => {
|
|
93
|
+
zlib.gzip(data, { level }, (err, result) => {
|
|
94
|
+
if (err)
|
|
95
|
+
reject(err);
|
|
96
|
+
else
|
|
97
|
+
resolve(result);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Compress with brotli
|
|
103
|
+
*/
|
|
104
|
+
compressBrotli(data, level) {
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
zlib.brotliCompress(data, {
|
|
107
|
+
params: {
|
|
108
|
+
[zlib.constants.BROTLI_PARAM_QUALITY]: level
|
|
109
|
+
}
|
|
110
|
+
}, (err, result) => {
|
|
111
|
+
if (err)
|
|
112
|
+
reject(err);
|
|
113
|
+
else
|
|
114
|
+
resolve(result);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Compress with deflate
|
|
120
|
+
*/
|
|
121
|
+
compressDeflate(data, level) {
|
|
122
|
+
return new Promise((resolve, reject) => {
|
|
123
|
+
zlib.deflate(data, { level }, (err, result) => {
|
|
124
|
+
if (err)
|
|
125
|
+
reject(err);
|
|
126
|
+
else
|
|
127
|
+
resolve(result);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Get current compression level
|
|
133
|
+
*/
|
|
134
|
+
getCurrentLevel() {
|
|
135
|
+
const levels = COMPRESSION_LEVELS[this.config.algorithm];
|
|
136
|
+
return levels[this.currentLevelIndex] || levels[1];
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Adjust compression level based on CPU
|
|
140
|
+
*/
|
|
141
|
+
adjustLevel(cpuUsage) {
|
|
142
|
+
const levels = COMPRESSION_LEVELS[this.config.algorithm];
|
|
143
|
+
const oldIndex = this.currentLevelIndex;
|
|
144
|
+
if (cpuUsage > this.config.cpuThresholdHigh && this.currentLevelIndex > 0) {
|
|
145
|
+
// CPU high, reduce compression level
|
|
146
|
+
this.currentLevelIndex--;
|
|
147
|
+
this.stats.cpuAdjustments++;
|
|
148
|
+
}
|
|
149
|
+
else if (cpuUsage < this.config.cpuThresholdLow && this.currentLevelIndex < levels.length - 1) {
|
|
150
|
+
// CPU low, increase compression level
|
|
151
|
+
this.currentLevelIndex++;
|
|
152
|
+
this.stats.cpuAdjustments++;
|
|
153
|
+
}
|
|
154
|
+
if (oldIndex !== this.currentLevelIndex) {
|
|
155
|
+
this.stats.levelChanges++;
|
|
156
|
+
this.stats.currentLevel = this.getCurrentLevel().level;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Start CPU monitoring
|
|
161
|
+
*/
|
|
162
|
+
startCPUMonitoring() {
|
|
163
|
+
this.monitorInterval = setInterval(() => {
|
|
164
|
+
const cpuUsage = this.getCPUUsage();
|
|
165
|
+
this.cpuSamples.push(cpuUsage);
|
|
166
|
+
// Keep last 10 samples
|
|
167
|
+
if (this.cpuSamples.length > 10) {
|
|
168
|
+
this.cpuSamples.shift();
|
|
169
|
+
}
|
|
170
|
+
// Calculate average CPU
|
|
171
|
+
const avgCPU = this.cpuSamples.reduce((a, b) => a + b, 0) / this.cpuSamples.length;
|
|
172
|
+
// Adjust compression level
|
|
173
|
+
this.adjustLevel(avgCPU);
|
|
174
|
+
}, this.config.checkInterval);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Get CPU usage percentage
|
|
178
|
+
*/
|
|
179
|
+
getCPUUsage() {
|
|
180
|
+
const cpus = require('os').cpus();
|
|
181
|
+
let totalIdle = 0;
|
|
182
|
+
let totalTick = 0;
|
|
183
|
+
for (const cpu of cpus) {
|
|
184
|
+
for (const type in cpu.times) {
|
|
185
|
+
totalTick += cpu.times[type];
|
|
186
|
+
}
|
|
187
|
+
totalIdle += cpu.times.idle;
|
|
188
|
+
}
|
|
189
|
+
const idle = totalIdle / cpus.length;
|
|
190
|
+
const total = totalTick / cpus.length;
|
|
191
|
+
const usage = 100 - ~~(100 * idle / total);
|
|
192
|
+
return Math.max(0, Math.min(100, usage));
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Update statistics
|
|
196
|
+
*/
|
|
197
|
+
updateStats(originalSize, compressedSize, time) {
|
|
198
|
+
this.stats.totalBytes += originalSize;
|
|
199
|
+
this.stats.compressedBytes += compressedSize;
|
|
200
|
+
this.stats.compressionRatio = this.stats.totalBytes / this.stats.compressedBytes;
|
|
201
|
+
// Update average compression time
|
|
202
|
+
const totalCompressions = this.stats.totalBytes / (originalSize || 1);
|
|
203
|
+
this.stats.avgCompressionTime =
|
|
204
|
+
(this.stats.avgCompressionTime * (totalCompressions - 1) + time) / totalCompressions;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Get statistics
|
|
208
|
+
*/
|
|
209
|
+
getStats() {
|
|
210
|
+
const level = this.getCurrentLevel();
|
|
211
|
+
const avgCPU = this.cpuSamples.length > 0
|
|
212
|
+
? this.cpuSamples.reduce((a, b) => a + b, 0) / this.cpuSamples.length
|
|
213
|
+
: 0;
|
|
214
|
+
return {
|
|
215
|
+
...this.stats,
|
|
216
|
+
currentLevelName: level.name,
|
|
217
|
+
cpuUsage: avgCPU
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Get compression savings
|
|
222
|
+
*/
|
|
223
|
+
getSavings() {
|
|
224
|
+
const byteSavings = this.stats.totalBytes - this.stats.compressedBytes;
|
|
225
|
+
const percentSavings = (byteSavings / this.stats.totalBytes) * 100;
|
|
226
|
+
const mbSaved = byteSavings / (1024 * 1024);
|
|
227
|
+
return {
|
|
228
|
+
byteSavings,
|
|
229
|
+
percentSavings,
|
|
230
|
+
mbSaved
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Manually set compression level
|
|
235
|
+
*/
|
|
236
|
+
setLevel(levelName) {
|
|
237
|
+
const levels = COMPRESSION_LEVELS[this.config.algorithm];
|
|
238
|
+
const index = levels.findIndex(l => l.name === levelName);
|
|
239
|
+
if (index !== -1) {
|
|
240
|
+
this.currentLevelIndex = index;
|
|
241
|
+
this.stats.currentLevel = levels[index].level;
|
|
242
|
+
this.stats.levelChanges++;
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Reset statistics
|
|
249
|
+
*/
|
|
250
|
+
resetStats() {
|
|
251
|
+
this.stats = {
|
|
252
|
+
totalBytes: 0,
|
|
253
|
+
compressedBytes: 0,
|
|
254
|
+
compressionRatio: 1,
|
|
255
|
+
avgCompressionTime: 0,
|
|
256
|
+
currentLevel: this.getCurrentLevel().level,
|
|
257
|
+
levelChanges: 0,
|
|
258
|
+
cpuAdjustments: 0
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Stop CPU monitoring
|
|
263
|
+
*/
|
|
264
|
+
destroy() {
|
|
265
|
+
if (this.monitorInterval) {
|
|
266
|
+
clearInterval(this.monitorInterval);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Content-type aware compression
|
|
272
|
+
*/
|
|
273
|
+
export function shouldCompress(contentType, size, minSize = 1024) {
|
|
274
|
+
if (size < minSize) {
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
const compressibleTypes = [
|
|
278
|
+
'text/',
|
|
279
|
+
'application/json',
|
|
280
|
+
'application/javascript',
|
|
281
|
+
'application/xml',
|
|
282
|
+
'application/x-www-form-urlencoded'
|
|
283
|
+
];
|
|
284
|
+
return compressibleTypes.some(type => contentType.startsWith(type));
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Calculate compression efficiency
|
|
288
|
+
*/
|
|
289
|
+
export function calculateCompressionEfficiency(stats) {
|
|
290
|
+
const timePerMB = stats.avgCompressionTime / ((stats.totalBytes / (1024 * 1024)) || 1);
|
|
291
|
+
const ratioScore = Math.min(100, (stats.compressionRatio - 1) * 10);
|
|
292
|
+
const efficiency = (ratioScore * 0.7) + ((100 - Math.min(100, timePerMB)) * 0.3);
|
|
293
|
+
return {
|
|
294
|
+
efficiency,
|
|
295
|
+
timePerMB,
|
|
296
|
+
ratioScore
|
|
297
|
+
};
|
|
298
|
+
}
|