@crawlee/core 3.17.1-beta.7 → 3.17.1-beta.71
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/autoscaling/autoscaled_pool.js +4 -1
- package/autoscaling/snapshotter.d.ts +2 -2
- package/cookie_utils.js +2 -0
- package/crawlers/statistics.d.ts +11 -2
- package/crawlers/statistics.js +7 -4
- package/enqueue_links/enqueue_links.d.ts +3 -53
- package/enqueue_links/enqueue_links.js +13 -63
- package/enqueue_links/shared.d.ts +1 -1
- package/errors.d.ts +20 -0
- package/errors.js +32 -1
- package/http_clients/got-scraping-http-client.js +8 -0
- package/index.mjs +3 -0
- package/package.json +7 -6
- package/router.d.ts +133 -14
- package/router.js +136 -34
- package/session_pool/session_pool.d.ts +4 -1
- package/session_pool/session_pool.js +5 -2
- package/storages/request_dedup_cache.d.ts +23 -0
- package/storages/request_dedup_cache.js +66 -0
- package/storages/request_list.d.ts +6 -0
- package/storages/request_list.js +16 -2
- package/storages/request_provider.d.ts +7 -0
- package/storages/request_provider.js +39 -9
- package/storages/sitemap_request_list.d.ts +12 -0
- package/storages/sitemap_request_list.js +22 -3
|
@@ -11,8 +11,14 @@ 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");
|
|
17
|
+
/**
|
|
18
|
+
* Maximum number of consecutive batch-add attempts that make no progress before the remaining
|
|
19
|
+
* unprocessed requests are skipped, so permanently rejected requests don't retry forever.
|
|
20
|
+
*/
|
|
21
|
+
const MAX_UNPROCESSED_REQUESTS_RETRIES = 3;
|
|
16
22
|
class RequestProvider {
|
|
17
23
|
constructor(options, config = configuration_1.Configuration.getGlobalConfig()) {
|
|
18
24
|
Object.defineProperty(this, "config", {
|
|
@@ -113,6 +119,17 @@ class RequestProvider {
|
|
|
113
119
|
writable: true,
|
|
114
120
|
value: void 0
|
|
115
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
|
+
});
|
|
116
133
|
Object.defineProperty(this, "recentlyHandledRequestsCache", {
|
|
117
134
|
enumerable: true,
|
|
118
135
|
configurable: true,
|
|
@@ -151,6 +168,7 @@ class RequestProvider {
|
|
|
151
168
|
});
|
|
152
169
|
this.proxyConfiguration = options.proxyConfiguration;
|
|
153
170
|
this.requestCache = new datastructures_1.LruCache({ maxLength: options.requestCacheMaxSize });
|
|
171
|
+
this.requestSeenCache = new request_dedup_cache_1.RequestDeduplicationCache();
|
|
154
172
|
this.recentlyHandledRequestsCache = new datastructures_1.LruCache({ maxLength: options.recentlyHandledRequestsMaxSize });
|
|
155
173
|
this.log = log_1.log.child({ prefix: `${options.logPrefix}(${this.id}, ${this.name ?? 'no-name'})` });
|
|
156
174
|
const eventManager = config.getEventManager();
|
|
@@ -227,6 +245,7 @@ class RequestProvider {
|
|
|
227
245
|
};
|
|
228
246
|
const { requestId, wasAlreadyPresent } = queueOperationInfo;
|
|
229
247
|
this._cacheRequest(cacheKey, queueOperationInfo);
|
|
248
|
+
this.requestSeenCache.add(cacheKey, requestId);
|
|
230
249
|
if (!wasAlreadyPresent && !this.recentlyHandledRequestsCache.get(requestId)) {
|
|
231
250
|
this.assumedTotalCount++;
|
|
232
251
|
// Performance optimization: add request straight to head if possible
|
|
@@ -288,16 +307,17 @@ class RequestProvider {
|
|
|
288
307
|
const requestsToAdd = new Map();
|
|
289
308
|
for (const request of requests) {
|
|
290
309
|
const cacheKey = getCachedRequestId(request.uniqueKey);
|
|
310
|
+
// Prefer the full `requestCache` record; fall back to the dedup cache for background batches it skips.
|
|
291
311
|
const cachedInfo = this.requestCache.get(cacheKey);
|
|
292
|
-
|
|
293
|
-
|
|
312
|
+
const knownRequestId = cachedInfo?.id ?? this.requestSeenCache.get(cacheKey);
|
|
313
|
+
if (knownRequestId) {
|
|
314
|
+
request.id = knownRequestId;
|
|
294
315
|
results.processedRequests.push({
|
|
295
316
|
wasAlreadyPresent: true,
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
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,
|
|
301
321
|
});
|
|
302
322
|
}
|
|
303
323
|
else if (!requestsToAdd.has(request.uniqueKey)) {
|
|
@@ -320,6 +340,8 @@ class RequestProvider {
|
|
|
320
340
|
if (cache) {
|
|
321
341
|
this._cacheRequest(cacheKey, { ...newRequest, forefront });
|
|
322
342
|
}
|
|
343
|
+
// Unlike `requestCache`, populate this on every batch (including background ones).
|
|
344
|
+
this.requestSeenCache.add(cacheKey, requestId);
|
|
323
345
|
if (!wasAlreadyPresent && !this.recentlyHandledRequestsCache.get(requestId)) {
|
|
324
346
|
this.assumedTotalCount++;
|
|
325
347
|
// Performance optimization: add request straight to head if possible
|
|
@@ -385,13 +407,20 @@ class RequestProvider {
|
|
|
385
407
|
const requestIterator = generateRequests();
|
|
386
408
|
const chunks = (0, utils_1.peekableAsyncIterable)((0, utils_1.chunkedAsyncIterable)(requestIterator, effectiveChunkSize));
|
|
387
409
|
const chunksIterator = chunks[Symbol.asyncIterator]();
|
|
388
|
-
const attemptToAddToQueueAndAddAnyUnprocessed = async (providedRequests, cache = true) => {
|
|
410
|
+
const attemptToAddToQueueAndAddAnyUnprocessed = async (providedRequests, cache = true, unsuccessfulAttempts = 0) => {
|
|
389
411
|
const resultsToReturn = [];
|
|
390
412
|
const apiResult = await this.addRequests(providedRequests, { forefront: options.forefront, cache });
|
|
391
413
|
resultsToReturn.push(...apiResult.processedRequests);
|
|
392
414
|
if (apiResult.unprocessedRequests.length) {
|
|
415
|
+
// Count attempts that make no progress, so permanently rejected requests (e.g. a malformed
|
|
416
|
+
// `userData` shape causing a 400) don't loop forever. Any progress resets the counter.
|
|
417
|
+
const attempts = apiResult.processedRequests.length ? 0 : unsuccessfulAttempts + 1;
|
|
418
|
+
if (attempts >= MAX_UNPROCESSED_REQUESTS_RETRIES) {
|
|
419
|
+
this.log.warning(`Some requests were consistently rejected by the request queue and will be skipped after ${MAX_UNPROCESSED_REQUESTS_RETRIES} attempts. This usually means the request data is malformed (e.g. an invalid 'userData' shape).`, { unprocessedRequests: apiResult.unprocessedRequests });
|
|
420
|
+
return resultsToReturn;
|
|
421
|
+
}
|
|
393
422
|
await (0, utils_1.sleep)(waitBetweenBatchesMillis);
|
|
394
|
-
resultsToReturn.push(...(await attemptToAddToQueueAndAddAnyUnprocessed(providedRequests.filter((r) => !apiResult.processedRequests.some((pr) => pr.uniqueKey === r.uniqueKey)), false)));
|
|
423
|
+
resultsToReturn.push(...(await attemptToAddToQueueAndAddAnyUnprocessed(providedRequests.filter((r) => !apiResult.processedRequests.some((pr) => pr.uniqueKey === r.uniqueKey)), false, attempts)));
|
|
395
424
|
}
|
|
396
425
|
return resultsToReturn;
|
|
397
426
|
};
|
|
@@ -546,6 +575,7 @@ class RequestProvider {
|
|
|
546
575
|
this.assumedTotalCount = 0;
|
|
547
576
|
this.assumedHandledCount = 0;
|
|
548
577
|
this.requestCache.clear();
|
|
578
|
+
this.requestSeenCache.clear();
|
|
549
579
|
}
|
|
550
580
|
/**
|
|
551
581
|
* Caches information about request to beware of unneeded addRequest() calls.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type ParseSitemapOptions } from '@crawlee/utils';
|
|
2
2
|
import { Configuration } from '../configuration';
|
|
3
3
|
import type { GlobInput, RegExpInput } from '../enqueue_links';
|
|
4
|
+
import { EnqueueStrategy } from '../enqueue_links';
|
|
4
5
|
import { Request } from '../request';
|
|
5
6
|
import type { IRequestList } from './request_list';
|
|
6
7
|
interface UrlConstraints {
|
|
@@ -76,6 +77,13 @@ export interface SitemapRequestListOptions extends UrlConstraints {
|
|
|
76
77
|
* @default 200
|
|
77
78
|
*/
|
|
78
79
|
maxBufferSize?: number;
|
|
80
|
+
/**
|
|
81
|
+
* Keep only sitemap-derived URLs matching this strategy relative to the parent sitemap URL; non-`http(s)`
|
|
82
|
+
* schemes are always dropped. The filtering stays enforced after navigation (e.g. across redirects).
|
|
83
|
+
* Pass `'all'` to disable host filtering.
|
|
84
|
+
* @default EnqueueStrategy.SameHostname
|
|
85
|
+
*/
|
|
86
|
+
enqueueStrategy?: EnqueueStrategy | `${EnqueueStrategy}`;
|
|
79
87
|
/**
|
|
80
88
|
* Advanced options for the underlying `parseSitemap` call.
|
|
81
89
|
*/
|
|
@@ -133,6 +141,10 @@ export declare class SitemapRequestList implements IRequestList {
|
|
|
133
141
|
* Proxy URL to be used for sitemap loading.
|
|
134
142
|
*/
|
|
135
143
|
private proxyUrl;
|
|
144
|
+
/**
|
|
145
|
+
* Enqueue strategy applied to sitemap-derived URLs and stamped onto the emitted `Request` objects.
|
|
146
|
+
*/
|
|
147
|
+
private enqueueStrategy;
|
|
136
148
|
/**
|
|
137
149
|
* Logger instance.
|
|
138
150
|
*/
|
|
@@ -132,6 +132,15 @@ class SitemapRequestList {
|
|
|
132
132
|
writable: true,
|
|
133
133
|
value: void 0
|
|
134
134
|
});
|
|
135
|
+
/**
|
|
136
|
+
* Enqueue strategy applied to sitemap-derived URLs and stamped onto the emitted `Request` objects.
|
|
137
|
+
*/
|
|
138
|
+
Object.defineProperty(this, "enqueueStrategy", {
|
|
139
|
+
enumerable: true,
|
|
140
|
+
configurable: true,
|
|
141
|
+
writable: true,
|
|
142
|
+
value: void 0
|
|
143
|
+
});
|
|
135
144
|
/**
|
|
136
145
|
* Logger instance.
|
|
137
146
|
*/
|
|
@@ -173,6 +182,7 @@ class SitemapRequestList {
|
|
|
173
182
|
signal: ow_1.default.optional.any(),
|
|
174
183
|
timeoutMillis: ow_1.default.optional.number,
|
|
175
184
|
maxBufferSize: ow_1.default.optional.number,
|
|
185
|
+
enqueueStrategy: ow_1.default.optional.string.oneOf(Object.values(enqueue_links_1.EnqueueStrategy)),
|
|
176
186
|
parseSitemapOptions: ow_1.default.optional.object,
|
|
177
187
|
globs: ow_1.default.optional.array.ofType(ow_1.default.any(ow_1.default.string, ow_1.default.object.hasKeys('glob'))),
|
|
178
188
|
exclude: ow_1.default.optional.array.ofType(ow_1.default.any(ow_1.default.string, ow_1.default.regExp, ow_1.default.object.hasKeys('glob'), ow_1.default.object.hasKeys('regexp'))),
|
|
@@ -200,6 +210,7 @@ class SitemapRequestList {
|
|
|
200
210
|
this.persistStateKey = options.persistStateKey;
|
|
201
211
|
this.persistenceOptions = { enable: true, ...options.persistenceOptions };
|
|
202
212
|
this.proxyUrl = options.proxyUrl;
|
|
213
|
+
this.enqueueStrategy = options.enqueueStrategy ?? enqueue_links_1.EnqueueStrategy.SameHostname;
|
|
203
214
|
this.urlQueueStream = this.createNewStream(options.maxBufferSize ?? 200);
|
|
204
215
|
this.sitemapParsingProgress.pendingSitemapUrls = new Set(options.sitemapUrls);
|
|
205
216
|
this.events = config.getEventManager();
|
|
@@ -308,6 +319,7 @@ class SitemapRequestList {
|
|
|
308
319
|
...parseSitemapOptions,
|
|
309
320
|
maxDepth: 0,
|
|
310
321
|
emitNestedSitemaps: true,
|
|
322
|
+
enqueueStrategy: this.enqueueStrategy,
|
|
311
323
|
})) {
|
|
312
324
|
if (!item.originSitemapUrl) {
|
|
313
325
|
// This is a nested sitemap
|
|
@@ -398,14 +410,21 @@ class SitemapRequestList {
|
|
|
398
410
|
}
|
|
399
411
|
// Create a new stream, as we have read all the URLs from the current one.
|
|
400
412
|
// Pushing the urls back to the original stream might not be possible if it has been ended.
|
|
401
|
-
const
|
|
413
|
+
const previousStream = this.urlQueueStream;
|
|
414
|
+
const newStream = this.createNewStream(previousStream.readableHighWaterMark);
|
|
402
415
|
for (const url of urlQueue) {
|
|
403
416
|
newStream.push(url);
|
|
404
417
|
}
|
|
405
|
-
if (
|
|
418
|
+
if (previousStream.writableEnded) {
|
|
406
419
|
newStream.end();
|
|
407
420
|
}
|
|
408
421
|
this.urlQueueStream = newStream;
|
|
422
|
+
// A `pushNextUrl()` call may be blocked on backpressure, waiting for a `readdata` event on the
|
|
423
|
+
// previous stream. That event is only ever emitted by `readNextUrl()` on the current stream, so
|
|
424
|
+
// after the swap the waiter would never be notified and the background sitemap loading would hang.
|
|
425
|
+
// Re-emit `readdata` on the previous stream to release any such pending waiter (its URL has already
|
|
426
|
+
// been transferred to the new stream above).
|
|
427
|
+
previousStream.emit('readdata');
|
|
409
428
|
await this.store.setValue(this.persistStateKey, {
|
|
410
429
|
sitemapParsingProgress: {
|
|
411
430
|
pendingSitemapUrls: Array.from(this.sitemapParsingProgress.pendingSitemapUrls),
|
|
@@ -457,7 +476,7 @@ class SitemapRequestList {
|
|
|
457
476
|
if (!nextUrl) {
|
|
458
477
|
return null;
|
|
459
478
|
}
|
|
460
|
-
this.requestData.set(nextUrl, new request_1.Request({ url: nextUrl }));
|
|
479
|
+
this.requestData.set(nextUrl, new request_1.Request({ url: nextUrl, enqueueStrategy: this.enqueueStrategy }));
|
|
461
480
|
}
|
|
462
481
|
this.inProgress.add(nextUrl);
|
|
463
482
|
return this.requestData.get(nextUrl);
|