@fanboynz/network-scanner 2.0.55 → 2.0.57
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/.github/workflows/npm-publish.yml +3 -4
- package/lib/browserhealth.js +207 -179
- package/lib/cloudflare.js +117 -65
- package/lib/ignore_similar.js +78 -209
- package/lib/post-processing.js +282 -356
- package/lib/smart-cache.js +347 -267
- package/nwss.js +53 -13
- package/package.json +3 -2
package/lib/smart-cache.js
CHANGED
|
@@ -8,12 +8,15 @@ const fs = require('fs');
|
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const { formatLogMessage } = require('./colorize');
|
|
10
10
|
|
|
11
|
+
// Shared frozen empty object -- avoids allocating new {} for every default param
|
|
12
|
+
const EMPTY = Object.freeze({});
|
|
13
|
+
|
|
11
14
|
/**
|
|
12
15
|
* SmartCache - Intelligent caching system with multiple cache layers
|
|
13
16
|
* @class
|
|
14
17
|
*/
|
|
15
18
|
class SmartCache {
|
|
16
|
-
constructor(options =
|
|
19
|
+
constructor(options = EMPTY) {
|
|
17
20
|
// Calculate dynamic values first
|
|
18
21
|
const concurrency = options.concurrency || 6;
|
|
19
22
|
const optimalHeapLimit = this._calculateOptimalHeapLimit(concurrency);
|
|
@@ -44,16 +47,33 @@ class SmartCache {
|
|
|
44
47
|
this.saveInProgress = false;
|
|
45
48
|
this.saveTimeout = null;
|
|
46
49
|
this.pendingSave = false;
|
|
47
|
-
|
|
48
|
-
//
|
|
50
|
+
|
|
51
|
+
// Cache hot-path flags BEFORE _initializeCaches uses them
|
|
52
|
+
this._debug = this.options.forceDebug;
|
|
53
|
+
this._highConcurrency = this.options.concurrency > 10;
|
|
54
|
+
this._criticalThreshold = this._highConcurrency ? 0.85 : 1.0;
|
|
55
|
+
this._warningThreshold = this._highConcurrency ? 0.70 : 0.85;
|
|
56
|
+
this._infoThreshold = this._highConcurrency ? 0.60 : 0.75;
|
|
57
|
+
|
|
58
|
+
// Initialize cache layers (may disable responseCache for high concurrency)
|
|
49
59
|
this._initializeCaches();
|
|
60
|
+
|
|
61
|
+
// Cache enable flags AFTER _initializeCaches which may modify options
|
|
62
|
+
this._enablePattern = this.options.enablePatternCache;
|
|
63
|
+
this._enableResponse = this.options.enableResponseCache;
|
|
64
|
+
this._enableWhois = this.options.enableWhoisCache;
|
|
65
|
+
this._enableRequest = this.options.enableRequestCache;
|
|
50
66
|
|
|
51
67
|
// Initialize statistics
|
|
52
68
|
this._initializeStats();
|
|
53
69
|
|
|
70
|
+
// Cached memory usage (updated by _checkMemoryPressure interval)
|
|
71
|
+
this._lastHeapUsed = 0;
|
|
72
|
+
this._memoryPressure = false;
|
|
73
|
+
|
|
54
74
|
|
|
55
75
|
// NEW: Clear request cache
|
|
56
|
-
if (this.
|
|
76
|
+
if (this._enableRequest) {
|
|
57
77
|
this.clearRequestCache();
|
|
58
78
|
}
|
|
59
79
|
|
|
@@ -110,15 +130,15 @@ class SmartCache {
|
|
|
110
130
|
});
|
|
111
131
|
|
|
112
132
|
// Pattern matching results cache - reduce size for high concurrency
|
|
113
|
-
const patternCacheSize = this.
|
|
133
|
+
const patternCacheSize = this._highConcurrency ? 500 : 1000;
|
|
114
134
|
this.patternCache = new LRUCache({
|
|
115
135
|
max: patternCacheSize,
|
|
116
136
|
ttl: this.options.ttl * 2 // Patterns are more stable
|
|
117
137
|
});
|
|
118
138
|
|
|
119
139
|
// Response content cache - aggressive limits for high concurrency
|
|
120
|
-
const responseCacheSize = this.
|
|
121
|
-
const responseCacheMemory = this.
|
|
140
|
+
const responseCacheSize = this._highConcurrency ? 50 : 200;
|
|
141
|
+
const responseCacheMemory = this._highConcurrency ? 20 * 1024 * 1024 : 50 * 1024 * 1024;
|
|
122
142
|
this.responseCache = new LRUCache({
|
|
123
143
|
max: responseCacheSize,
|
|
124
144
|
ttl: 1000 * 60 * 30, // 30 minutes for response content
|
|
@@ -127,21 +147,24 @@ class SmartCache {
|
|
|
127
147
|
});
|
|
128
148
|
|
|
129
149
|
// NEW: Request-level cache for --cache-requests feature
|
|
130
|
-
if (this.
|
|
150
|
+
if (this._enableRequest) {
|
|
131
151
|
this.requestCache = new LRUCache({
|
|
132
152
|
max: this.options.requestCacheMaxSize,
|
|
133
153
|
ttl: 1000 * 60 * 15, // 15 minutes for request cache (shorter than response cache)
|
|
134
154
|
maxSize: this.options.requestCacheMaxMemory,
|
|
135
155
|
sizeCalculation: (value) => {
|
|
136
|
-
let size =
|
|
137
|
-
if (value.headers)
|
|
156
|
+
let size = 100; // Base overhead for object shell + metadata fields
|
|
157
|
+
if (value.headers) {
|
|
158
|
+
// Estimate header size without JSON.stringify (each header ~50 bytes avg)
|
|
159
|
+
const keys = Object.keys(value.headers);
|
|
160
|
+
size += keys.length * 50;
|
|
161
|
+
}
|
|
138
162
|
if (value.body) size += value.body.length;
|
|
139
|
-
if (value.status) size += 4;
|
|
140
163
|
return size;
|
|
141
164
|
}
|
|
142
165
|
});
|
|
143
166
|
|
|
144
|
-
if (this.
|
|
167
|
+
if (this._debug) {
|
|
145
168
|
console.log(formatLogMessage('debug', `[SmartCache] Request cache initialized: ${this.options.requestCacheMaxSize} entries`));
|
|
146
169
|
}
|
|
147
170
|
}
|
|
@@ -149,7 +172,7 @@ class SmartCache {
|
|
|
149
172
|
// Disable response cache entirely for very high concurrency
|
|
150
173
|
if (this.options.concurrency > 15 || this.options.aggressiveMode) {
|
|
151
174
|
this.options.enableResponseCache = false;
|
|
152
|
-
if (this.
|
|
175
|
+
if (this._debug) {
|
|
153
176
|
console.log(formatLogMessage('debug',
|
|
154
177
|
`[SmartCache] Response cache disabled for high concurrency (${this.options.concurrency})`
|
|
155
178
|
));
|
|
@@ -163,14 +186,17 @@ class SmartCache {
|
|
|
163
186
|
});
|
|
164
187
|
|
|
165
188
|
// Similarity cache - reduce for high concurrency
|
|
166
|
-
const similarityCacheSize = this.
|
|
189
|
+
const similarityCacheSize = this._highConcurrency ? 1000 : 2000;
|
|
167
190
|
this.similarityCache = new LRUCache({
|
|
168
191
|
max: similarityCacheSize,
|
|
169
192
|
ttl: this.options.ttl
|
|
170
193
|
});
|
|
171
194
|
|
|
172
|
-
// Regex compilation cache
|
|
173
|
-
this.regexCache = new
|
|
195
|
+
// Regex compilation cache (bounded to prevent unbounded growth)
|
|
196
|
+
this.regexCache = new LRUCache({ max: 500 });
|
|
197
|
+
|
|
198
|
+
// Precompile the regex-stripping pattern (used in getCompiledRegex)
|
|
199
|
+
this._regexStripPattern = /^\/(.*)\/$/;
|
|
174
200
|
}
|
|
175
201
|
|
|
176
202
|
/**
|
|
@@ -210,16 +236,16 @@ class SmartCache {
|
|
|
210
236
|
* @param {Object} context - Processing context
|
|
211
237
|
* @returns {boolean} True if domain should be skipped
|
|
212
238
|
*/
|
|
213
|
-
shouldSkipDomain(domain, context =
|
|
239
|
+
shouldSkipDomain(domain, context = EMPTY) {
|
|
214
240
|
const cacheKey = this._generateCacheKey(domain, context);
|
|
241
|
+
const cached = this.domainCache.get(cacheKey);
|
|
215
242
|
|
|
216
|
-
if (
|
|
243
|
+
if (cached !== undefined) {
|
|
217
244
|
this.stats.hits++;
|
|
218
|
-
if (this.
|
|
219
|
-
const cached = this.domainCache.get(cacheKey);
|
|
245
|
+
if (this._debug) {
|
|
220
246
|
const age = Date.now() - cached.timestamp;
|
|
221
247
|
console.log(formatLogMessage('debug',
|
|
222
|
-
`[SmartCache] Cache hit for ${domain} (age: ${Math.round(age/1000)}s
|
|
248
|
+
`[SmartCache] Cache hit for ${domain} (age: ${Math.round(age/1000)}s)`
|
|
223
249
|
));
|
|
224
250
|
}
|
|
225
251
|
return true;
|
|
@@ -235,18 +261,19 @@ class SmartCache {
|
|
|
235
261
|
* @param {Object} context - Processing context
|
|
236
262
|
* @param {Object} metadata - Additional metadata to store
|
|
237
263
|
*/
|
|
238
|
-
markDomainProcessed(domain, context =
|
|
264
|
+
markDomainProcessed(domain, context = EMPTY, metadata = EMPTY) {
|
|
239
265
|
const cacheKey = this._generateCacheKey(domain, context);
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
266
|
+
// Consistent property order ensures V8 reuses the same hidden class
|
|
267
|
+
const entry = { timestamp: 0, domain: '', context: null, metadata: null };
|
|
268
|
+
entry.timestamp = Date.now();
|
|
269
|
+
entry.domain = domain;
|
|
270
|
+
entry.context = context;
|
|
271
|
+
entry.metadata = metadata;
|
|
272
|
+
this.domainCache.set(cacheKey, entry);
|
|
273
|
+
|
|
274
|
+
if (this._debug) {
|
|
248
275
|
console.log(formatLogMessage('debug',
|
|
249
|
-
`[SmartCache] Marked ${domain} as processed
|
|
276
|
+
`[SmartCache] Marked ${domain} as processed`
|
|
250
277
|
));
|
|
251
278
|
}
|
|
252
279
|
}
|
|
@@ -258,20 +285,20 @@ class SmartCache {
|
|
|
258
285
|
* @private
|
|
259
286
|
*/
|
|
260
287
|
_normalizeUrl(url) {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
288
|
+
// Fast path: strip fragment with indexOf (avoids new URL() for most URLs)
|
|
289
|
+
const hashIdx = url.indexOf('#');
|
|
290
|
+
let normalized = hashIdx !== -1 ? url.substring(0, hashIdx) : url;
|
|
291
|
+
|
|
292
|
+
// Strip trailing slash (but not root path '/')
|
|
293
|
+
if (normalized.length > 1 && normalized.charCodeAt(normalized.length - 1) === 47) { // '/'
|
|
294
|
+
// Make sure we're not stripping query string trailing slash
|
|
295
|
+
const qIdx = normalized.indexOf('?');
|
|
296
|
+
if (qIdx === -1 || normalized.length - 1 < qIdx) {
|
|
297
|
+
normalized = normalized.slice(0, -1);
|
|
269
298
|
}
|
|
270
|
-
parsedUrl.pathname = pathname;
|
|
271
|
-
return parsedUrl.toString();
|
|
272
|
-
} catch (err) {
|
|
273
|
-
return url; // Return original if URL parsing fails
|
|
274
299
|
}
|
|
300
|
+
|
|
301
|
+
return normalized;
|
|
275
302
|
}
|
|
276
303
|
|
|
277
304
|
|
|
@@ -284,15 +311,12 @@ class SmartCache {
|
|
|
284
311
|
*/
|
|
285
312
|
_generateCacheKey(domain, context) {
|
|
286
313
|
const { filterRegex, searchString, resourceType, nettools } = context;
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
].filter(Boolean);
|
|
294
|
-
|
|
295
|
-
return components.join(':');
|
|
314
|
+
let key = domain;
|
|
315
|
+
if (filterRegex) key += ':' + filterRegex;
|
|
316
|
+
if (searchString) key += ':' + searchString;
|
|
317
|
+
if (resourceType) key += ':' + resourceType;
|
|
318
|
+
if (nettools) key += ':nt';
|
|
319
|
+
return key;
|
|
296
320
|
}
|
|
297
321
|
|
|
298
322
|
/**
|
|
@@ -302,7 +326,7 @@ class SmartCache {
|
|
|
302
326
|
* @returns {boolean} True if should bypass cache
|
|
303
327
|
* @private
|
|
304
328
|
*/
|
|
305
|
-
_shouldBypassCache(url, siteConfig =
|
|
329
|
+
_shouldBypassCache(url, siteConfig = EMPTY) {
|
|
306
330
|
if (!siteConfig.bypass_cache) return false;
|
|
307
331
|
|
|
308
332
|
const bypassPatterns = Array.isArray(siteConfig.bypass_cache)
|
|
@@ -310,12 +334,8 @@ class SmartCache {
|
|
|
310
334
|
: [siteConfig.bypass_cache];
|
|
311
335
|
|
|
312
336
|
return bypassPatterns.some(pattern => {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
return regex.test(url);
|
|
316
|
-
} catch (regexErr) {
|
|
317
|
-
return pattern === url; // Exact string match fallback
|
|
318
|
-
}
|
|
337
|
+
const regex = this.getCompiledRegex(pattern);
|
|
338
|
+
return regex ? regex.test(url) : pattern === url;
|
|
319
339
|
});
|
|
320
340
|
}
|
|
321
341
|
|
|
@@ -326,23 +346,20 @@ class SmartCache {
|
|
|
326
346
|
* @returns {string} Cache key
|
|
327
347
|
* @private
|
|
328
348
|
*/
|
|
329
|
-
_generateRequestCacheKey(url, options =
|
|
349
|
+
_generateRequestCacheKey(url, options = EMPTY, normalizedUrl = null) {
|
|
330
350
|
const method = options.method || 'GET';
|
|
331
|
-
const
|
|
351
|
+
const normUrl = normalizedUrl || this._normalizeUrl(url);
|
|
332
352
|
const headers = options.headers || {};
|
|
333
|
-
|
|
334
|
-
// Include relevant headers that might affect the response
|
|
335
|
-
const relevantHeaders = {};
|
|
336
353
|
const importantHeaders = ['accept', 'accept-encoding', 'accept-language', 'user-agent'];
|
|
337
354
|
|
|
355
|
+
// Build header portion of key by direct concat (avoids JSON.stringify + intermediate array)
|
|
356
|
+
let headerKey = '';
|
|
338
357
|
for (const header of importantHeaders) {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
}
|
|
358
|
+
const val = headers[header];
|
|
359
|
+
if (val) headerKey += header + '=' + val + '&';
|
|
342
360
|
}
|
|
343
361
|
|
|
344
|
-
|
|
345
|
-
return cacheKey;
|
|
362
|
+
return method + '|' + normUrl + '|' + headerKey;
|
|
346
363
|
}
|
|
347
364
|
|
|
348
365
|
/**
|
|
@@ -351,8 +368,8 @@ class SmartCache {
|
|
|
351
368
|
* @param {Object} options - Request options
|
|
352
369
|
* @returns {Object|null} Cached request result or null
|
|
353
370
|
*/
|
|
354
|
-
getCachedRequest(url, options =
|
|
355
|
-
if (!this.
|
|
371
|
+
getCachedRequest(url, options = EMPTY) {
|
|
372
|
+
if (!this._enableRequest) return null;
|
|
356
373
|
|
|
357
374
|
// Check bypass_cache setting
|
|
358
375
|
if (options.siteConfig && this._shouldBypassCache(url, options.siteConfig)) {
|
|
@@ -364,16 +381,15 @@ class SmartCache {
|
|
|
364
381
|
|
|
365
382
|
if (cached) {
|
|
366
383
|
this.stats.requestCacheHits++;
|
|
367
|
-
if (this.
|
|
384
|
+
if (this._debug) {
|
|
368
385
|
console.log(formatLogMessage('debug',
|
|
369
386
|
`[SmartCache] Request cache hit for ${url.substring(0, 50)}... (${cached.status || 'unknown status'})`
|
|
370
387
|
));
|
|
371
388
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
};
|
|
389
|
+
// Set directly on cached object (avoids spread allocation per hit)
|
|
390
|
+
cached.fromCache = true;
|
|
391
|
+
cached.cacheAge = Date.now() - cached.timestamp;
|
|
392
|
+
return cached;
|
|
377
393
|
}
|
|
378
394
|
|
|
379
395
|
this.stats.requestCacheMisses++;
|
|
@@ -386,8 +402,8 @@ class SmartCache {
|
|
|
386
402
|
* @param {Object} options - Request options
|
|
387
403
|
* @param {Object} result - Request result (status, headers, body)
|
|
388
404
|
*/
|
|
389
|
-
cacheRequest(url, options =
|
|
390
|
-
if (!this.
|
|
405
|
+
cacheRequest(url, options = EMPTY, result = EMPTY) {
|
|
406
|
+
if (!this._enableRequest) return;
|
|
391
407
|
|
|
392
408
|
// Check bypass_cache setting
|
|
393
409
|
if (options.siteConfig && this._shouldBypassCache(url, options.siteConfig)) {
|
|
@@ -400,9 +416,8 @@ class SmartCache {
|
|
|
400
416
|
return;
|
|
401
417
|
}
|
|
402
418
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
if (memUsage.heapUsed > this.options.maxHeapUsage * threshold) {
|
|
419
|
+
// Use cached memory pressure flag (updated by periodic _checkMemoryPressure)
|
|
420
|
+
if (this._memoryPressure) {
|
|
406
421
|
this.stats.requestCacheSkips++;
|
|
407
422
|
this._logMemorySkip('request cache');
|
|
408
423
|
return;
|
|
@@ -413,39 +428,34 @@ class SmartCache {
|
|
|
413
428
|
return;
|
|
414
429
|
}
|
|
415
430
|
|
|
416
|
-
|
|
431
|
+
// Check size limit BEFORE building cache value object (avoids wasted allocation)
|
|
432
|
+
const method = options.method || 'GET';
|
|
433
|
+
const isHeadRequest = method.toUpperCase() === 'HEAD';
|
|
434
|
+
if (!isHeadRequest && result.body && result.body.length >= 10485760) {
|
|
435
|
+
return; // Over 10MB, skip
|
|
436
|
+
}
|
|
437
|
+
|
|
417
438
|
const normalizedUrl = this._normalizeUrl(url);
|
|
418
|
-
const
|
|
439
|
+
const cacheKey = this._generateRequestCacheKey(url, options, normalizedUrl);
|
|
440
|
+
|
|
441
|
+
this.requestCache.set(cacheKey, {
|
|
419
442
|
timestamp: Date.now(),
|
|
420
443
|
status: result.status,
|
|
421
444
|
statusText: result.statusText,
|
|
422
445
|
headers: result.headers,
|
|
423
446
|
body: result.body,
|
|
424
|
-
url: normalizedUrl,
|
|
425
|
-
originalUrl: url,
|
|
426
|
-
requestOptions: {
|
|
427
|
-
|
|
428
|
-
headers: options.headers || {}
|
|
429
|
-
}
|
|
430
|
-
};
|
|
431
|
-
|
|
432
|
-
// HEAD requests have no body, so always cache them
|
|
433
|
-
// For other requests, only cache if body is reasonable size
|
|
434
|
-
const isHeadRequest = (options.method || 'GET').toUpperCase() === 'HEAD';
|
|
435
|
-
const shouldCache = isHeadRequest || (!result.body || result.body.length < 10 * 1024 * 1024);
|
|
447
|
+
url: normalizedUrl,
|
|
448
|
+
originalUrl: url,
|
|
449
|
+
requestOptions: { method: method, headers: options.headers || {} }
|
|
450
|
+
});
|
|
436
451
|
|
|
437
|
-
if (
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
));
|
|
445
|
-
// Log path distinction if different from original
|
|
446
|
-
if (normalizedUrl !== url) {
|
|
447
|
-
console.log(formatLogMessage('debug', `[SmartCache] URL normalized: ${url} -> ${normalizedUrl}`));
|
|
448
|
-
}
|
|
452
|
+
if (this._debug) {
|
|
453
|
+
const bodySize = result.body ? result.body.length : 0;
|
|
454
|
+
console.log(formatLogMessage('debug',
|
|
455
|
+
`[SmartCache] Cached request: ${normalizedUrl.substring(0, 50)}... (${result.status || 'unknown'}, ${Math.round(bodySize / 1024)}KB)`
|
|
456
|
+
));
|
|
457
|
+
if (normalizedUrl !== url) {
|
|
458
|
+
console.log(formatLogMessage('debug', `[SmartCache] URL normalized: ${url} -> ${normalizedUrl}`));
|
|
449
459
|
}
|
|
450
460
|
}
|
|
451
461
|
}
|
|
@@ -455,13 +465,13 @@ class SmartCache {
|
|
|
455
465
|
* Useful when switching between different JSON configs
|
|
456
466
|
*/
|
|
457
467
|
clearRequestCache() {
|
|
458
|
-
if (!this.
|
|
468
|
+
if (!this._enableRequest || !this.requestCache) return;
|
|
459
469
|
|
|
460
470
|
const clearedCount = this.requestCache.size;
|
|
461
471
|
this.requestCache.clear();
|
|
462
472
|
this.stats.requestCacheClears++;
|
|
463
473
|
|
|
464
|
-
if (this.
|
|
474
|
+
if (this._debug) {
|
|
465
475
|
console.log(formatLogMessage('debug',
|
|
466
476
|
`[SmartCache] Cleared request cache: ${clearedCount} entries removed`
|
|
467
477
|
));
|
|
@@ -474,7 +484,7 @@ class SmartCache {
|
|
|
474
484
|
* Get request cache statistics
|
|
475
485
|
*/
|
|
476
486
|
getRequestCacheStats() {
|
|
477
|
-
if (!this.
|
|
487
|
+
if (!this._enableRequest || !this.requestCache) {
|
|
478
488
|
return { enabled: false, size: 0, hitRate: '0%', memoryUsage: 0 };
|
|
479
489
|
}
|
|
480
490
|
|
|
@@ -501,25 +511,31 @@ class SmartCache {
|
|
|
501
511
|
* @returns {Object} Enhanced statistics including path distribution
|
|
502
512
|
*/
|
|
503
513
|
getEnhancedRequestCacheStats() {
|
|
504
|
-
if (!this.
|
|
514
|
+
if (!this._enableRequest || !this.requestCache) {
|
|
505
515
|
return this.getRequestCacheStats();
|
|
506
516
|
}
|
|
507
517
|
|
|
508
518
|
const basicStats = this.getRequestCacheStats();
|
|
509
519
|
|
|
510
|
-
// Analyze path distribution
|
|
511
|
-
const pathDistribution =
|
|
520
|
+
// Analyze path distribution using string split (avoids new URL() per entry)
|
|
521
|
+
const pathDistribution = {};
|
|
512
522
|
for (const [key, value] of this.requestCache.entries()) {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
523
|
+
const rawUrl = value.url || value.originalUrl;
|
|
524
|
+
if (!rawUrl) continue;
|
|
525
|
+
// Extract hostname+path: skip protocol, find first '/' after '://', take up to '?'
|
|
526
|
+
const protoEnd = rawUrl.indexOf('://');
|
|
527
|
+
if (protoEnd === -1) continue;
|
|
528
|
+
const pathStart = rawUrl.indexOf('/', protoEnd + 3);
|
|
529
|
+
const qIdx = rawUrl.indexOf('?', pathStart);
|
|
530
|
+
const pathKey = pathStart !== -1
|
|
531
|
+
? rawUrl.substring(protoEnd + 3, qIdx !== -1 ? qIdx : rawUrl.length)
|
|
532
|
+
: rawUrl.substring(protoEnd + 3);
|
|
533
|
+
pathDistribution[pathKey] = (pathDistribution[pathKey] || 0) + 1;
|
|
520
534
|
}
|
|
521
535
|
|
|
522
|
-
|
|
536
|
+
// Assign directly instead of spread
|
|
537
|
+
basicStats.pathDistribution = pathDistribution;
|
|
538
|
+
return basicStats;
|
|
523
539
|
}
|
|
524
540
|
|
|
525
541
|
/**
|
|
@@ -528,24 +544,25 @@ class SmartCache {
|
|
|
528
544
|
* @returns {RegExp} Compiled regex
|
|
529
545
|
*/
|
|
530
546
|
getCompiledRegex(pattern) {
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
try {
|
|
534
|
-
const regex = new RegExp(pattern.replace(/^\/(.*)\/$/, '$1'));
|
|
535
|
-
this.regexCache.set(pattern, regex);
|
|
536
|
-
} catch (err) {
|
|
537
|
-
if (this.options.forceDebug) {
|
|
538
|
-
console.log(formatLogMessage('debug',
|
|
539
|
-
`[SmartCache] Failed to compile regex: ${pattern}`
|
|
540
|
-
));
|
|
541
|
-
}
|
|
542
|
-
return null;
|
|
543
|
-
}
|
|
544
|
-
} else {
|
|
547
|
+
const cached = this.regexCache.get(pattern);
|
|
548
|
+
if (cached !== undefined) {
|
|
545
549
|
this.stats.regexCacheHits++;
|
|
550
|
+
return cached;
|
|
546
551
|
}
|
|
547
552
|
|
|
548
|
-
|
|
553
|
+
this.stats.regexCompilations++;
|
|
554
|
+
try {
|
|
555
|
+
const regex = new RegExp(pattern.replace(this._regexStripPattern, '$1'));
|
|
556
|
+
this.regexCache.set(pattern, regex);
|
|
557
|
+
return regex;
|
|
558
|
+
} catch (err) {
|
|
559
|
+
if (this._debug) {
|
|
560
|
+
console.log(formatLogMessage('debug',
|
|
561
|
+
`[SmartCache] Failed to compile regex: ${pattern}`
|
|
562
|
+
));
|
|
563
|
+
}
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
549
566
|
}
|
|
550
567
|
|
|
551
568
|
/**
|
|
@@ -555,14 +572,14 @@ class SmartCache {
|
|
|
555
572
|
* @returns {boolean|null} Cached result or null if not cached
|
|
556
573
|
*/
|
|
557
574
|
getCachedPatternMatch(url, pattern) {
|
|
558
|
-
if (!this.
|
|
575
|
+
if (!this._enablePattern) return null;
|
|
559
576
|
|
|
560
|
-
const cacheKey =
|
|
577
|
+
const cacheKey = url + ':' + pattern;
|
|
561
578
|
const cached = this.patternCache.get(cacheKey);
|
|
562
579
|
|
|
563
580
|
if (cached !== undefined) {
|
|
564
581
|
this.stats.patternHits++;
|
|
565
|
-
if (this.
|
|
582
|
+
if (this._debug) {
|
|
566
583
|
console.log(formatLogMessage('debug',
|
|
567
584
|
`[SmartCache] Pattern cache hit for ${url.substring(0, 50)}...`
|
|
568
585
|
));
|
|
@@ -581,9 +598,9 @@ class SmartCache {
|
|
|
581
598
|
* @param {boolean} result - Match result
|
|
582
599
|
*/
|
|
583
600
|
cachePatternMatch(url, pattern, result) {
|
|
584
|
-
if (!this.
|
|
601
|
+
if (!this._enablePattern) return;
|
|
585
602
|
|
|
586
|
-
const cacheKey =
|
|
603
|
+
const cacheKey = url + ':' + pattern;
|
|
587
604
|
this.patternCache.set(cacheKey, result);
|
|
588
605
|
}
|
|
589
606
|
|
|
@@ -593,7 +610,7 @@ class SmartCache {
|
|
|
593
610
|
* @returns {string|null} Cached content or null
|
|
594
611
|
*/
|
|
595
612
|
getCachedResponse(url) {
|
|
596
|
-
if (!this.
|
|
613
|
+
if (!this._enableResponse) return null;
|
|
597
614
|
|
|
598
615
|
// Note: Response cache doesn't have direct access to siteConfig
|
|
599
616
|
// bypass_cache primarily affects request cache, not response cache
|
|
@@ -602,7 +619,7 @@ class SmartCache {
|
|
|
602
619
|
const cached = this.responseCache.get(url);
|
|
603
620
|
if (cached) {
|
|
604
621
|
this.stats.responseHits++;
|
|
605
|
-
if (this.
|
|
622
|
+
if (this._debug) {
|
|
606
623
|
console.log(formatLogMessage('debug',
|
|
607
624
|
`[SmartCache] Response cache hit for ${url.substring(0, 50)}...`
|
|
608
625
|
));
|
|
@@ -620,18 +637,16 @@ class SmartCache {
|
|
|
620
637
|
* @param {string} content - Response content
|
|
621
638
|
*/
|
|
622
639
|
cacheResponse(url, content) {
|
|
623
|
-
if (!this.
|
|
640
|
+
if (!this._enableResponse) return;
|
|
624
641
|
|
|
625
642
|
// Skip response caching entirely for very high concurrency
|
|
626
|
-
if (this.
|
|
643
|
+
if (this._highConcurrency) {
|
|
627
644
|
this.stats.responseCacheSkips++;
|
|
628
645
|
return;
|
|
629
646
|
}
|
|
630
647
|
|
|
631
|
-
// Check memory before caching large content
|
|
632
|
-
|
|
633
|
-
const threshold = this.options.concurrency > 10 ? 0.7 : 0.8; // Lower threshold for high concurrency
|
|
634
|
-
if (memUsage.heapUsed > this.options.maxHeapUsage * threshold) {
|
|
648
|
+
// Check memory before caching large content (use cached flag from periodic check)
|
|
649
|
+
if (this._memoryPressure) {
|
|
635
650
|
this.stats.responseCacheSkips++;
|
|
636
651
|
this._logMemorySkip('response cache');
|
|
637
652
|
return;
|
|
@@ -651,14 +666,14 @@ class SmartCache {
|
|
|
651
666
|
* @returns {Object|null} Cached result or null
|
|
652
667
|
*/
|
|
653
668
|
getCachedNetTools(domain, tool, recordType = null) {
|
|
654
|
-
if (!this.
|
|
669
|
+
if (!this._enableWhois) return null;
|
|
655
670
|
|
|
656
|
-
const cacheKey =
|
|
671
|
+
const cacheKey = recordType ? tool + ':' + domain + ':' + recordType : tool + ':' + domain;
|
|
657
672
|
const cached = this.netToolsCache.get(cacheKey);
|
|
658
673
|
|
|
659
674
|
if (cached) {
|
|
660
675
|
this.stats.netToolsHits++;
|
|
661
|
-
if (this.
|
|
676
|
+
if (this._debug) {
|
|
662
677
|
console.log(formatLogMessage('debug',
|
|
663
678
|
`[SmartCache] ${tool.toUpperCase()} cache hit for ${domain}`
|
|
664
679
|
));
|
|
@@ -678,9 +693,9 @@ class SmartCache {
|
|
|
678
693
|
* @param {string} recordType - Record type for dig
|
|
679
694
|
*/
|
|
680
695
|
cacheNetTools(domain, tool, result, recordType = null) {
|
|
681
|
-
if (!this.
|
|
696
|
+
if (!this._enableWhois) return;
|
|
682
697
|
|
|
683
|
-
const cacheKey =
|
|
698
|
+
const cacheKey = recordType ? tool + ':' + domain + ':' + recordType : tool + ':' + domain;
|
|
684
699
|
this.netToolsCache.set(cacheKey, result);
|
|
685
700
|
}
|
|
686
701
|
|
|
@@ -691,7 +706,8 @@ class SmartCache {
|
|
|
691
706
|
* @param {number} similarity - Similarity score
|
|
692
707
|
*/
|
|
693
708
|
cacheSimilarity(domain1, domain2, similarity) {
|
|
694
|
-
|
|
709
|
+
// Consistent key without array allocation: alphabetically smaller domain first
|
|
710
|
+
const key = domain1 < domain2 ? domain1 + '|' + domain2 : domain2 + '|' + domain1;
|
|
695
711
|
this.similarityCache.set(key, similarity);
|
|
696
712
|
}
|
|
697
713
|
|
|
@@ -702,7 +718,7 @@ class SmartCache {
|
|
|
702
718
|
* @returns {number|null} Cached similarity or null
|
|
703
719
|
*/
|
|
704
720
|
getCachedSimilarity(domain1, domain2) {
|
|
705
|
-
const key =
|
|
721
|
+
const key = domain1 < domain2 ? domain1 + '|' + domain2 : domain2 + '|' + domain1;
|
|
706
722
|
const cached = this.similarityCache.get(key);
|
|
707
723
|
|
|
708
724
|
if (cached !== undefined) {
|
|
@@ -720,31 +736,32 @@ class SmartCache {
|
|
|
720
736
|
*/
|
|
721
737
|
_checkMemoryPressure() {
|
|
722
738
|
const memUsage = process.memoryUsage();
|
|
723
|
-
|
|
724
|
-
const
|
|
725
|
-
const
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
const criticalThreshold = this.options.concurrency > 10 ? 0.85 : 1.0;
|
|
729
|
-
const warningThreshold = this.options.concurrency > 10 ? 0.70 : 0.85;
|
|
730
|
-
const infoThreshold = this.options.concurrency > 10 ? 0.60 : 0.75;
|
|
739
|
+
this._lastHeapUsed = memUsage.heapUsed;
|
|
740
|
+
const maxHeap = this.options.maxHeapUsage;
|
|
741
|
+
const heapUsedMB = Math.round(memUsage.heapUsed / 1048576);
|
|
742
|
+
const maxHeapMB = Math.round(maxHeap / 1048576);
|
|
743
|
+
const usagePercent = (memUsage.heapUsed / maxHeap) * 100;
|
|
731
744
|
|
|
732
745
|
// Critical threshold - aggressive cleanup
|
|
733
|
-
if (memUsage.heapUsed >
|
|
746
|
+
if (memUsage.heapUsed > maxHeap * this._criticalThreshold) {
|
|
747
|
+
this._memoryPressure = true;
|
|
734
748
|
this._performMemoryCleanup('critical', heapUsedMB, maxHeapMB);
|
|
735
749
|
return true;
|
|
736
750
|
}
|
|
737
751
|
|
|
738
752
|
// Warning threshold - moderate cleanup
|
|
739
|
-
if (memUsage.heapUsed >
|
|
753
|
+
if (memUsage.heapUsed > maxHeap * this._warningThreshold) {
|
|
754
|
+
this._memoryPressure = true;
|
|
740
755
|
this._performMemoryCleanup('warning', heapUsedMB, maxHeapMB);
|
|
741
756
|
return true;
|
|
742
757
|
}
|
|
743
758
|
|
|
759
|
+
this._memoryPressure = false;
|
|
760
|
+
|
|
744
761
|
// Info threshold - log only
|
|
745
|
-
if (memUsage.heapUsed >
|
|
762
|
+
if (memUsage.heapUsed > maxHeap * this._infoThreshold) {
|
|
746
763
|
this.stats.memoryWarnings++;
|
|
747
|
-
if (this.
|
|
764
|
+
if (this._debug) {
|
|
748
765
|
console.log(formatLogMessage('debug',
|
|
749
766
|
`[SmartCache] Memory info: ${heapUsedMB}MB/${maxHeapMB}MB (${usagePercent.toFixed(1)}%)`
|
|
750
767
|
));
|
|
@@ -761,26 +778,26 @@ class SmartCache {
|
|
|
761
778
|
_performMemoryCleanup(level, heapUsedMB, maxHeapMB) {
|
|
762
779
|
this.stats.memoryPressureEvents++;
|
|
763
780
|
|
|
764
|
-
if (this.
|
|
781
|
+
if (this._debug) {
|
|
765
782
|
console.log(formatLogMessage('debug',
|
|
766
783
|
`[SmartCache] Memory ${level}: ${heapUsedMB}MB/${maxHeapMB}MB, performing cleanup...`
|
|
767
784
|
));
|
|
768
785
|
}
|
|
769
786
|
|
|
770
|
-
if (level === 'critical' || this.
|
|
787
|
+
if (level === 'critical' || this._highConcurrency) {
|
|
771
788
|
// Aggressive cleanup - clear volatile caches
|
|
772
789
|
this.responseCache.clear();
|
|
773
790
|
this.patternCache.clear();
|
|
774
791
|
this.similarityCache.clear();
|
|
775
792
|
|
|
776
793
|
// NEW: Clear request cache during critical cleanup
|
|
777
|
-
if (this.
|
|
794
|
+
if (this._enableRequest) this.clearRequestCache();
|
|
778
795
|
|
|
779
796
|
// For very high concurrency, also trim domain cache
|
|
780
797
|
if (this.options.concurrency > 15) {
|
|
781
798
|
const currentSize = this.domainCache.size;
|
|
782
799
|
this.domainCache.clear();
|
|
783
|
-
if (this.
|
|
800
|
+
if (this._debug) {
|
|
784
801
|
console.log(formatLogMessage('debug', `[SmartCache] Cleared ${currentSize} domain cache entries`));
|
|
785
802
|
}
|
|
786
803
|
}
|
|
@@ -789,7 +806,7 @@ class SmartCache {
|
|
|
789
806
|
this.responseCache.clear();
|
|
790
807
|
|
|
791
808
|
// NEW: Clear request cache during warning cleanup if it's large
|
|
792
|
-
if (this.
|
|
809
|
+
if (this._enableRequest && this.requestCache.size > this.options.requestCacheMaxSize * 0.8) {
|
|
793
810
|
this.clearRequestCache();
|
|
794
811
|
}
|
|
795
812
|
}
|
|
@@ -805,24 +822,41 @@ class SmartCache {
|
|
|
805
822
|
* @returns {Object} Statistics object
|
|
806
823
|
*/
|
|
807
824
|
getStats() {
|
|
808
|
-
const
|
|
809
|
-
const
|
|
810
|
-
const
|
|
811
|
-
|
|
812
|
-
const responseHitRate =
|
|
813
|
-
|
|
814
|
-
const
|
|
815
|
-
(this.stats.netToolsHits + this.stats.netToolsMisses) || 0;
|
|
816
|
-
|
|
817
|
-
// NEW: Request cache hit rate
|
|
818
|
-
const requestHitRate = this.stats.requestCacheHits /
|
|
819
|
-
(this.stats.requestCacheHits + this.stats.requestCacheMisses) || 0;
|
|
825
|
+
const s = this.stats;
|
|
826
|
+
const runtime = Date.now() - s.startTime;
|
|
827
|
+
const hitRate = s.hits / (s.hits + s.misses) || 0;
|
|
828
|
+
const patternHitRate = s.patternHits / (s.patternHits + s.patternMisses) || 0;
|
|
829
|
+
const responseHitRate = s.responseHits / (s.responseHits + s.responseMisses) || 0;
|
|
830
|
+
const netToolsHitRate = s.netToolsHits / (s.netToolsHits + s.netToolsMisses) || 0;
|
|
831
|
+
const requestHitRate = s.requestCacheHits / (s.requestCacheHits + s.requestCacheMisses) || 0;
|
|
820
832
|
|
|
821
|
-
|
|
833
|
+
// Use cached heap value from periodic _checkMemoryPressure (avoids syscall)
|
|
834
|
+
const heapUsed = this._lastHeapUsed;
|
|
835
|
+
const maxHeap = this.options.maxHeapUsage;
|
|
822
836
|
|
|
823
837
|
return {
|
|
824
|
-
|
|
825
|
-
|
|
838
|
+
hits: s.hits,
|
|
839
|
+
misses: s.misses,
|
|
840
|
+
patternHits: s.patternHits,
|
|
841
|
+
patternMisses: s.patternMisses,
|
|
842
|
+
responseHits: s.responseHits,
|
|
843
|
+
responseMisses: s.responseMisses,
|
|
844
|
+
netToolsHits: s.netToolsHits,
|
|
845
|
+
netToolsMisses: s.netToolsMisses,
|
|
846
|
+
similarityHits: s.similarityHits,
|
|
847
|
+
similarityMisses: s.similarityMisses,
|
|
848
|
+
regexCompilations: s.regexCompilations,
|
|
849
|
+
regexCacheHits: s.regexCacheHits,
|
|
850
|
+
persistenceLoads: s.persistenceLoads,
|
|
851
|
+
persistenceSaves: s.persistenceSaves,
|
|
852
|
+
memoryPressureEvents: s.memoryPressureEvents,
|
|
853
|
+
memoryWarnings: s.memoryWarnings,
|
|
854
|
+
responseCacheSkips: s.responseCacheSkips,
|
|
855
|
+
requestCacheHits: s.requestCacheHits,
|
|
856
|
+
requestCacheMisses: s.requestCacheMisses,
|
|
857
|
+
requestCacheSkips: s.requestCacheSkips,
|
|
858
|
+
requestCacheClears: s.requestCacheClears,
|
|
859
|
+
runtime: Math.round(runtime / 1000),
|
|
826
860
|
hitRate: (hitRate * 100).toFixed(2) + '%',
|
|
827
861
|
patternHitRate: (patternHitRate * 100).toFixed(2) + '%',
|
|
828
862
|
responseHitRate: (responseHitRate * 100).toFixed(2) + '%',
|
|
@@ -833,19 +867,18 @@ class SmartCache {
|
|
|
833
867
|
netToolsCacheSize: this.netToolsCache.size,
|
|
834
868
|
similarityCacheSize: this.similarityCache.size,
|
|
835
869
|
regexCacheSize: this.regexCache.size,
|
|
836
|
-
|
|
837
|
-
requestHitRate: this.options.enableRequestCache ?
|
|
870
|
+
requestHitRate: this._enableRequest ?
|
|
838
871
|
(requestHitRate * 100).toFixed(2) + '%' : '0% (disabled)',
|
|
839
|
-
requestCacheSize: this.
|
|
840
|
-
requestCacheMemoryMB: this.
|
|
841
|
-
Math.round((this.requestCache.calculatedSize || 0) /
|
|
872
|
+
requestCacheSize: this._enableRequest ? this.requestCache.size : 0,
|
|
873
|
+
requestCacheMemoryMB: this._enableRequest ?
|
|
874
|
+
Math.round((this.requestCache.calculatedSize || 0) / 1048576) : 0,
|
|
842
875
|
totalCacheEntries: this.domainCache.size + this.patternCache.size +
|
|
843
876
|
this.responseCache.size + this.netToolsCache.size +
|
|
844
|
-
this.similarityCache.size + this.regexCache.size + (this.
|
|
845
|
-
memoryUsageMB: Math.round(
|
|
846
|
-
memoryMaxMB: Math.round(
|
|
847
|
-
memoryUsagePercent: ((
|
|
848
|
-
responseCacheMemoryMB: Math.round((this.responseCache.calculatedSize || 0) /
|
|
877
|
+
this.similarityCache.size + this.regexCache.size + (this._enableRequest ? this.requestCache.size : 0),
|
|
878
|
+
memoryUsageMB: Math.round(heapUsed / 1048576),
|
|
879
|
+
memoryMaxMB: Math.round(maxHeap / 1048576),
|
|
880
|
+
memoryUsagePercent: ((heapUsed / maxHeap) * 100).toFixed(1) + '%',
|
|
881
|
+
responseCacheMemoryMB: Math.round((this.responseCache.calculatedSize || 0) / 1048576)
|
|
849
882
|
};
|
|
850
883
|
}
|
|
851
884
|
|
|
@@ -859,12 +892,12 @@ class SmartCache {
|
|
|
859
892
|
this.netToolsCache.clear();
|
|
860
893
|
this.similarityCache.clear();
|
|
861
894
|
this.regexCache.clear();
|
|
862
|
-
if (this.
|
|
895
|
+
if (this._enableRequest && this.requestCache) {
|
|
863
896
|
this.requestCache.clear();
|
|
864
897
|
}
|
|
865
898
|
this._initializeStats();
|
|
866
899
|
|
|
867
|
-
if (this.
|
|
900
|
+
if (this._debug) {
|
|
868
901
|
console.log(formatLogMessage('debug', '[SmartCache] All caches cleared'));
|
|
869
902
|
}
|
|
870
903
|
}
|
|
@@ -874,7 +907,7 @@ class SmartCache {
|
|
|
874
907
|
* @private
|
|
875
908
|
*/
|
|
876
909
|
_logMemorySkip(operation) {
|
|
877
|
-
if (this.
|
|
910
|
+
if (this._debug) {
|
|
878
911
|
console.log(formatLogMessage('debug',
|
|
879
912
|
`[SmartCache] Skipping ${operation} due to memory pressure`
|
|
880
913
|
));
|
|
@@ -888,17 +921,21 @@ class SmartCache {
|
|
|
888
921
|
_loadPersistentCache() {
|
|
889
922
|
const cacheFile = path.join(this.options.persistencePath, 'smart-cache.json');
|
|
890
923
|
|
|
891
|
-
|
|
924
|
+
let raw;
|
|
925
|
+
try {
|
|
926
|
+
raw = fs.readFileSync(cacheFile, 'utf8');
|
|
927
|
+
} catch (readErr) {
|
|
928
|
+
// File doesn't exist or unreadable -- nothing to load
|
|
892
929
|
return;
|
|
893
930
|
}
|
|
894
931
|
|
|
895
932
|
try {
|
|
896
|
-
const data = JSON.parse(
|
|
933
|
+
const data = JSON.parse(raw);
|
|
897
934
|
const now = Date.now();
|
|
898
935
|
|
|
899
936
|
// Validate cache age
|
|
900
937
|
if (data.timestamp && now - data.timestamp > 24 * 60 * 60 * 1000) {
|
|
901
|
-
if (this.
|
|
938
|
+
if (this._debug) {
|
|
902
939
|
console.log(formatLogMessage('debug',
|
|
903
940
|
'[SmartCache] Persistent cache too old, ignoring'
|
|
904
941
|
));
|
|
@@ -908,30 +945,31 @@ class SmartCache {
|
|
|
908
945
|
|
|
909
946
|
// Load domain cache
|
|
910
947
|
if (data.domainCache && Array.isArray(data.domainCache)) {
|
|
911
|
-
data.domainCache
|
|
912
|
-
|
|
913
|
-
if (now -
|
|
914
|
-
this.domainCache.set(
|
|
948
|
+
const entries = data.domainCache;
|
|
949
|
+
for (let i = 0; i < entries.length; i++) {
|
|
950
|
+
if (now - entries[i][1].timestamp < this.options.ttl) {
|
|
951
|
+
this.domainCache.set(entries[i][0], entries[i][1]);
|
|
915
952
|
}
|
|
916
|
-
}
|
|
953
|
+
}
|
|
917
954
|
}
|
|
918
955
|
|
|
919
956
|
// Load nettools cache
|
|
920
957
|
if (data.netToolsCache && Array.isArray(data.netToolsCache)) {
|
|
921
|
-
data.netToolsCache
|
|
922
|
-
|
|
923
|
-
|
|
958
|
+
const entries = data.netToolsCache;
|
|
959
|
+
for (let i = 0; i < entries.length; i++) {
|
|
960
|
+
this.netToolsCache.set(entries[i][0], entries[i][1]);
|
|
961
|
+
}
|
|
924
962
|
}
|
|
925
963
|
|
|
926
964
|
this.stats.persistenceLoads++;
|
|
927
965
|
|
|
928
|
-
if (this.
|
|
966
|
+
if (this._debug) {
|
|
929
967
|
console.log(formatLogMessage('debug',
|
|
930
968
|
`[SmartCache] Loaded persistent cache: ${this.domainCache.size} domains, ${this.netToolsCache.size} nettools`
|
|
931
969
|
));
|
|
932
970
|
}
|
|
933
971
|
} catch (err) {
|
|
934
|
-
if (this.
|
|
972
|
+
if (this._debug) {
|
|
935
973
|
console.log(formatLogMessage('debug',
|
|
936
974
|
`[SmartCache] Failed to load persistent cache: ${err.message}`
|
|
937
975
|
));
|
|
@@ -948,7 +986,7 @@ class SmartCache {
|
|
|
948
986
|
// Prevent concurrent saves
|
|
949
987
|
if (this.saveInProgress) {
|
|
950
988
|
this.pendingSave = true;
|
|
951
|
-
if (this.
|
|
989
|
+
if (this._debug) {
|
|
952
990
|
console.log(formatLogMessage('debug', '[SmartCache] Save in progress, marking pending...'));
|
|
953
991
|
}
|
|
954
992
|
return;
|
|
@@ -975,12 +1013,11 @@ class SmartCache {
|
|
|
975
1013
|
|
|
976
1014
|
const cacheDir = this.options.persistencePath;
|
|
977
1015
|
const cacheFile = path.join(cacheDir, 'smart-cache.json');
|
|
1016
|
+
const tmpFile = cacheFile + '.tmp';
|
|
978
1017
|
|
|
979
1018
|
try {
|
|
980
|
-
//
|
|
981
|
-
|
|
982
|
-
fs.mkdirSync(cacheDir, { recursive: true });
|
|
983
|
-
}
|
|
1019
|
+
// recursive:true is a no-op if dir exists -- no need for existsSync check
|
|
1020
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
984
1021
|
|
|
985
1022
|
const data = {
|
|
986
1023
|
timestamp: now,
|
|
@@ -989,21 +1026,52 @@ class SmartCache {
|
|
|
989
1026
|
stats: this.stats
|
|
990
1027
|
};
|
|
991
1028
|
|
|
992
|
-
|
|
993
|
-
|
|
1029
|
+
// Async write to temp file, then atomic rename (no pretty-print -- saves ~30% serialization time)
|
|
1030
|
+
const jsonStr = JSON.stringify(data);
|
|
1031
|
+
fs.writeFile(tmpFile, jsonStr, (writeErr) => {
|
|
1032
|
+
if (writeErr) {
|
|
1033
|
+
if (this._debug) {
|
|
1034
|
+
console.log(formatLogMessage('debug',
|
|
1035
|
+
`[SmartCache] Failed to write cache temp file: ${writeErr.message}`
|
|
1036
|
+
));
|
|
1037
|
+
}
|
|
1038
|
+
this.saveInProgress = false;
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
fs.rename(tmpFile, cacheFile, (renameErr) => {
|
|
1043
|
+
if (renameErr) {
|
|
1044
|
+
if (this._debug) {
|
|
1045
|
+
console.log(formatLogMessage('debug',
|
|
1046
|
+
`[SmartCache] Failed to rename cache file: ${renameErr.message}`
|
|
1047
|
+
));
|
|
1048
|
+
}
|
|
1049
|
+
} else {
|
|
1050
|
+
this.stats.persistenceSaves++;
|
|
1051
|
+
if (this._debug) {
|
|
1052
|
+
console.log(formatLogMessage('debug',
|
|
1053
|
+
`[SmartCache] Saved cache to disk: ${cacheFile}`
|
|
1054
|
+
));
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
this.saveInProgress = false;
|
|
1059
|
+
|
|
1060
|
+
// Process any pending saves
|
|
1061
|
+
if (this.pendingSave && !this.saveTimeout) {
|
|
1062
|
+
this.pendingSave = false;
|
|
1063
|
+
setTimeout(() => this.savePersistentCache(), 1000);
|
|
1064
|
+
}
|
|
1065
|
+
});
|
|
1066
|
+
});
|
|
1067
|
+
return; // Async -- don't fall through to finally
|
|
994
1068
|
|
|
995
|
-
if (this.options.forceDebug) {
|
|
996
|
-
console.log(formatLogMessage('debug',
|
|
997
|
-
`[SmartCache] Saved cache to disk: ${cacheFile}`
|
|
998
|
-
));
|
|
999
|
-
}
|
|
1000
1069
|
} catch (err) {
|
|
1001
|
-
if (this.
|
|
1070
|
+
if (this._debug) {
|
|
1002
1071
|
console.log(formatLogMessage('debug',
|
|
1003
1072
|
`[SmartCache] Failed to save cache: ${err.message}`
|
|
1004
1073
|
));
|
|
1005
1074
|
}
|
|
1006
|
-
} finally {
|
|
1007
1075
|
this.saveInProgress = false;
|
|
1008
1076
|
|
|
1009
1077
|
// Process any pending saves
|
|
@@ -1054,15 +1122,9 @@ class SmartCache {
|
|
|
1054
1122
|
* @param {boolean} options.forceDebug - Enable debug logging
|
|
1055
1123
|
* @returns {Object} Clear operation results
|
|
1056
1124
|
*/
|
|
1057
|
-
static clearPersistentCache(options =
|
|
1125
|
+
static clearPersistentCache(options = EMPTY) {
|
|
1058
1126
|
const { silent = false, forceDebug = false, cachePath = '.cache' } = options;
|
|
1059
1127
|
|
|
1060
|
-
const cachePaths = [
|
|
1061
|
-
cachePath,
|
|
1062
|
-
path.join(cachePath, 'smart-cache.json'),
|
|
1063
|
-
// Add other potential cache files here if needed
|
|
1064
|
-
];
|
|
1065
|
-
|
|
1066
1128
|
let clearedItems = 0;
|
|
1067
1129
|
let totalSize = 0;
|
|
1068
1130
|
const clearedFiles = [];
|
|
@@ -1072,40 +1134,58 @@ class SmartCache {
|
|
|
1072
1134
|
console.log(`\n??? Clearing cache...`);
|
|
1073
1135
|
}
|
|
1074
1136
|
|
|
1075
|
-
|
|
1076
|
-
|
|
1137
|
+
// Try the directory first -- rmSync recursive handles all files inside
|
|
1138
|
+
const cacheFile = path.join(cachePath, 'smart-cache.json');
|
|
1139
|
+
let dirHandled = false;
|
|
1140
|
+
|
|
1141
|
+
try {
|
|
1142
|
+
const stats = fs.statSync(cachePath);
|
|
1143
|
+
if (stats.isDirectory()) {
|
|
1144
|
+
// Sum file sizes (readdirSync proves they exist -- no existsSync needed)
|
|
1077
1145
|
try {
|
|
1078
|
-
const
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
const filePath = path.join(currentCachePath, file);
|
|
1084
|
-
if (fs.existsSync(filePath)) {
|
|
1085
|
-
totalSize += fs.statSync(filePath).size;
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
fs.rmSync(currentCachePath, { recursive: true, force: true });
|
|
1089
|
-
clearedItems++;
|
|
1090
|
-
clearedFiles.push({ type: 'directory', path: currentCachePath, size: totalSize });
|
|
1091
|
-
if (forceDebug) {
|
|
1092
|
-
console.log(formatLogMessage('debug', `Cleared cache directory: ${currentCachePath}`));
|
|
1093
|
-
}
|
|
1094
|
-
} else {
|
|
1095
|
-
totalSize += stats.size;
|
|
1096
|
-
fs.unlinkSync(currentCachePath);
|
|
1097
|
-
clearedItems++;
|
|
1098
|
-
clearedFiles.push({ type: 'file', path: currentCachePath, size: stats.size });
|
|
1099
|
-
if (forceDebug) {
|
|
1100
|
-
console.log(formatLogMessage('debug', `Cleared cache file: ${currentCachePath}`));
|
|
1101
|
-
}
|
|
1102
|
-
}
|
|
1103
|
-
} catch (clearErr) {
|
|
1104
|
-
errors.push({ path: currentCachePath, error: clearErr.message });
|
|
1105
|
-
if (forceDebug) {
|
|
1106
|
-
console.log(formatLogMessage('debug', `Failed to clear ${currentCachePath}: ${clearErr.message}`));
|
|
1146
|
+
const files = fs.readdirSync(cachePath);
|
|
1147
|
+
for (const file of files) {
|
|
1148
|
+
try {
|
|
1149
|
+
totalSize += fs.statSync(path.join(cachePath, file)).size;
|
|
1150
|
+
} catch (e) { /* file disappeared between readdir and stat */ }
|
|
1107
1151
|
}
|
|
1152
|
+
} catch (e) { /* empty dir or read error */ }
|
|
1153
|
+
fs.rmSync(cachePath, { recursive: true, force: true });
|
|
1154
|
+
dirHandled = true;
|
|
1155
|
+
clearedItems++;
|
|
1156
|
+
clearedFiles.push({ type: 'directory', path: cachePath, size: totalSize });
|
|
1157
|
+
if (forceDebug) {
|
|
1158
|
+
console.log(formatLogMessage('debug', `Cleared cache directory: ${cachePath}`));
|
|
1108
1159
|
}
|
|
1160
|
+
} else {
|
|
1161
|
+
totalSize += stats.size;
|
|
1162
|
+
fs.unlinkSync(cachePath);
|
|
1163
|
+
dirHandled = true;
|
|
1164
|
+
clearedItems++;
|
|
1165
|
+
clearedFiles.push({ type: 'file', path: cachePath, size: stats.size });
|
|
1166
|
+
if (forceDebug) {
|
|
1167
|
+
console.log(formatLogMessage('debug', `Cleared cache file: ${cachePath}`));
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
} catch (clearErr) {
|
|
1171
|
+
if (clearErr.code !== 'ENOENT') {
|
|
1172
|
+
errors.push({ path: cachePath, error: clearErr.message });
|
|
1173
|
+
if (forceDebug) {
|
|
1174
|
+
console.log(formatLogMessage('debug', `Failed to clear ${cachePath}: ${clearErr.message}`));
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// Fallback: if directory was missing or failed, try the json file directly (orphaned file safety net)
|
|
1180
|
+
if (!dirHandled) {
|
|
1181
|
+
try {
|
|
1182
|
+
const fileStats = fs.statSync(cacheFile);
|
|
1183
|
+
totalSize += fileStats.size;
|
|
1184
|
+
fs.unlinkSync(cacheFile);
|
|
1185
|
+
clearedItems++;
|
|
1186
|
+
clearedFiles.push({ type: 'file', path: cacheFile, size: fileStats.size });
|
|
1187
|
+
} catch (e) {
|
|
1188
|
+
// ENOENT = nothing to clean, any other error is ignorable
|
|
1109
1189
|
}
|
|
1110
1190
|
}
|
|
1111
1191
|
|