crawlforge-mcp-server 3.0.0

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 (75) hide show
  1. package/CLAUDE.md +315 -0
  2. package/LICENSE +21 -0
  3. package/README.md +181 -0
  4. package/package.json +115 -0
  5. package/server.js +1963 -0
  6. package/setup.js +112 -0
  7. package/src/constants/config.js +615 -0
  8. package/src/core/ActionExecutor.js +1104 -0
  9. package/src/core/AlertNotificationSystem.js +601 -0
  10. package/src/core/AuthManager.js +315 -0
  11. package/src/core/ChangeTracker.js +2306 -0
  12. package/src/core/JobManager.js +687 -0
  13. package/src/core/LLMsTxtAnalyzer.js +753 -0
  14. package/src/core/LocalizationManager.js +1615 -0
  15. package/src/core/PerformanceManager.js +828 -0
  16. package/src/core/ResearchOrchestrator.js +1327 -0
  17. package/src/core/SnapshotManager.js +1037 -0
  18. package/src/core/StealthBrowserManager.js +1795 -0
  19. package/src/core/WebhookDispatcher.js +745 -0
  20. package/src/core/analysis/ContentAnalyzer.js +749 -0
  21. package/src/core/analysis/LinkAnalyzer.js +972 -0
  22. package/src/core/cache/CacheManager.js +821 -0
  23. package/src/core/connections/ConnectionPool.js +553 -0
  24. package/src/core/crawlers/BFSCrawler.js +845 -0
  25. package/src/core/integrations/PerformanceIntegration.js +377 -0
  26. package/src/core/llm/AnthropicProvider.js +135 -0
  27. package/src/core/llm/LLMManager.js +415 -0
  28. package/src/core/llm/LLMProvider.js +97 -0
  29. package/src/core/llm/OpenAIProvider.js +127 -0
  30. package/src/core/processing/BrowserProcessor.js +986 -0
  31. package/src/core/processing/ContentProcessor.js +505 -0
  32. package/src/core/processing/PDFProcessor.js +448 -0
  33. package/src/core/processing/StreamProcessor.js +673 -0
  34. package/src/core/queue/QueueManager.js +98 -0
  35. package/src/core/workers/WorkerPool.js +585 -0
  36. package/src/core/workers/worker.js +743 -0
  37. package/src/monitoring/healthCheck.js +600 -0
  38. package/src/monitoring/metrics.js +761 -0
  39. package/src/optimization/wave3-optimizations.js +932 -0
  40. package/src/security/security-patches.js +120 -0
  41. package/src/security/security-tests.js +355 -0
  42. package/src/security/wave3-security.js +652 -0
  43. package/src/tools/advanced/BatchScrapeTool.js +1089 -0
  44. package/src/tools/advanced/ScrapeWithActionsTool.js +669 -0
  45. package/src/tools/crawl/crawlDeep.js +449 -0
  46. package/src/tools/crawl/mapSite.js +400 -0
  47. package/src/tools/extract/analyzeContent.js +624 -0
  48. package/src/tools/extract/extractContent.js +329 -0
  49. package/src/tools/extract/processDocument.js +503 -0
  50. package/src/tools/extract/summarizeContent.js +376 -0
  51. package/src/tools/llmstxt/generateLLMsTxt.js +570 -0
  52. package/src/tools/research/deepResearch.js +706 -0
  53. package/src/tools/search/adapters/duckduckgoSearch.js +398 -0
  54. package/src/tools/search/adapters/googleSearch.js +236 -0
  55. package/src/tools/search/adapters/searchProviderFactory.js +96 -0
  56. package/src/tools/search/queryExpander.js +543 -0
  57. package/src/tools/search/ranking/ResultDeduplicator.js +676 -0
  58. package/src/tools/search/ranking/ResultRanker.js +497 -0
  59. package/src/tools/search/searchWeb.js +482 -0
  60. package/src/tools/tracking/trackChanges.js +1355 -0
  61. package/src/utils/CircuitBreaker.js +515 -0
  62. package/src/utils/ErrorHandlingConfig.js +342 -0
  63. package/src/utils/HumanBehaviorSimulator.js +569 -0
  64. package/src/utils/Logger.js +568 -0
  65. package/src/utils/MemoryMonitor.js +173 -0
  66. package/src/utils/RetryManager.js +386 -0
  67. package/src/utils/contentUtils.js +588 -0
  68. package/src/utils/domainFilter.js +612 -0
  69. package/src/utils/inputValidation.js +766 -0
  70. package/src/utils/rateLimiter.js +196 -0
  71. package/src/utils/robotsChecker.js +91 -0
  72. package/src/utils/securityMiddleware.js +416 -0
  73. package/src/utils/sitemapParser.js +678 -0
  74. package/src/utils/ssrfProtection.js +640 -0
  75. package/src/utils/urlNormalizer.js +168 -0
@@ -0,0 +1,821 @@
1
+ import { LRUCache } from 'lru-cache';
2
+ import { promises as fs } from 'fs';
3
+ import path from 'path';
4
+ import crypto from 'crypto';
5
+ import { EventEmitter } from 'events';
6
+
7
+ export class CacheManager extends EventEmitter {
8
+ constructor(options = {}) {
9
+ super();
10
+
11
+ const {
12
+ maxSize = 1000,
13
+ ttl = 3600000, // 1 hour default
14
+ diskCacheDir = './cache',
15
+ enableDiskCache = true,
16
+ enableCacheWarming = false,
17
+ warmingBatchSize = 10,
18
+ enableMonitoring = true,
19
+ monitoringInterval = 60000, // 1 minute
20
+ autoCleanupInterval = 300000, // 5 minutes
21
+ dependencyTracking = false
22
+ } = options;
23
+
24
+ this.memoryCache = new LRUCache({
25
+ max: maxSize,
26
+ ttl,
27
+ updateAgeOnGet: true,
28
+ updateAgeOnHas: false,
29
+ dispose: (value, key, reason) => {
30
+ if (reason === 'evict') {
31
+ this.stats.evictions++;
32
+ this.emit('evict', key, value);
33
+ }
34
+ }
35
+ });
36
+
37
+ this.diskCacheDir = diskCacheDir;
38
+ this.enableDiskCache = enableDiskCache;
39
+ this.enableCacheWarming = enableCacheWarming;
40
+ this.warmingBatchSize = warmingBatchSize;
41
+ this.enableMonitoring = enableMonitoring;
42
+ this.dependencyTracking = dependencyTracking;
43
+
44
+ // Enhanced features
45
+ this.dependencies = new Map(); // key -> Set of dependent keys
46
+ this.reverseDependencies = new Map(); // key -> Set of keys it depends on
47
+ this.warmingQueue = [];
48
+ this.warmingJobs = new Map();
49
+ this.eventInvalidators = new Map(); // event -> Set of cache keys
50
+ this.tags = new Map(); // key -> Set of tags
51
+ this.taggedKeys = new Map(); // tag -> Set of keys
52
+
53
+ this.stats = {
54
+ hits: 0,
55
+ misses: 0,
56
+ memoryHits: 0,
57
+ diskHits: 0,
58
+ evictions: 0,
59
+ invalidations: 0,
60
+ warmingHits: 0,
61
+ totalRequests: 0,
62
+ averageResponseTime: 0,
63
+ memoryUsage: 0,
64
+ diskUsage: 0,
65
+ efficiency: 0,
66
+ lastUpdated: Date.now()
67
+ };
68
+
69
+ this.performanceMetrics = {
70
+ responseTimes: [],
71
+ hitRateHistory: [],
72
+ memoryUsageHistory: [],
73
+ operationCounts: new Map()
74
+ };
75
+
76
+ if (this.enableDiskCache) {
77
+ this.initDiskCache();
78
+ }
79
+
80
+ // Initialize monitoring
81
+ if (this.enableMonitoring) {
82
+ this.startMonitoring(monitoringInterval);
83
+ }
84
+
85
+ // Initialize auto cleanup
86
+ this.cleanupTimer = setInterval(() => {
87
+ this.cleanupExpired();
88
+ }, autoCleanupInterval);
89
+
90
+ // Eviction tracking is handled in the LRU cache dispose callback above
91
+ }
92
+
93
+ async initDiskCache() {
94
+ try {
95
+ await fs.mkdir(this.diskCacheDir, { recursive: true });
96
+ } catch (error) {
97
+ console.error('Failed to create cache directory:', error);
98
+ this.enableDiskCache = false;
99
+ }
100
+ }
101
+
102
+ generateKey(url, options = {}) {
103
+ const keyData = { url, ...options };
104
+ const hash = crypto.createHash('sha256');
105
+ hash.update(JSON.stringify(keyData));
106
+ return hash.digest('hex');
107
+ }
108
+
109
+ async get(key) {
110
+ const startTime = performance.now();
111
+ this.stats.totalRequests++;
112
+
113
+ try {
114
+ // Check memory cache first
115
+ if (this.memoryCache.has(key)) {
116
+ this.stats.hits++;
117
+ this.stats.memoryHits++;
118
+ const value = this.memoryCache.get(key);
119
+ this.recordResponseTime(performance.now() - startTime);
120
+ this.emit('hit', key, 'memory');
121
+ return value;
122
+ }
123
+
124
+ // Check disk cache if enabled
125
+ if (this.enableDiskCache) {
126
+ const diskData = await this.getDiskCache(key);
127
+ if (diskData) {
128
+ this.stats.hits++;
129
+ this.stats.diskHits++;
130
+ // Promote to memory cache
131
+ this.memoryCache.set(key, diskData);
132
+ this.recordResponseTime(performance.now() - startTime);
133
+ this.emit('hit', key, 'disk');
134
+ return diskData;
135
+ }
136
+ }
137
+
138
+ this.stats.misses++;
139
+ this.recordResponseTime(performance.now() - startTime);
140
+ this.emit('miss', key);
141
+ return null;
142
+ } catch (error) {
143
+ this.emit('error', error, key);
144
+ throw error;
145
+ }
146
+ }
147
+
148
+ async set(key, value, options = {}) {
149
+ const { ttl, tags = [], dependencies = [], events = [] } = options;
150
+
151
+ // Handle legacy API
152
+ const finalTtl = typeof options === 'number' ? options : ttl;
153
+
154
+ // Set in memory cache
155
+ this.memoryCache.set(key, value, { ttl: finalTtl });
156
+
157
+ // Set in disk cache if enabled
158
+ if (this.enableDiskCache) {
159
+ await this.setDiskCache(key, value, finalTtl);
160
+ }
161
+
162
+ // Handle tags
163
+ if (tags.length > 0) {
164
+ this.tags.set(key, new Set(tags));
165
+ tags.forEach(tag => {
166
+ if (!this.taggedKeys.has(tag)) {
167
+ this.taggedKeys.set(tag, new Set());
168
+ }
169
+ this.taggedKeys.get(tag).add(key);
170
+ });
171
+ }
172
+
173
+ // Handle dependencies
174
+ if (dependencies.length > 0 && this.dependencyTracking) {
175
+ this.reverseDependencies.set(key, new Set(dependencies));
176
+ dependencies.forEach(dep => {
177
+ if (!this.dependencies.has(dep)) {
178
+ this.dependencies.set(dep, new Set());
179
+ }
180
+ this.dependencies.get(dep).add(key);
181
+ });
182
+ }
183
+
184
+ // Handle event-based invalidation
185
+ events.forEach(event => {
186
+ if (!this.eventInvalidators.has(event)) {
187
+ this.eventInvalidators.set(event, new Set());
188
+ }
189
+ this.eventInvalidators.get(event).add(key);
190
+ });
191
+
192
+ this.emit('set', key, value, options);
193
+ }
194
+
195
+ async getDiskCache(key) {
196
+ try {
197
+ const filePath = path.join(this.diskCacheDir, `${key}.json`);
198
+ const data = await fs.readFile(filePath, 'utf8');
199
+ const cached = JSON.parse(data);
200
+
201
+ // Check if expired
202
+ if (cached.expiry && Date.now() > cached.expiry) {
203
+ await fs.unlink(filePath).catch(() => {}); // Clean up expired file
204
+ return null;
205
+ }
206
+
207
+ return cached.value;
208
+ } catch (error) {
209
+ return null;
210
+ }
211
+ }
212
+
213
+ async setDiskCache(key, value, ttl) {
214
+ try {
215
+ const filePath = path.join(this.diskCacheDir, `${key}.json`);
216
+ const expiry = ttl ? Date.now() + ttl : null;
217
+ const data = JSON.stringify({ value, expiry }, null, 2);
218
+ await fs.writeFile(filePath, data, 'utf8');
219
+ } catch (error) {
220
+ console.error('Failed to write disk cache:', error);
221
+ }
222
+ }
223
+
224
+ has(key) {
225
+ return this.memoryCache.has(key);
226
+ }
227
+
228
+ delete(key) {
229
+ this.memoryCache.delete(key);
230
+ if (this.enableDiskCache) {
231
+ const filePath = path.join(this.diskCacheDir, `${key}.json`);
232
+ fs.unlink(filePath).catch(() => {});
233
+ }
234
+
235
+ // Clean up metadata
236
+ this.cleanupKeyMetadata(key);
237
+
238
+ // Handle dependency invalidation
239
+ if (this.dependencyTracking && this.dependencies.has(key)) {
240
+ const dependentKeys = this.dependencies.get(key);
241
+ dependentKeys.forEach(depKey => this.delete(depKey));
242
+ this.dependencies.delete(key);
243
+ }
244
+
245
+ this.stats.invalidations++;
246
+ this.emit('delete', key);
247
+ }
248
+
249
+ clear() {
250
+ this.memoryCache.clear();
251
+ if (this.enableDiskCache) {
252
+ this.clearDiskCache();
253
+ }
254
+ }
255
+
256
+ async clearDiskCache() {
257
+ try {
258
+ const files = await fs.readdir(this.diskCacheDir);
259
+ const deletePromises = files
260
+ .filter(file => file.endsWith('.json'))
261
+ .map(file => fs.unlink(path.join(this.diskCacheDir, file)));
262
+ await Promise.all(deletePromises);
263
+ } catch (error) {
264
+ console.error('Failed to clear disk cache:', error);
265
+ }
266
+ }
267
+
268
+ getStats() {
269
+ const total = this.stats.hits + this.stats.misses;
270
+ return {
271
+ ...this.stats,
272
+ hitRate: total > 0 ? (this.stats.hits / total) * 100 : 0,
273
+ memorySize: this.memoryCache.size,
274
+ memoryMax: this.memoryCache.max
275
+ };
276
+ }
277
+
278
+ async cleanupExpired() {
279
+ if (!this.enableDiskCache) return;
280
+
281
+ try {
282
+ const files = await fs.readdir(this.diskCacheDir);
283
+ for (const file of files) {
284
+ if (!file.endsWith('.json')) continue;
285
+
286
+ const filePath = path.join(this.diskCacheDir, file);
287
+ const data = await fs.readFile(filePath, 'utf8');
288
+ const cached = JSON.parse(data);
289
+
290
+ if (cached.expiry && Date.now() > cached.expiry) {
291
+ await fs.unlink(filePath);
292
+ }
293
+ }
294
+ } catch (error) {
295
+ console.error('Failed to cleanup expired cache:', error);
296
+ }
297
+ }
298
+
299
+ // ADVANCED CACHE INVALIDATION STRATEGIES
300
+
301
+ /**
302
+ * Invalidate cache entries by tags
303
+ */
304
+ invalidateByTag(tag) {
305
+ if (!this.taggedKeys.has(tag)) return 0;
306
+
307
+ const keys = this.taggedKeys.get(tag);
308
+ let invalidated = 0;
309
+
310
+ keys.forEach(key => {
311
+ this.delete(key);
312
+ invalidated++;
313
+ });
314
+
315
+ this.taggedKeys.delete(tag);
316
+ this.emit('invalidateByTag', tag, invalidated);
317
+ return invalidated;
318
+ }
319
+
320
+ /**
321
+ * Invalidate cache entries by multiple tags
322
+ */
323
+ invalidateByTags(tags) {
324
+ let totalInvalidated = 0;
325
+ tags.forEach(tag => {
326
+ totalInvalidated += this.invalidateByTag(tag);
327
+ });
328
+ return totalInvalidated;
329
+ }
330
+
331
+ /**
332
+ * Invalidate cache entries by event
333
+ */
334
+ invalidateByEvent(event) {
335
+ if (!this.eventInvalidators.has(event)) return 0;
336
+
337
+ const keys = this.eventInvalidators.get(event);
338
+ let invalidated = 0;
339
+
340
+ keys.forEach(key => {
341
+ this.delete(key);
342
+ invalidated++;
343
+ });
344
+
345
+ this.eventInvalidators.delete(event);
346
+ this.emit('invalidateByEvent', event, invalidated);
347
+ return invalidated;
348
+ }
349
+
350
+ /**
351
+ * Invalidate cache entries by pattern
352
+ */
353
+ invalidateByPattern(pattern) {
354
+ const regex = new RegExp(pattern);
355
+ const keysToDelete = [];
356
+
357
+ // Check memory cache keys
358
+ for (const key of this.memoryCache.keys()) {
359
+ if (regex.test(key)) {
360
+ keysToDelete.push(key);
361
+ }
362
+ }
363
+
364
+ keysToDelete.forEach(key => this.delete(key));
365
+ this.emit('invalidateByPattern', pattern, keysToDelete.length);
366
+ return keysToDelete.length;
367
+ }
368
+
369
+ /**
370
+ * Selective cache clearing with options
371
+ */
372
+ selectiveClear(options = {}) {
373
+ const { tags, events, pattern, olderThan, excludeKeys = [] } = options;
374
+ let cleared = 0;
375
+
376
+ if (tags) {
377
+ cleared += this.invalidateByTags(Array.isArray(tags) ? tags : [tags]);
378
+ }
379
+
380
+ if (events) {
381
+ const eventList = Array.isArray(events) ? events : [events];
382
+ eventList.forEach(event => {
383
+ cleared += this.invalidateByEvent(event);
384
+ });
385
+ }
386
+
387
+ if (pattern) {
388
+ cleared += this.invalidateByPattern(pattern);
389
+ }
390
+
391
+ if (olderThan) {
392
+ cleared += this.clearOlderThan(olderThan, excludeKeys);
393
+ }
394
+
395
+ this.emit('selectiveClear', options, cleared);
396
+ return cleared;
397
+ }
398
+
399
+ /**
400
+ * Clear entries older than specified time
401
+ */
402
+ clearOlderThan(maxAge, excludeKeys = []) {
403
+ const cutoff = Date.now() - maxAge;
404
+ const excludeSet = new Set(excludeKeys);
405
+ let cleared = 0;
406
+
407
+ // This would require storing timestamps with each entry
408
+ // For now, we'll implement a basic version
409
+ this.memoryCache.forEach((value, key) => {
410
+ if (!excludeSet.has(key)) {
411
+ // LRU cache doesn't expose creation time, so we'll use a different approach
412
+ // This is a simplified implementation
413
+ this.delete(key);
414
+ cleared++;
415
+ }
416
+ });
417
+
418
+ return cleared;
419
+ }
420
+
421
+ // CACHE WARMING FEATURES
422
+
423
+ /**
424
+ * Add cache warming job
425
+ */
426
+ addWarmingJob(jobId, dataProvider, options = {}) {
427
+ const job = {
428
+ id: jobId,
429
+ dataProvider,
430
+ priority: options.priority || 1,
431
+ interval: options.interval || null,
432
+ enabled: options.enabled !== false,
433
+ lastRun: null,
434
+ nextRun: options.scheduleTime || null
435
+ };
436
+
437
+ this.warmingJobs.set(jobId, job);
438
+
439
+ if (job.interval) {
440
+ this.scheduleWarmingJob(job);
441
+ }
442
+
443
+ this.emit('warmingJobAdded', jobId, job);
444
+ return job;
445
+ }
446
+
447
+ /**
448
+ * Remove cache warming job
449
+ */
450
+ removeWarmingJob(jobId) {
451
+ const job = this.warmingJobs.get(jobId);
452
+ if (job && job.timer) {
453
+ clearTimeout(job.timer);
454
+ }
455
+
456
+ const removed = this.warmingJobs.delete(jobId);
457
+ this.emit('warmingJobRemoved', jobId);
458
+ return removed;
459
+ }
460
+
461
+ /**
462
+ * Execute cache warming job
463
+ */
464
+ async executeWarmingJob(jobId) {
465
+ const job = this.warmingJobs.get(jobId);
466
+ if (!job || !job.enabled) return false;
467
+
468
+ try {
469
+ job.lastRun = Date.now();
470
+ const data = await job.dataProvider();
471
+
472
+ if (Array.isArray(data)) {
473
+ const batches = this.chunkArray(data, this.warmingBatchSize);
474
+
475
+ for (const batch of batches) {
476
+ await Promise.all(batch.map(async ({ key, value, options }) => {
477
+ await this.set(key, value, options);
478
+ this.stats.warmingHits++;
479
+ }));
480
+ }
481
+ }
482
+
483
+ if (job.interval) {
484
+ this.scheduleWarmingJob(job);
485
+ }
486
+
487
+ this.emit('warmingJobExecuted', jobId, data);
488
+ return true;
489
+ } catch (error) {
490
+ this.emit('warmingJobError', jobId, error);
491
+ return false;
492
+ }
493
+ }
494
+
495
+ /**
496
+ * Schedule warming job
497
+ */
498
+ scheduleWarmingJob(job) {
499
+ if (job.timer) {
500
+ clearTimeout(job.timer);
501
+ }
502
+
503
+ job.timer = setTimeout(() => {
504
+ this.executeWarmingJob(job.id);
505
+ }, job.interval);
506
+ }
507
+
508
+ /**
509
+ * Preemptive cache warming for popular queries
510
+ */
511
+ async warmPopularQueries(queries, dataProvider) {
512
+ const batches = this.chunkArray(queries, this.warmingBatchSize);
513
+ let warmed = 0;
514
+
515
+ for (const batch of batches) {
516
+ await Promise.all(batch.map(async (query) => {
517
+ try {
518
+ const key = this.generateKey(query);
519
+ if (!this.has(key)) {
520
+ const data = await dataProvider(query);
521
+ await this.set(key, data);
522
+ warmed++;
523
+ this.stats.warmingHits++;
524
+ }
525
+ } catch (error) {
526
+ this.emit('warmingError', query, error);
527
+ }
528
+ }));
529
+ }
530
+
531
+ this.emit('popularQueriesWarmed', warmed);
532
+ return warmed;
533
+ }
534
+
535
+ // CACHE STATISTICS & MONITORING
536
+
537
+ /**
538
+ * Start monitoring
539
+ */
540
+ startMonitoring(interval) {
541
+ if (this.monitoringTimer) {
542
+ clearInterval(this.monitoringTimer);
543
+ }
544
+
545
+ this.monitoringTimer = setInterval(() => {
546
+ this.updateStats();
547
+ this.emit('monitoring', this.getDetailedStats());
548
+ }, interval);
549
+ }
550
+
551
+ /**
552
+ * Stop monitoring
553
+ */
554
+ stopMonitoring() {
555
+ if (this.monitoringTimer) {
556
+ clearInterval(this.monitoringTimer);
557
+ this.monitoringTimer = null;
558
+ }
559
+ }
560
+
561
+ /**
562
+ * Update statistics
563
+ */
564
+ updateStats() {
565
+ const total = this.stats.hits + this.stats.misses;
566
+ const hitRate = total > 0 ? (this.stats.hits / total) * 100 : 0;
567
+
568
+ this.stats.efficiency = hitRate;
569
+ this.stats.lastUpdated = Date.now();
570
+
571
+ // Update history
572
+ this.performanceMetrics.hitRateHistory.push({
573
+ timestamp: Date.now(),
574
+ hitRate
575
+ });
576
+
577
+ // Keep only last 100 measurements
578
+ if (this.performanceMetrics.hitRateHistory.length > 100) {
579
+ this.performanceMetrics.hitRateHistory.shift();
580
+ }
581
+
582
+ // Update memory usage
583
+ this.stats.memoryUsage = this.calculateMemoryUsage();
584
+ this.performanceMetrics.memoryUsageHistory.push({
585
+ timestamp: Date.now(),
586
+ usage: this.stats.memoryUsage
587
+ });
588
+
589
+ if (this.performanceMetrics.memoryUsageHistory.length > 100) {
590
+ this.performanceMetrics.memoryUsageHistory.shift();
591
+ }
592
+ }
593
+
594
+ /**
595
+ * Get detailed statistics
596
+ */
597
+ getDetailedStats() {
598
+ const total = this.stats.hits + this.stats.misses;
599
+ const hitRate = total > 0 ? (this.stats.hits / total) * 100 : 0;
600
+ const averageResponseTime = this.performanceMetrics.responseTimes.length > 0
601
+ ? this.performanceMetrics.responseTimes.reduce((a, b) => a + b, 0) / this.performanceMetrics.responseTimes.length
602
+ : 0;
603
+
604
+ return {
605
+ ...this.stats,
606
+ hitRate,
607
+ averageResponseTime,
608
+ memorySize: this.memoryCache.size,
609
+ memoryMax: this.memoryCache.max,
610
+ cacheUtilization: (this.memoryCache.size / this.memoryCache.max) * 100,
611
+ warmingJobsCount: this.warmingJobs.size,
612
+ taggedKeysCount: this.taggedKeys.size,
613
+ dependenciesCount: this.dependencies.size,
614
+ performanceMetrics: {
615
+ recentHitRates: this.performanceMetrics.hitRateHistory.slice(-10),
616
+ recentMemoryUsage: this.performanceMetrics.memoryUsageHistory.slice(-10),
617
+ operationCounts: Object.fromEntries(this.performanceMetrics.operationCounts)
618
+ }
619
+ };
620
+ }
621
+
622
+ /**
623
+ * Generate cache performance report
624
+ */
625
+ generatePerformanceReport() {
626
+ const stats = this.getDetailedStats();
627
+ const report = {
628
+ timestamp: Date.now(),
629
+ summary: {
630
+ totalRequests: stats.totalRequests,
631
+ hitRate: stats.hitRate,
632
+ averageResponseTime: stats.averageResponseTime,
633
+ memoryUtilization: stats.cacheUtilization,
634
+ efficiency: stats.efficiency
635
+ },
636
+ breakdown: {
637
+ memoryHits: stats.memoryHits,
638
+ diskHits: stats.diskHits,
639
+ misses: stats.misses,
640
+ evictions: stats.evictions,
641
+ invalidations: stats.invalidations
642
+ },
643
+ trends: {
644
+ hitRateHistory: this.performanceMetrics.hitRateHistory,
645
+ memoryUsageHistory: this.performanceMetrics.memoryUsageHistory
646
+ },
647
+ configuration: {
648
+ maxSize: this.memoryCache.max,
649
+ enableDiskCache: this.enableDiskCache,
650
+ enableCacheWarming: this.enableCacheWarming,
651
+ dependencyTracking: this.dependencyTracking
652
+ },
653
+ recommendations: this.generateRecommendations(stats)
654
+ };
655
+
656
+ this.emit('performanceReport', report);
657
+ return report;
658
+ }
659
+
660
+ /**
661
+ * Generate performance recommendations
662
+ */
663
+ generateRecommendations(stats) {
664
+ const recommendations = [];
665
+
666
+ if (stats.hitRate < 50) {
667
+ recommendations.push({
668
+ type: 'hitRate',
669
+ severity: 'high',
670
+ message: 'Hit rate is below 50%. Consider increasing cache size or adjusting TTL.'
671
+ });
672
+ }
673
+
674
+ if (stats.cacheUtilization > 90) {
675
+ recommendations.push({
676
+ type: 'memory',
677
+ severity: 'medium',
678
+ message: 'Cache utilization is high. Consider increasing max cache size.'
679
+ });
680
+ }
681
+
682
+ if (stats.averageResponseTime > 100) {
683
+ recommendations.push({
684
+ type: 'performance',
685
+ severity: 'medium',
686
+ message: 'Average response time is high. Check disk cache performance.'
687
+ });
688
+ }
689
+
690
+ if (!this.enableCacheWarming && stats.misses > stats.hits) {
691
+ recommendations.push({
692
+ type: 'warming',
693
+ severity: 'low',
694
+ message: 'Consider enabling cache warming for popular queries.'
695
+ });
696
+ }
697
+
698
+ return recommendations;
699
+ }
700
+
701
+ // UTILITY METHODS
702
+
703
+ /**
704
+ * Record response time
705
+ */
706
+ recordResponseTime(time) {
707
+ this.performanceMetrics.responseTimes.push(time);
708
+
709
+ // Keep only last 1000 measurements
710
+ if (this.performanceMetrics.responseTimes.length > 1000) {
711
+ this.performanceMetrics.responseTimes.shift();
712
+ }
713
+
714
+ // Update average
715
+ this.stats.averageResponseTime =
716
+ this.performanceMetrics.responseTimes.reduce((a, b) => a + b, 0) /
717
+ this.performanceMetrics.responseTimes.length;
718
+ }
719
+
720
+ /**
721
+ * Clean up key metadata
722
+ */
723
+ cleanupKeyMetadata(key) {
724
+ // Clean up tags
725
+ if (this.tags.has(key)) {
726
+ const keyTags = this.tags.get(key);
727
+ keyTags.forEach(tag => {
728
+ if (this.taggedKeys.has(tag)) {
729
+ this.taggedKeys.get(tag).delete(key);
730
+ if (this.taggedKeys.get(tag).size === 0) {
731
+ this.taggedKeys.delete(tag);
732
+ }
733
+ }
734
+ });
735
+ this.tags.delete(key);
736
+ }
737
+
738
+ // Clean up dependencies
739
+ if (this.reverseDependencies.has(key)) {
740
+ const deps = this.reverseDependencies.get(key);
741
+ deps.forEach(dep => {
742
+ if (this.dependencies.has(dep)) {
743
+ this.dependencies.get(dep).delete(key);
744
+ if (this.dependencies.get(dep).size === 0) {
745
+ this.dependencies.delete(dep);
746
+ }
747
+ }
748
+ });
749
+ this.reverseDependencies.delete(key);
750
+ }
751
+
752
+ // Clean up event invalidators
753
+ for (const [event, keys] of this.eventInvalidators) {
754
+ keys.delete(key);
755
+ if (keys.size === 0) {
756
+ this.eventInvalidators.delete(event);
757
+ }
758
+ }
759
+ }
760
+
761
+ /**
762
+ * Calculate memory usage
763
+ */
764
+ calculateMemoryUsage() {
765
+ // Rough estimation of memory usage
766
+ let size = 0;
767
+ this.memoryCache.forEach((value, key) => {
768
+ size += this.estimateObjectSize(key) + this.estimateObjectSize(value);
769
+ });
770
+ return size;
771
+ }
772
+
773
+ /**
774
+ * Estimate object size in bytes
775
+ */
776
+ estimateObjectSize(obj) {
777
+ if (obj === null || obj === undefined) return 0;
778
+ if (typeof obj === 'string') return obj.length * 2;
779
+ if (typeof obj === 'number') return 8;
780
+ if (typeof obj === 'boolean') return 4;
781
+ if (typeof obj === 'object') {
782
+ return JSON.stringify(obj).length * 2;
783
+ }
784
+ return 0;
785
+ }
786
+
787
+ /**
788
+ * Chunk array into smaller arrays
789
+ */
790
+ chunkArray(array, chunkSize) {
791
+ const chunks = [];
792
+ for (let i = 0; i < array.length; i += chunkSize) {
793
+ chunks.push(array.slice(i, i + chunkSize));
794
+ }
795
+ return chunks;
796
+ }
797
+
798
+ /**
799
+ * Cleanup resources
800
+ */
801
+ destroy() {
802
+ if (this.cleanupTimer) {
803
+ clearInterval(this.cleanupTimer);
804
+ }
805
+
806
+ if (this.monitoringTimer) {
807
+ clearInterval(this.monitoringTimer);
808
+ }
809
+
810
+ this.warmingJobs.forEach(job => {
811
+ if (job.timer) {
812
+ clearTimeout(job.timer);
813
+ }
814
+ });
815
+
816
+ this.removeAllListeners();
817
+ this.emit('destroyed');
818
+ }
819
+ }
820
+
821
+ export default CacheManager;