@fanboynz/network-scanner 2.0.56 → 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.
@@ -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
- // Initialize cache layers
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.options.enableRequestCache) {
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.options.concurrency > 10 ? 500 : 1000;
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.options.concurrency > 10 ? 50 : 200;
121
- const responseCacheMemory = this.options.concurrency > 10 ? 20 * 1024 * 1024 : 50 * 1024 * 1024;
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.options.enableRequestCache) {
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 = 0;
137
- if (value.headers) size += JSON.stringify(value.headers).length;
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.options.forceDebug) {
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.options.forceDebug) {
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.options.concurrency > 10 ? 1000 : 2000;
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 Map();
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 (this.domainCache.has(cacheKey)) {
243
+ if (cached !== undefined) {
217
244
  this.stats.hits++;
218
- if (this.options.forceDebug) {
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, context: ${JSON.stringify(context)})`
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 = {}, metadata = {}) {
264
+ markDomainProcessed(domain, context = EMPTY, metadata = EMPTY) {
239
265
  const cacheKey = this._generateCacheKey(domain, context);
240
- this.domainCache.set(cacheKey, {
241
- timestamp: Date.now(),
242
- metadata,
243
- context,
244
- domain
245
- });
246
-
247
- if (this.options.forceDebug) {
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 (context: ${JSON.stringify(context)})`
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
- try {
262
- const parsedUrl = new URL(url);
263
- // Remove fragment but preserve query params and path
264
- parsedUrl.hash = '';
265
- // Normalize trailing slashes: /folder/ becomes /folder
266
- let pathname = parsedUrl.pathname;
267
- if (pathname.length > 1 && pathname.endsWith('/')) {
268
- pathname = pathname.slice(0, -1);
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
- const components = [
288
- domain,
289
- filterRegex || '',
290
- searchString || '',
291
- resourceType || '',
292
- nettools ? 'nt' : ''
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
- try {
314
- const regex = new RegExp(pattern);
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 normalizedUrl = this._normalizeUrl(url);
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
- if (headers[header]) {
340
- relevantHeaders[header] = headers[header];
341
- }
358
+ const val = headers[header];
359
+ if (val) headerKey += header + '=' + val + '&';
342
360
  }
343
361
 
344
- const cacheKey = [method, normalizedUrl, JSON.stringify(relevantHeaders)].join('|');
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.options.enableRequestCache) return null;
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.options.forceDebug) {
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
- return {
373
- ...cached,
374
- fromCache: true,
375
- cacheAge: Date.now() - cached.timestamp
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 = {}, result = {}) {
390
- if (!this.options.enableRequestCache) return;
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
- const memUsage = process.memoryUsage();
404
- const threshold = this.options.concurrency > 10 ? 0.75 : 0.85;
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
- const cacheKey = this._generateRequestCacheKey(url, options);
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 cacheValue = {
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, // Store normalized URL
425
- originalUrl: url, // Keep original for reference
426
- requestOptions: {
427
- method: options.method || 'GET',
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 (shouldCache) {
438
- this.requestCache.set(cacheKey, cacheValue);
439
-
440
- if (this.options.forceDebug) {
441
- const bodySize = result.body ? result.body.length : 0;
442
- console.log(formatLogMessage('debug',
443
- `[SmartCache] Cached request: ${normalizedUrl.substring(0, 50)}... (${result.status || 'unknown'}, ${Math.round(bodySize / 1024)}KB)`
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.options.enableRequestCache || !this.requestCache) return;
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.options.forceDebug) {
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.options.enableRequestCache || !this.requestCache) {
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.options.enableRequestCache || !this.requestCache) {
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 in cache
511
- const pathDistribution = new Map();
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
- try {
514
- const url = new URL(value.url || value.originalUrl);
515
- const pathKey = `${url.hostname}${url.pathname}`;
516
- pathDistribution.set(pathKey, (pathDistribution.get(pathKey) || 0) + 1);
517
- } catch (urlErr) {
518
- // Skip invalid URLs
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
- return { ...basicStats, pathDistribution: Object.fromEntries(pathDistribution) };
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
- if (!this.regexCache.has(pattern)) {
532
- this.stats.regexCompilations++;
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
- return this.regexCache.get(pattern);
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.options.enablePatternCache) return null;
575
+ if (!this._enablePattern) return null;
559
576
 
560
- const cacheKey = `${url}:${pattern}`;
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.options.forceDebug) {
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.options.enablePatternCache) return;
601
+ if (!this._enablePattern) return;
585
602
 
586
- const cacheKey = `${url}:${pattern}`;
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.options.enableResponseCache) return null;
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.options.forceDebug) {
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.options.enableResponseCache) return;
640
+ if (!this._enableResponse) return;
624
641
 
625
642
  // Skip response caching entirely for very high concurrency
626
- if (this.options.concurrency > 12) {
643
+ if (this._highConcurrency) {
627
644
  this.stats.responseCacheSkips++;
628
645
  return;
629
646
  }
630
647
 
631
- // Check memory before caching large content
632
- const memUsage = process.memoryUsage();
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.options.enableWhoisCache) return null;
669
+ if (!this._enableWhois) return null;
655
670
 
656
- const cacheKey = `${tool}:${domain}${recordType ? ':' + recordType : ''}`;
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.options.forceDebug) {
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.options.enableWhoisCache) return;
696
+ if (!this._enableWhois) return;
682
697
 
683
- const cacheKey = `${tool}:${domain}${recordType ? ':' + recordType : ''}`;
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
- const key = [domain1, domain2].sort().join('|');
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 = [domain1, domain2].sort().join('|');
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
- const heapUsedMB = Math.round(memUsage.heapUsed / 1024 / 1024);
724
- const maxHeapMB = Math.round(this.options.maxHeapUsage / 1024 / 1024);
725
- const usagePercent = (memUsage.heapUsed / this.options.maxHeapUsage) * 100;
726
-
727
- // Adjust thresholds based on concurrency
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 > this.options.maxHeapUsage * criticalThreshold) {
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 > this.options.maxHeapUsage * warningThreshold) {
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 > this.options.maxHeapUsage * infoThreshold) {
762
+ if (memUsage.heapUsed > maxHeap * this._infoThreshold) {
746
763
  this.stats.memoryWarnings++;
747
- if (this.options.forceDebug) {
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.options.forceDebug) {
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.options.concurrency > 12) {
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.options.enableRequestCache) this.clearRequestCache();
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.options.forceDebug) {
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.options.enableRequestCache && this.requestCache.size > this.options.requestCacheMaxSize * 0.8) {
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 runtime = Date.now() - this.stats.startTime;
809
- const hitRate = this.stats.hits / (this.stats.hits + this.stats.misses) || 0;
810
- const patternHitRate = this.stats.patternHits /
811
- (this.stats.patternHits + this.stats.patternMisses) || 0;
812
- const responseHitRate = this.stats.responseHits /
813
- (this.stats.responseHits + this.stats.responseMisses) || 0;
814
- const netToolsHitRate = this.stats.netToolsHits /
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
- const memUsage = process.memoryUsage();
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
- ...this.stats,
825
- runtime: Math.round(runtime / 1000), // seconds
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
- // NEW: Request cache statistics
837
- requestHitRate: this.options.enableRequestCache ?
870
+ requestHitRate: this._enableRequest ?
838
871
  (requestHitRate * 100).toFixed(2) + '%' : '0% (disabled)',
839
- requestCacheSize: this.options.enableRequestCache ? this.requestCache.size : 0,
840
- requestCacheMemoryMB: this.options.enableRequestCache ?
841
- Math.round((this.requestCache.calculatedSize || 0) / 1024 / 1024) : 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.options.enableRequestCache ? this.requestCache.size : 0),
845
- memoryUsageMB: Math.round(memUsage.heapUsed / 1024 / 1024),
846
- memoryMaxMB: Math.round(this.options.maxHeapUsage / 1024 / 1024),
847
- memoryUsagePercent: ((memUsage.heapUsed / this.options.maxHeapUsage) * 100).toFixed(1) + '%',
848
- responseCacheMemoryMB: Math.round((this.responseCache.calculatedSize || 0) / 1024 / 1024)
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.options.enableRequestCache && this.requestCache) {
895
+ if (this._enableRequest && this.requestCache) {
863
896
  this.requestCache.clear();
864
897
  }
865
898
  this._initializeStats();
866
899
 
867
- if (this.options.forceDebug) {
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.options.forceDebug) {
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
- if (!fs.existsSync(cacheFile)) {
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(fs.readFileSync(cacheFile, 'utf8'));
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.options.forceDebug) {
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.forEach(([key, value]) => {
912
- // Only load if not expired
913
- if (now - value.timestamp < this.options.ttl) {
914
- this.domainCache.set(key, value);
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.forEach(([key, value]) => {
922
- this.netToolsCache.set(key, value);
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.options.forceDebug) {
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.options.forceDebug) {
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.options.forceDebug) {
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
- // Create cache directory if it doesn't exist
981
- if (!fs.existsSync(cacheDir)) {
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
- fs.writeFileSync(cacheFile, JSON.stringify(data, null, 2));
993
- this.stats.persistenceSaves++;
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.options.forceDebug) {
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
- for (const currentCachePath of cachePaths) {
1076
- if (fs.existsSync(currentCachePath)) {
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 stats = fs.statSync(currentCachePath);
1079
- if (stats.isDirectory()) {
1080
- // Calculate total size of directory contents
1081
- const files = fs.readdirSync(currentCachePath);
1082
- for (const file of files) {
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