@fanboynz/network-scanner 2.0.31 → 2.0.32

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.
@@ -10,18 +10,24 @@ const { formatLogMessage } = require('./colorize');
10
10
  */
11
11
  class DomainCache {
12
12
  constructor(options = {}) {
13
+ // V8 Optimization: Initialize all properties in constructor for stable hidden class
13
14
  this.cache = new Set();
15
+
16
+ // V8 Optimization: Use consistent object shape (no dynamic property addition)
14
17
  this.stats = {
15
18
  totalDetected: 0,
16
19
  totalSkipped: 0,
17
20
  cacheHits: 0,
18
21
  cacheMisses: 0
19
22
  };
20
- this.options = {
21
- enableLogging: options.enableLogging || false,
22
- logPrefix: options.logPrefix || '[domain-cache]',
23
- maxCacheSize: options.maxCacheSize || 10000 // Prevent memory leaks
24
- };
23
+
24
+ // V8 Optimization: Store options directly instead of nested object for faster property access
25
+ this.enableLogging = options.enableLogging || false;
26
+ this.logPrefix = options.logPrefix || '[domain-cache]';
27
+ this.maxCacheSize = options.maxCacheSize || 10000; // Prevent memory leaks
28
+
29
+ // V8 Optimization: Pre-calculate 90% target to avoid repeated Math.floor
30
+ this.targetCacheSize = Math.floor(this.maxCacheSize * 0.9);
25
31
  }
26
32
 
27
33
  /**
@@ -40,14 +46,14 @@ class DomainCache {
40
46
  this.stats.totalSkipped++;
41
47
  this.stats.cacheHits++;
42
48
 
43
- if (this.options.enableLogging) {
44
- console.log(formatLogMessage('debug', `${this.options.logPrefix} Cache HIT: ${domain} (skipped)`));
49
+ if (this.enableLogging) {
50
+ console.log(formatLogMessage('debug', `${this.logPrefix} Cache HIT: ${domain} (skipped)`));
45
51
  }
46
52
  } else {
47
53
  this.stats.cacheMisses++;
48
54
 
49
- if (this.options.enableLogging) {
50
- console.log(formatLogMessage('debug', `${this.options.logPrefix} Cache MISS: ${domain} (processing)`));
55
+ if (this.enableLogging) {
56
+ console.log(formatLogMessage('debug', `${this.logPrefix} Cache MISS: ${domain} (processing)`));
51
57
  }
52
58
  }
53
59
 
@@ -63,25 +69,77 @@ class DomainCache {
63
69
  return false;
64
70
  }
65
71
 
66
- // Prevent cache from growing too large
67
- if (this.cache.size >= this.options.maxCacheSize) {
68
- this.clearOldestEntries(Math.floor(this.options.maxCacheSize * 0.1)); // Remove 10% of entries
69
- }
70
-
71
72
  const wasNew = !this.cache.has(domain);
72
73
  this.cache.add(domain);
73
74
 
74
75
  if (wasNew) {
75
76
  this.stats.totalDetected++;
76
77
 
77
- if (this.options.enableLogging) {
78
- console.log(formatLogMessage('debug', `${this.options.logPrefix} Marked as detected: ${domain} (cache size: ${this.cache.size})`));
78
+ if (this.enableLogging) {
79
+ console.log(formatLogMessage('debug', `${this.logPrefix} Marked as detected: ${domain} (cache size: ${this.cache.size})`));
80
+ }
81
+ }
82
+
83
+ // Check size AFTER adding to prevent race where multiple threads see same size
84
+ // and all trigger cleanup before any adds complete
85
+ // V8 Optimization: Use pre-calculated targetCacheSize
86
+ if (this.cache.size > this.maxCacheSize) {
87
+ const toRemove = this.cache.size - this.targetCacheSize;
88
+ if (toRemove > 0) {
89
+ this.clearOldestEntries(toRemove);
79
90
  }
80
91
  }
81
92
 
82
93
  return wasNew;
83
94
  }
84
95
 
96
+ /**
97
+ * Atomically check if domain was detected and mark it if new (race-condition free)
98
+ * This method combines isDomainAlreadyDetected + markDomainAsDetected in one atomic operation
99
+ * @param {string} domain - Domain to check and potentially mark
100
+ * @returns {boolean} True if domain was ALREADY detected (should skip), false if NEW (should process)
101
+ */
102
+ checkAndMark(domain) {
103
+ if (!domain || typeof domain !== 'string') {
104
+ return false;
105
+ }
106
+
107
+ const wasAlreadyDetected = this.cache.has(domain);
108
+
109
+ if (wasAlreadyDetected) {
110
+ // Domain already exists - update skip stats and return true (should skip)
111
+ this.stats.totalSkipped++;
112
+ this.stats.cacheHits++;
113
+
114
+ if (this.enableLogging) {
115
+ console.log(formatLogMessage('debug', `${this.logPrefix} Cache HIT: ${domain} (skipped)`));
116
+ }
117
+ return true; // Already detected, should skip
118
+ }
119
+
120
+ // Domain is NEW - mark it as detected
121
+ this.stats.cacheMisses++;
122
+
123
+ this.cache.add(domain);
124
+ this.stats.totalDetected++;
125
+
126
+ if (this.enableLogging) {
127
+ console.log(formatLogMessage('debug', `${this.logPrefix} Cache MISS: ${domain} (processing and marked, cache size: ${this.cache.size})`));
128
+ }
129
+
130
+ // Check size AFTER adding to prevent race where multiple threads see same size
131
+ // and all trigger cleanup before any adds complete
132
+ // V8 Optimization: Use pre-calculated targetCacheSize
133
+ if (this.cache.size > this.maxCacheSize) {
134
+ const toRemove = this.cache.size - this.targetCacheSize;
135
+ if (toRemove > 0) {
136
+ this.clearOldestEntries(toRemove);
137
+ }
138
+ }
139
+
140
+ return false; // New domain, should process
141
+ }
142
+
85
143
  /**
86
144
  * Clear oldest entries from cache (basic LRU simulation)
87
145
  * Note: Set doesn't maintain insertion order in all Node.js versions,
@@ -96,8 +154,8 @@ class DomainCache {
96
154
 
97
155
  toRemove.forEach(domain => this.cache.delete(domain));
98
156
 
99
- if (this.options.enableLogging) {
100
- console.log(formatLogMessage('debug', `${this.options.logPrefix} Cleared ${toRemove.length} old entries, cache size now: ${this.cache.size}`));
157
+ if (this.enableLogging) {
158
+ console.log(formatLogMessage('debug', `${this.logPrefix} Cleared ${toRemove.length} old entries, cache size now: ${this.cache.size}`));
101
159
  }
102
160
  }
103
161
 
@@ -128,8 +186,8 @@ class DomainCache {
128
186
  cacheMisses: 0
129
187
  };
130
188
 
131
- if (this.options.enableLogging) {
132
- console.log(formatLogMessage('debug', `${this.options.logPrefix} Cache cleared (${previousSize} entries removed)`));
189
+ if (this.enableLogging) {
190
+ console.log(formatLogMessage('debug', `${this.logPrefix} Cache cleared (${previousSize} entries removed)`));
133
191
  }
134
192
  }
135
193
 
@@ -158,8 +216,8 @@ class DomainCache {
158
216
  removeDomain(domain) {
159
217
  const wasRemoved = this.cache.delete(domain);
160
218
 
161
- if (wasRemoved && this.options.enableLogging) {
162
- console.log(formatLogMessage('debug', `${this.options.logPrefix} Removed from cache: ${domain}`));
219
+ if (wasRemoved && this.enableLogging) {
220
+ console.log(formatLogMessage('debug', `${this.logPrefix} Removed from cache: ${domain}`));
163
221
  }
164
222
 
165
223
  return wasRemoved;
@@ -193,6 +251,7 @@ class DomainCache {
193
251
  return {
194
252
  isDomainAlreadyDetected: this.isDomainAlreadyDetected.bind(this),
195
253
  markDomainAsDetected: this.markDomainAsDetected.bind(this),
254
+ checkAndMark: this.checkAndMark.bind(this),
196
255
  getSkippedCount: () => this.stats.totalSkipped,
197
256
  getCacheSize: () => this.cache.size,
198
257
  getStats: this.getStats.bind(this)
@@ -261,6 +320,16 @@ function markDomainAsDetected(domain) {
261
320
  cache.markDomainAsDetected(domain);
262
321
  }
263
322
 
323
+ /**
324
+ * Atomically check and mark a domain (race-condition free)
325
+ * @param {string} domain - Domain to check and mark
326
+ * @returns {boolean} True if already detected (skip), false if new (process)
327
+ */
328
+ function checkAndMark(domain) {
329
+ const cache = getGlobalDomainCache();
330
+ return cache.checkAndMark(domain);
331
+ }
332
+
264
333
  /**
265
334
  * Get total domains skipped (legacy wrapper)
266
335
  * @returns {number} Number of domains skipped
@@ -291,6 +360,7 @@ module.exports = {
291
360
  // Legacy wrapper functions for backward compatibility
292
361
  isDomainAlreadyDetected,
293
362
  markDomainAsDetected,
363
+ checkAndMark,
294
364
  getTotalDomainsSkipped,
295
365
  getDetectedDomainsCount
296
366
  };
package/lib/referrer.js CHANGED
@@ -1,6 +1,16 @@
1
1
  // === Referrer Header Generation Module ===
2
2
  // This module handles generation of referrer headers for different traffic simulation modes
3
3
 
4
+ /**
5
+ * Performance utility: Get random element from array
6
+ * Reduces code duplication and improves readability
7
+ * @param {Array} array - Array to select from
8
+ * @returns {*} Random element from array
9
+ */
10
+ function getRandomElement(array) {
11
+ return array[Math.floor(Math.random() * array.length)];
12
+ }
13
+
4
14
  /**
5
15
  * Referrer URL collections for different modes
6
16
  */
@@ -69,7 +79,7 @@ const REFERRER_COLLECTIONS = Object.freeze({
69
79
  */
70
80
  function generateSearchTerm(customTerms, context = null) {
71
81
  if (customTerms && customTerms.length > 0) {
72
- return customTerms[Math.floor(Math.random() * customTerms.length)];
82
+ return getRandomElement(customTerms);
73
83
  }
74
84
 
75
85
  // Use context-specific terms if available
@@ -80,7 +90,7 @@ function generateSearchTerm(customTerms, context = null) {
80
90
  termCollection = REFERRER_COLLECTIONS.TECH_TERMS;
81
91
  }
82
92
 
83
- return termCollection[Math.floor(Math.random() * termCollection.length)];
93
+ return getRandomElement(termCollection);
84
94
  }
85
95
 
86
96
  /**
@@ -91,9 +101,8 @@ function generateSearchTerm(customTerms, context = null) {
91
101
  * @returns {string} Generated search engine referrer URL
92
102
  */
93
103
  function generateSearchReferrer(searchTerms, context, forceDebug) {
94
- const randomEngine = REFERRER_COLLECTIONS.SEARCH_ENGINES[
95
- Math.floor(Math.random() * REFERRER_COLLECTIONS.SEARCH_ENGINES.length)
96
- ];
104
+ const randomEngine = getRandomElement(REFERRER_COLLECTIONS.SEARCH_ENGINES);
105
+
97
106
  const searchTerm = generateSearchTerm(searchTerms, context);
98
107
  const referrerUrl = randomEngine + encodeURIComponent(searchTerm);
99
108
 
@@ -110,9 +119,7 @@ function generateSearchReferrer(searchTerms, context, forceDebug) {
110
119
  * @returns {string} Generated social media referrer URL
111
120
  */
112
121
  function generateSocialMediaReferrer(forceDebug) {
113
- const randomSocial = REFERRER_COLLECTIONS.SOCIAL_MEDIA[
114
- Math.floor(Math.random() * REFERRER_COLLECTIONS.SOCIAL_MEDIA.length)
115
- ];
122
+ const randomSocial = getRandomElement(REFERRER_COLLECTIONS.SOCIAL_MEDIA);
116
123
 
117
124
  if (forceDebug) {
118
125
  console.log(`[debug] Generated social media referrer: ${randomSocial}`);
@@ -127,9 +134,7 @@ function generateSocialMediaReferrer(forceDebug) {
127
134
  * @returns {string} Generated news site referrer URL
128
135
  */
129
136
  function generateNewsReferrer(forceDebug) {
130
- const randomNews = REFERRER_COLLECTIONS.NEWS_SITES[
131
- Math.floor(Math.random() * REFERRER_COLLECTIONS.NEWS_SITES.length)
132
- ];
137
+ const randomNews = getRandomElement(REFERRER_COLLECTIONS.NEWS_SITES);
133
138
 
134
139
  if (forceDebug) {
135
140
  console.log(`[debug] Generated news referrer: ${randomNews}`);
@@ -155,33 +160,47 @@ function isValidUrl(url) {
155
160
  * @returns {boolean} True if referrer should be disabled for this URL
156
161
  */
157
162
  function shouldDisableReferrer(targetUrl, disableList, forceDebug = false) {
158
- if (!disableList || !Array.isArray(disableList) || disableList.length === 0) {
163
+ // Fast path: early return for empty/invalid inputs
164
+ if (!disableList?.length || !targetUrl || typeof targetUrl !== 'string') {
159
165
  return false;
160
166
  }
161
167
 
162
- if (!targetUrl || typeof targetUrl !== 'string') {
163
- return false;
168
+ // Parse target URL once (performance optimization)
169
+ let targetHostname = null;
170
+ let targetUrlParsed = false;
171
+
172
+ try {
173
+ targetHostname = new URL(targetUrl).hostname;
174
+ targetUrlParsed = true;
175
+ } catch (e) {
176
+ // Invalid URL - can only do string matching
177
+ targetUrlParsed = false;
164
178
  }
165
179
 
166
180
  for (const disablePattern of disableList) {
167
181
  if (typeof disablePattern !== 'string') continue;
168
182
 
169
- // Exact URL match
183
+ // Fast check: Exact URL match (no parsing needed)
170
184
  if (targetUrl === disablePattern) {
171
- if (forceDebug) console.log(`[debug] Referrer disabled for exact URL match: ${targetUrl}`);
185
+ if (forceDebug) console.log(`[debug] Referrer disabled for exact match: ${targetUrl}`);
172
186
  return true;
173
187
  }
174
188
 
175
- // Domain/hostname match
176
- try {
177
- const targetHostname = new URL(targetUrl).hostname;
178
- const disableHostname = new URL(disablePattern).hostname;
179
- if (targetHostname === disableHostname) {
180
- if (forceDebug) console.log(`[debug] Referrer disabled for domain match: ${targetHostname}`);
181
- return true;
189
+ // Domain/hostname match (use cached parsed URL)
190
+ if (targetUrlParsed) {
191
+ try {
192
+ const disableHostname = new URL(disablePattern).hostname;
193
+ if (targetHostname === disableHostname) {
194
+ if (forceDebug) console.log(`[debug] Referrer disabled for domain match: ${targetHostname}`);
195
+ return true;
196
+ }
197
+ } catch (e) {
198
+ // disablePattern is not a valid URL, try substring match below
182
199
  }
183
- } catch (e) {
184
- // If pattern is not a valid URL, try simple string matching
200
+ }
201
+
202
+ // Fallback: Simple substring match (for patterns like 'example.com')
203
+ if (!targetUrlParsed || disablePattern.includes('/') === false) {
185
204
  if (targetUrl.includes(disablePattern)) {
186
205
  if (forceDebug) console.log(`[debug] Referrer disabled for pattern match: ${disablePattern} in ${targetUrl}`);
187
206
  return true;
@@ -218,7 +237,7 @@ function generateReferrerUrl(referrerConfig, forceDebug = false) {
218
237
  return '';
219
238
  }
220
239
 
221
- const randomUrl = referrerConfig[Math.floor(Math.random() * referrerConfig.length)];
240
+ const randomUrl = getRandomElement(referrerConfig);
222
241
  const url = isValidUrl(randomUrl) ? randomUrl : '';
223
242
 
224
243
  if (forceDebug) {
@@ -261,7 +280,7 @@ function generateReferrerUrl(referrerConfig, forceDebug = false) {
261
280
  case 'mixed': {
262
281
  // Randomly choose between different referrer types
263
282
  const modes = ['random_search', 'social_media', 'news_sites'];
264
- const randomMode = modes[Math.floor(Math.random() * modes.length)];
283
+ const randomMode = getRandomElement(modes);
265
284
 
266
285
  if (forceDebug) console.log(`[debug] Mixed mode selected: ${randomMode}`);
267
286
 
@@ -335,14 +354,26 @@ function validateReferrerConfig(referrerConfig) {
335
354
  if (Array.isArray(referrerConfig)) {
336
355
  if (referrerConfig.length === 0) {
337
356
  result.warnings.push('Empty referrer array will result in no referrer');
338
- } else {
339
- referrerConfig.forEach((url, index) => {
357
+ return result;
358
+ }
359
+
360
+ // Fast validation: check only first and last items if array is large
361
+ const itemsToCheck = referrerConfig.length > 10
362
+ ? [referrerConfig[0], referrerConfig[referrerConfig.length - 1]]
363
+ : referrerConfig;
364
+
365
+ itemsToCheck.forEach((url, index) => {
340
366
  if (!isValidUrl(url)) {
341
- result.errors.push(`Array item ${index} is not a valid HTTP/HTTPS URL: ${url}`);
367
+ const actualIndex = itemsToCheck === referrerConfig ? index : (index === 0 ? 0 : referrerConfig.length - 1);
368
+ result.errors.push(`Array item ${actualIndex} is not a valid HTTP/HTTPS URL: ${url}`);
342
369
  result.isValid = false;
343
370
  }
344
371
  });
372
+
373
+ if (referrerConfig.length > 10 && itemsToCheck.length < referrerConfig.length) {
374
+ result.warnings.push(`Large array (${referrerConfig.length} items): only validated first and last items for performance`);
345
375
  }
376
+
346
377
  return result;
347
378
  }
348
379
 
package/nwss.js CHANGED
@@ -1,4 +1,4 @@
1
- // === Network scanner script (nwss.js) v2.0.31 ===
1
+ // === Network scanner script (nwss.js) v2.0.32 ===
2
2
 
3
3
  // puppeteer for browser automation, fs for file system operations, psl for domain parsing.
4
4
  // const pLimit = require('p-limit'); // Will be dynamically imported
@@ -145,7 +145,7 @@ const { navigateWithRedirectHandling, handleRedirectTimeout } = require('./lib/r
145
145
  const { monitorBrowserHealth, isBrowserHealthy, isQuicklyResponsive, performGroupWindowCleanup, performRealtimeWindowCleanup, trackPageForRealtime, updatePageUsage, cleanupPageBeforeReload } = require('./lib/browserhealth');
146
146
 
147
147
  // --- Script Configuration & Constants ---
148
- const VERSION = '2.0.31'; // Script version
148
+ const VERSION = '2.0.32'; // Script version
149
149
 
150
150
  // get startTime
151
151
  const startTime = Date.now();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fanboynz/network-scanner",
3
- "version": "2.0.31",
3
+ "version": "2.0.32",
4
4
  "description": "A Puppeteer-based network scanner for analyzing web traffic, generating adblock filter rules, and identifying third-party requests. Features include fingerprint spoofing, Cloudflare bypass, content analysis with curl/grep, and multiple output formats.",
5
5
  "main": "nwss.js",
6
6
  "scripts": {