@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.
|
|
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.
|
|
63
|
-
"@crawlee/types": "3.17.1-beta.
|
|
64
|
-
"@crawlee/utils": "3.17.1-beta.
|
|
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": "
|
|
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
|
-
|
|
298
|
-
|
|
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
|
-
//
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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.
|