@crawlee/core 3.17.1-beta.45 → 3.17.1-beta.47

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crawlee/core",
3
- "version": "3.17.1-beta.45",
3
+ "version": "3.17.1-beta.47",
4
4
  "description": "The scalable web crawling and scraping library for JavaScript/Node.js. Enables development of data extraction and web automation jobs (not only) with headless Chrome and Puppeteer.",
5
5
  "engines": {
6
6
  "node": ">=16.0.0"
@@ -59,9 +59,9 @@
59
59
  "@apify/pseudo_url": "^2.0.30",
60
60
  "@apify/timeout": "^0.3.0",
61
61
  "@apify/utilities": "^2.7.10",
62
- "@crawlee/memory-storage": "3.17.1-beta.45",
63
- "@crawlee/types": "3.17.1-beta.45",
64
- "@crawlee/utils": "3.17.1-beta.45",
62
+ "@crawlee/memory-storage": "3.17.1-beta.47",
63
+ "@crawlee/types": "3.17.1-beta.47",
64
+ "@crawlee/utils": "3.17.1-beta.47",
65
65
  "@sapphire/async-queue": "^1.5.1",
66
66
  "@vladfrangu/async_event_emitter": "^2.2.2",
67
67
  "csv-stringify": "^6.2.0",
@@ -83,5 +83,5 @@
83
83
  }
84
84
  }
85
85
  },
86
- "gitHead": "c0676ff5f444967c1a323e02dd9e34c00f7469f9"
86
+ "gitHead": "93d7b6fccbd6e4f4b474adbba55e5f05051bbaf2"
87
87
  }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * A fixed-size, direct-mapped cache for `uniqueKey`-based request deduplication.
3
+ *
4
+ * `RequestProvider.requestCache` only remembers the first batch of requests, so repeated
5
+ * `addRequestsBatched()` calls with overlapping URLs re-submit already-enqueued requests
6
+ * (https://github.com/apify/crawlee/issues/3120). This is a separate, cheaper cache we can populate on
7
+ * every batch: a fixed number of slots indexed by a hash of the request's cache key, storing the
8
+ * server-assigned `requestId`. Memory is capped by the slot count regardless of the working set size;
9
+ * a hash collision just overwrites a slot, causing an occasional cache miss (a harmless re-submission)
10
+ * but never a false hit — so a genuinely new request is never dropped.
11
+ *
12
+ * @internal
13
+ */
14
+ export declare class RequestDeduplicationCache {
15
+ private readonly size;
16
+ private keys;
17
+ private ids;
18
+ constructor(size?: number);
19
+ get(cacheKey: string): string | null;
20
+ add(cacheKey: string, requestId: string): void;
21
+ clear(): void;
22
+ private indexOf;
23
+ }
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RequestDeduplicationCache = void 0;
4
+ /**
5
+ * A fixed-size, direct-mapped cache for `uniqueKey`-based request deduplication.
6
+ *
7
+ * `RequestProvider.requestCache` only remembers the first batch of requests, so repeated
8
+ * `addRequestsBatched()` calls with overlapping URLs re-submit already-enqueued requests
9
+ * (https://github.com/apify/crawlee/issues/3120). This is a separate, cheaper cache we can populate on
10
+ * every batch: a fixed number of slots indexed by a hash of the request's cache key, storing the
11
+ * server-assigned `requestId`. Memory is capped by the slot count regardless of the working set size;
12
+ * a hash collision just overwrites a slot, causing an occasional cache miss (a harmless re-submission)
13
+ * but never a false hit — so a genuinely new request is never dropped.
14
+ *
15
+ * @internal
16
+ */
17
+ class RequestDeduplicationCache {
18
+ // The slot count is the same for every queue, so it's a fixed default rather than a per-consumer option.
19
+ constructor(size = 1000000) {
20
+ Object.defineProperty(this, "size", {
21
+ enumerable: true,
22
+ configurable: true,
23
+ writable: true,
24
+ value: size
25
+ });
26
+ Object.defineProperty(this, "keys", {
27
+ enumerable: true,
28
+ configurable: true,
29
+ writable: true,
30
+ value: void 0
31
+ });
32
+ Object.defineProperty(this, "ids", {
33
+ enumerable: true,
34
+ configurable: true,
35
+ writable: true,
36
+ value: void 0
37
+ });
38
+ this.keys = new Array(size);
39
+ this.ids = new Array(size);
40
+ }
41
+ get(cacheKey) {
42
+ const index = this.indexOf(cacheKey);
43
+ return this.keys[index] === cacheKey ? this.ids[index] : null;
44
+ }
45
+ add(cacheKey, requestId) {
46
+ const index = this.indexOf(cacheKey);
47
+ this.keys[index] = cacheKey;
48
+ this.ids[index] = requestId;
49
+ }
50
+ clear() {
51
+ this.keys = new Array(this.size);
52
+ this.ids = new Array(this.size);
53
+ }
54
+ // A cheap FNV-1a hash of the cache key — avoids pulling in a dedicated hashing dependency.
55
+ indexOf(cacheKey) {
56
+ /* eslint-disable no-bitwise */
57
+ let hash = 0x811c9dc5;
58
+ for (let i = 0; i < cacheKey.length; i++) {
59
+ hash ^= cacheKey.charCodeAt(i);
60
+ hash = Math.imul(hash, 0x01000193);
61
+ }
62
+ return (hash >>> 0) % this.size;
63
+ /* eslint-enable no-bitwise */
64
+ }
65
+ }
66
+ exports.RequestDeduplicationCache = RequestDeduplicationCache;
@@ -5,6 +5,7 @@ import { Configuration } from '../configuration';
5
5
  import type { ProxyConfiguration } from '../proxy_configuration';
6
6
  import type { InternalSource, RequestOptions, Source } from '../request';
7
7
  import { Request } from '../request';
8
+ import { RequestDeduplicationCache } from './request_dedup_cache';
8
9
  import type { IStorage, StorageManagerOptions } from './storage_manager';
9
10
  export type RequestsLike = AsyncIterable<Source | string> | Iterable<Source | string> | (Source | string)[];
10
11
  /**
@@ -74,6 +75,12 @@ export declare abstract class RequestProvider implements IStorage, IRequestManag
74
75
  private initialHandledCount;
75
76
  protected queueHeadIds: ListDictionary<string>;
76
77
  protected requestCache: LruCache<RequestLruItem>;
78
+ /**
79
+ * Remembers the `requestId` of every request already submitted to the client — including background
80
+ * batches that `requestCache` skips — so overlapping URL sets aren't re-submitted.
81
+ * See {@link RequestDeduplicationCache} for why this is a separate, cheaper cache.
82
+ */
83
+ protected requestSeenCache: RequestDeduplicationCache;
77
84
  protected recentlyHandledRequestsCache: LruCache<boolean>;
78
85
  protected queuePausedForMigration: boolean;
79
86
  protected lastActivity: Date;
@@ -11,6 +11,7 @@ const configuration_1 = require("../configuration");
11
11
  const log_1 = require("../log");
12
12
  const request_1 = require("../request");
13
13
  const access_checking_1 = require("./access_checking");
14
+ const request_dedup_cache_1 = require("./request_dedup_cache");
14
15
  const storage_manager_1 = require("./storage_manager");
15
16
  const utils_2 = require("./utils");
16
17
  /**
@@ -118,6 +119,17 @@ class RequestProvider {
118
119
  writable: true,
119
120
  value: void 0
120
121
  });
122
+ /**
123
+ * Remembers the `requestId` of every request already submitted to the client — including background
124
+ * batches that `requestCache` skips — so overlapping URL sets aren't re-submitted.
125
+ * See {@link RequestDeduplicationCache} for why this is a separate, cheaper cache.
126
+ */
127
+ Object.defineProperty(this, "requestSeenCache", {
128
+ enumerable: true,
129
+ configurable: true,
130
+ writable: true,
131
+ value: void 0
132
+ });
121
133
  Object.defineProperty(this, "recentlyHandledRequestsCache", {
122
134
  enumerable: true,
123
135
  configurable: true,
@@ -156,6 +168,7 @@ class RequestProvider {
156
168
  });
157
169
  this.proxyConfiguration = options.proxyConfiguration;
158
170
  this.requestCache = new datastructures_1.LruCache({ maxLength: options.requestCacheMaxSize });
171
+ this.requestSeenCache = new request_dedup_cache_1.RequestDeduplicationCache();
159
172
  this.recentlyHandledRequestsCache = new datastructures_1.LruCache({ maxLength: options.recentlyHandledRequestsMaxSize });
160
173
  this.log = log_1.log.child({ prefix: `${options.logPrefix}(${this.id}, ${this.name ?? 'no-name'})` });
161
174
  const eventManager = config.getEventManager();
@@ -232,6 +245,7 @@ class RequestProvider {
232
245
  };
233
246
  const { requestId, wasAlreadyPresent } = queueOperationInfo;
234
247
  this._cacheRequest(cacheKey, queueOperationInfo);
248
+ this.requestSeenCache.add(cacheKey, requestId);
235
249
  if (!wasAlreadyPresent && !this.recentlyHandledRequestsCache.get(requestId)) {
236
250
  this.assumedTotalCount++;
237
251
  // Performance optimization: add request straight to head if possible
@@ -293,16 +307,17 @@ class RequestProvider {
293
307
  const requestsToAdd = new Map();
294
308
  for (const request of requests) {
295
309
  const cacheKey = getCachedRequestId(request.uniqueKey);
310
+ // Prefer the full `requestCache` record; fall back to the dedup cache for background batches it skips.
296
311
  const cachedInfo = this.requestCache.get(cacheKey);
297
- if (cachedInfo) {
298
- request.id = cachedInfo.id;
312
+ const knownRequestId = cachedInfo?.id ?? this.requestSeenCache.get(cacheKey);
313
+ if (knownRequestId) {
314
+ request.id = knownRequestId;
299
315
  results.processedRequests.push({
300
316
  wasAlreadyPresent: true,
301
- // We may assume that if request is in local cache then also the information if the
302
- // request was already handled is there because just one client should be using one queue.
303
- wasAlreadyHandled: cachedInfo.isHandled,
304
- requestId: cachedInfo.id,
305
- uniqueKey: cachedInfo.uniqueKey,
317
+ // The dedup cache doesn't track the handled state; only the full record does.
318
+ wasAlreadyHandled: cachedInfo?.isHandled ?? false,
319
+ requestId: knownRequestId,
320
+ uniqueKey: request.uniqueKey,
306
321
  });
307
322
  }
308
323
  else if (!requestsToAdd.has(request.uniqueKey)) {
@@ -325,6 +340,8 @@ class RequestProvider {
325
340
  if (cache) {
326
341
  this._cacheRequest(cacheKey, { ...newRequest, forefront });
327
342
  }
343
+ // Unlike `requestCache`, populate this on every batch (including background ones).
344
+ this.requestSeenCache.add(cacheKey, requestId);
328
345
  if (!wasAlreadyPresent && !this.recentlyHandledRequestsCache.get(requestId)) {
329
346
  this.assumedTotalCount++;
330
347
  // Performance optimization: add request straight to head if possible
@@ -558,6 +575,7 @@ class RequestProvider {
558
575
  this.assumedTotalCount = 0;
559
576
  this.assumedHandledCount = 0;
560
577
  this.requestCache.clear();
578
+ this.requestSeenCache.clear();
561
579
  }
562
580
  /**
563
581
  * Caches information about request to beware of unneeded addRequest() calls.