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.
- package/CLAUDE.md +315 -0
- package/LICENSE +21 -0
- package/README.md +181 -0
- package/package.json +115 -0
- package/server.js +1963 -0
- package/setup.js +112 -0
- package/src/constants/config.js +615 -0
- package/src/core/ActionExecutor.js +1104 -0
- package/src/core/AlertNotificationSystem.js +601 -0
- package/src/core/AuthManager.js +315 -0
- package/src/core/ChangeTracker.js +2306 -0
- package/src/core/JobManager.js +687 -0
- package/src/core/LLMsTxtAnalyzer.js +753 -0
- package/src/core/LocalizationManager.js +1615 -0
- package/src/core/PerformanceManager.js +828 -0
- package/src/core/ResearchOrchestrator.js +1327 -0
- package/src/core/SnapshotManager.js +1037 -0
- package/src/core/StealthBrowserManager.js +1795 -0
- package/src/core/WebhookDispatcher.js +745 -0
- package/src/core/analysis/ContentAnalyzer.js +749 -0
- package/src/core/analysis/LinkAnalyzer.js +972 -0
- package/src/core/cache/CacheManager.js +821 -0
- package/src/core/connections/ConnectionPool.js +553 -0
- package/src/core/crawlers/BFSCrawler.js +845 -0
- package/src/core/integrations/PerformanceIntegration.js +377 -0
- package/src/core/llm/AnthropicProvider.js +135 -0
- package/src/core/llm/LLMManager.js +415 -0
- package/src/core/llm/LLMProvider.js +97 -0
- package/src/core/llm/OpenAIProvider.js +127 -0
- package/src/core/processing/BrowserProcessor.js +986 -0
- package/src/core/processing/ContentProcessor.js +505 -0
- package/src/core/processing/PDFProcessor.js +448 -0
- package/src/core/processing/StreamProcessor.js +673 -0
- package/src/core/queue/QueueManager.js +98 -0
- package/src/core/workers/WorkerPool.js +585 -0
- package/src/core/workers/worker.js +743 -0
- package/src/monitoring/healthCheck.js +600 -0
- package/src/monitoring/metrics.js +761 -0
- package/src/optimization/wave3-optimizations.js +932 -0
- package/src/security/security-patches.js +120 -0
- package/src/security/security-tests.js +355 -0
- package/src/security/wave3-security.js +652 -0
- package/src/tools/advanced/BatchScrapeTool.js +1089 -0
- package/src/tools/advanced/ScrapeWithActionsTool.js +669 -0
- package/src/tools/crawl/crawlDeep.js +449 -0
- package/src/tools/crawl/mapSite.js +400 -0
- package/src/tools/extract/analyzeContent.js +624 -0
- package/src/tools/extract/extractContent.js +329 -0
- package/src/tools/extract/processDocument.js +503 -0
- package/src/tools/extract/summarizeContent.js +376 -0
- package/src/tools/llmstxt/generateLLMsTxt.js +570 -0
- package/src/tools/research/deepResearch.js +706 -0
- package/src/tools/search/adapters/duckduckgoSearch.js +398 -0
- package/src/tools/search/adapters/googleSearch.js +236 -0
- package/src/tools/search/adapters/searchProviderFactory.js +96 -0
- package/src/tools/search/queryExpander.js +543 -0
- package/src/tools/search/ranking/ResultDeduplicator.js +676 -0
- package/src/tools/search/ranking/ResultRanker.js +497 -0
- package/src/tools/search/searchWeb.js +482 -0
- package/src/tools/tracking/trackChanges.js +1355 -0
- package/src/utils/CircuitBreaker.js +515 -0
- package/src/utils/ErrorHandlingConfig.js +342 -0
- package/src/utils/HumanBehaviorSimulator.js +569 -0
- package/src/utils/Logger.js +568 -0
- package/src/utils/MemoryMonitor.js +173 -0
- package/src/utils/RetryManager.js +386 -0
- package/src/utils/contentUtils.js +588 -0
- package/src/utils/domainFilter.js +612 -0
- package/src/utils/inputValidation.js +766 -0
- package/src/utils/rateLimiter.js +196 -0
- package/src/utils/robotsChecker.js +91 -0
- package/src/utils/securityMiddleware.js +416 -0
- package/src/utils/sitemapParser.js +678 -0
- package/src/utils/ssrfProtection.js +640 -0
- 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;
|