@crawlee/basic 4.0.0-beta.188 → 4.0.0-beta.189
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/internals/autoscaling/concurrency_system.d.ts +2 -1
- package/internals/autoscaling/concurrency_system.js +1 -0
- package/internals/basic-crawler.d.ts +0 -6
- package/internals/basic-crawler.js +5 -3
- package/internals/throttling_request_manager.d.ts +0 -2
- package/internals/throttling_request_manager.js +17 -18
- package/package.json +7 -7
|
@@ -163,7 +163,8 @@ export declare class ConcurrencySystem implements IConcurrencySystem {
|
|
|
163
163
|
private readonly desiredConcurrencyRatio;
|
|
164
164
|
private readonly scaleUpStepRatio;
|
|
165
165
|
private readonly scaleDownStepRatio;
|
|
166
|
-
|
|
166
|
+
/** The cap on tasks started per minute, or `Infinity` when uncapped. */
|
|
167
|
+
readonly maxTasksPerMinute: number;
|
|
167
168
|
private readonly snapshotter;
|
|
168
169
|
private readonly systemStatus;
|
|
169
170
|
constructor(options?: ConcurrencySystemOptions);
|
|
@@ -386,12 +386,6 @@ export interface BasicCrawlerOptions<Context extends CrawlingContext = CrawlingC
|
|
|
386
386
|
}
|
|
387
387
|
export declare class BasicCrawler<Context extends CrawlingContext = CrawlingContext, ContextExtension = Dictionary<never>, ExtendedContext extends Context = Context & ContextExtension, Routes extends Record<keyof Routes, Dictionary> = Record<string, GetUserDataFromRequest<Context['request']>>, StatisticStateExtension extends object = {}> {
|
|
388
388
|
#private;
|
|
389
|
-
/**
|
|
390
|
-
* Tracks the number of crawler instances created. The first crawler uses the default
|
|
391
|
-
* request queue; subsequent ones get their own queue via a unique alias so they don't
|
|
392
|
-
* collide.
|
|
393
|
-
*/
|
|
394
|
-
private static instanceCount;
|
|
395
389
|
/**
|
|
396
390
|
* The statistics instance collecting the crawler's run statistics - either the injected `statistics` option or a
|
|
397
391
|
* crawler-built default. Typed as {@link IStatistics} so custom implementations can be plugged in.
|
|
@@ -90,8 +90,7 @@ export class BasicCrawler {
|
|
|
90
90
|
* request queue; subsequent ones get their own queue via a unique alias so they don't
|
|
91
91
|
* collide.
|
|
92
92
|
*/
|
|
93
|
-
|
|
94
|
-
static instanceCount = 0;
|
|
93
|
+
static #instanceCount = 0;
|
|
95
94
|
/**
|
|
96
95
|
* Tracks crawler instances that accessed shared state without having an explicit id.
|
|
97
96
|
* Used to detect and warn about multiple crawlers sharing the same state.
|
|
@@ -344,7 +343,7 @@ export class BasicCrawler {
|
|
|
344
343
|
this.#log = serviceLocator.getLogger().child({ prefix: this.constructor.name });
|
|
345
344
|
// Initialize the Configuration instance to avoid lazy loading in the components
|
|
346
345
|
serviceLocator.getConfiguration();
|
|
347
|
-
const instanceIndex = BasicCrawler
|
|
346
|
+
const instanceIndex = BasicCrawler.#instanceCount++;
|
|
348
347
|
this.#identity = { instanceIndex, hasExplicitId: id !== undefined, id: id ?? String(instanceIndex) };
|
|
349
348
|
if (requestManager !== undefined && (requestList !== undefined || requestQueue !== undefined)) {
|
|
350
349
|
throw new Error('The `requestManager` option cannot be used in conjunction with `requestList` and/or `requestQueue`');
|
|
@@ -1525,6 +1524,7 @@ export class BasicCrawler {
|
|
|
1525
1524
|
return this.requestManager.fetchNextRequest();
|
|
1526
1525
|
}
|
|
1527
1526
|
/** Handles a single request - runs the request handler with retries, error handling, and lifecycle management. */
|
|
1527
|
+
// oxlint-disable-next-line crawlee/prefer-private-fields -- patched by @crawlee/otel
|
|
1528
1528
|
async handleRequest(crawlingContext, requestSource, request) {
|
|
1529
1529
|
// An earlier phase we cannot cancel (e.g. a slow `extendContext`) may have run past the internal timeout,
|
|
1530
1530
|
// which already failed the request in `runTaskFunction`. Bail before running the handler so it does not
|
|
@@ -1662,6 +1662,7 @@ export class BasicCrawler {
|
|
|
1662
1662
|
*
|
|
1663
1663
|
* @param request The request object, passed separately to circumvent potential dynamic logic in crawlingContext.request
|
|
1664
1664
|
*/
|
|
1665
|
+
// oxlint-disable-next-line crawlee/prefer-private-fields -- patched by @crawlee/otel
|
|
1665
1666
|
async requestFunctionErrorHandler(error, crawlingContext, request, source) {
|
|
1666
1667
|
if (error instanceof RequestThrottledError) {
|
|
1667
1668
|
// The domain told us to come back later, so the request was never really attempted. Put it back
|
|
@@ -1720,6 +1721,7 @@ export class BasicCrawler {
|
|
|
1720
1721
|
this.statistics.recordRequestFailure(request.id || request.uniqueKey, request.retryCount);
|
|
1721
1722
|
await this.handleFailedRequestHandler(crawlingContext, error); // This function prints an error message.
|
|
1722
1723
|
}
|
|
1724
|
+
// oxlint-disable-next-line crawlee/prefer-private-fields -- patched by @crawlee/otel
|
|
1723
1725
|
async handleFailedRequestHandler(crawlingContext, error) {
|
|
1724
1726
|
// Always log the last error regardless if the user provided a failedRequestHandler
|
|
1725
1727
|
const { id, url, method, uniqueKey } = crawlingContext.request;
|
|
@@ -151,8 +151,6 @@ export interface ThrottlingRequestManagerOptions<T extends IRequestManager = IRe
|
|
|
151
151
|
export declare class ThrottlingRequestManager<T extends IRequestManager = IRequestManager> implements IRequestManager {
|
|
152
152
|
#private;
|
|
153
153
|
private readonly config;
|
|
154
|
-
private readonly domainStates;
|
|
155
|
-
private readonly log;
|
|
156
154
|
constructor(options: ThrottlingRequestManagerOptions<T>, config?: Configuration);
|
|
157
155
|
/**
|
|
158
156
|
* The wrapped manager, holding every request whose domain is not throttled. `undefined` until an `inner`
|
|
@@ -105,9 +105,8 @@ export class ThrottlingRequestManager {
|
|
|
105
105
|
#maxThrottledDomains;
|
|
106
106
|
#persistStateKey;
|
|
107
107
|
#subManagers = new Map();
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
log;
|
|
108
|
+
#domainStates = new Map();
|
|
109
|
+
#log;
|
|
111
110
|
/** Domains from the `domains` option, which are throttled whether or not the crawl ever visits them. */
|
|
112
111
|
#listedDomains = new Set();
|
|
113
112
|
/**
|
|
@@ -169,7 +168,7 @@ export class ThrottlingRequestManager {
|
|
|
169
168
|
this.#throttleBy = options.throttleBy ?? 'hostname';
|
|
170
169
|
this.#maxThrottledDomains = options.maxThrottledDomains ?? 100;
|
|
171
170
|
this.#persistStateKey = options.persistStateKey ?? DEFAULT_PERSIST_STATE_KEY;
|
|
172
|
-
this
|
|
171
|
+
this.#log = serviceLocator.getLogger().child({ prefix: 'ThrottlingRequestManager' });
|
|
173
172
|
for (const domain of Array.isArray(options.domains) ? options.domains : []) {
|
|
174
173
|
let hostname;
|
|
175
174
|
try {
|
|
@@ -182,7 +181,7 @@ export class ThrottlingRequestManager {
|
|
|
182
181
|
}
|
|
183
182
|
const key = this.#domainKey(hostname);
|
|
184
183
|
this.#listedDomains.add(key);
|
|
185
|
-
this
|
|
184
|
+
this.#domainStates.set(key, newDomainState(key));
|
|
186
185
|
}
|
|
187
186
|
}
|
|
188
187
|
/**
|
|
@@ -239,7 +238,7 @@ export class ThrottlingRequestManager {
|
|
|
239
238
|
return;
|
|
240
239
|
}
|
|
241
240
|
this.#warnedAbout.add(key);
|
|
242
|
-
this
|
|
241
|
+
this.#log.warning(message);
|
|
243
242
|
}
|
|
244
243
|
#extractDomain(url) {
|
|
245
244
|
try {
|
|
@@ -251,7 +250,7 @@ export class ThrottlingRequestManager {
|
|
|
251
250
|
}
|
|
252
251
|
#getDomainState(url) {
|
|
253
252
|
const domain = this.#extractDomain(url);
|
|
254
|
-
return this
|
|
253
|
+
return this.#domainStates.get(domain) ?? null;
|
|
255
254
|
}
|
|
256
255
|
/**
|
|
257
256
|
* The manager that owns a URL's requests, opening a sub-queue for its domain if this is the first time we
|
|
@@ -307,10 +306,10 @@ export class ThrottlingRequestManager {
|
|
|
307
306
|
return subManager;
|
|
308
307
|
}
|
|
309
308
|
#ensureDomainState(domain) {
|
|
310
|
-
let state = this
|
|
309
|
+
let state = this.#domainStates.get(domain);
|
|
311
310
|
if (!state) {
|
|
312
311
|
state = newDomainState(domain);
|
|
313
|
-
this
|
|
312
|
+
this.#domainStates.set(domain, state);
|
|
314
313
|
}
|
|
315
314
|
return state;
|
|
316
315
|
}
|
|
@@ -353,7 +352,7 @@ export class ThrottlingRequestManager {
|
|
|
353
352
|
*/
|
|
354
353
|
#fetchableDomains() {
|
|
355
354
|
const now = Date.now();
|
|
356
|
-
return Array.from(this
|
|
355
|
+
return Array.from(this.#domainStates.values())
|
|
357
356
|
.filter((state) => now >= throttledUntil(state) && this.#subManagers.has(state.domain))
|
|
358
357
|
.sort((a, b) => throttledUntil(a) - throttledUntil(b))
|
|
359
358
|
.map((state) => state.domain);
|
|
@@ -397,7 +396,7 @@ export class ThrottlingRequestManager {
|
|
|
397
396
|
`on this manager yourself via \`minCrawlDelaySecs\`.`);
|
|
398
397
|
}
|
|
399
398
|
this.#minCrawlDelayMs = Math.max(this.#minCrawlDelayMs, intervalMs);
|
|
400
|
-
this
|
|
399
|
+
this.#log.debug(`Crawl-delay floor for every domain set to ${(this.#minCrawlDelayMs / 1000).toFixed(1)}s`);
|
|
401
400
|
return true;
|
|
402
401
|
}
|
|
403
402
|
/**
|
|
@@ -416,7 +415,7 @@ export class ThrottlingRequestManager {
|
|
|
416
415
|
}
|
|
417
416
|
if (scope === 'hostname' && this.#throttleBy === 'registrableDomain') {
|
|
418
417
|
// Only debug: grouping by registrable domain deliberately paces whole sites, subdomains included.
|
|
419
|
-
this
|
|
418
|
+
this.#log.debug(`Applying a pacing signal scoped to "hostname" across the whole registrable domain, because that ` +
|
|
420
419
|
`is how this manager groups requests (\`throttleBy\`). Sibling subdomains are paced with it.`);
|
|
421
420
|
return;
|
|
422
421
|
}
|
|
@@ -458,14 +457,14 @@ export class ThrottlingRequestManager {
|
|
|
458
457
|
let delayMs = waitGiven ? waitMs : this.#baseDelayMs * Math.pow(2, state.consecutive429Count - 1);
|
|
459
458
|
if (delayMs > this.#maxDelayMs) {
|
|
460
459
|
const source = waitGiven ? 'requested wait' : 'exponential backoff';
|
|
461
|
-
this
|
|
460
|
+
this.#log.warning(`Capping ${source} delay of ${(delayMs / 1000).toFixed(1)}s for domain "${state.domain}" ` +
|
|
462
461
|
`to maxDelaySecs (${(this.#maxDelayMs / 1000).toFixed(1)}s); the domain may continue to rate-limit. ` +
|
|
463
462
|
`Consider increasing maxDelaySecs if this recurs.`);
|
|
464
463
|
delayMs = this.#maxDelayMs;
|
|
465
464
|
}
|
|
466
465
|
state.backoffUntil = now + delayMs;
|
|
467
466
|
state.backoffDecaysAt = state.backoffUntil + delayMs;
|
|
468
|
-
this
|
|
467
|
+
this.#log.info(`Rate limit (429) detected for domain "${state.domain}" ` +
|
|
469
468
|
`(consecutive: ${state.consecutive429Count}, delay: ${(delayMs / 1000).toFixed(1)}s)`);
|
|
470
469
|
return true;
|
|
471
470
|
}
|
|
@@ -487,7 +486,7 @@ export class ThrottlingRequestManager {
|
|
|
487
486
|
const state = this.#ensureDomainState(domain);
|
|
488
487
|
if (state.declaredCrawlDelayMs === null) {
|
|
489
488
|
state.declaredCrawlDelayMs = intervalMs;
|
|
490
|
-
this
|
|
489
|
+
this.#log.debug(`Set crawl-delay for domain "${state.domain}" to ${(intervalMs / 1000).toFixed(1)}s`);
|
|
491
490
|
}
|
|
492
491
|
return true;
|
|
493
492
|
}
|
|
@@ -615,7 +614,7 @@ export class ThrottlingRequestManager {
|
|
|
615
614
|
const dispatchable = [];
|
|
616
615
|
let readyAt;
|
|
617
616
|
let stallCandidates;
|
|
618
|
-
for (const state of this
|
|
617
|
+
for (const state of this.#domainStates.values()) {
|
|
619
618
|
// A `Crawl-delay` can give a domain a clock before its first request gives it a queue - nothing
|
|
620
619
|
// to fetch from and nothing to wait for until then.
|
|
621
620
|
if (!this.#subManagers.has(state.domain)) {
|
|
@@ -691,7 +690,7 @@ export class ThrottlingRequestManager {
|
|
|
691
690
|
const subManagers = await this.#getSubManagers();
|
|
692
691
|
await Promise.all(subManagers.map(async (manager) => manager.purge?.()));
|
|
693
692
|
this.#migratedFromInner = 0;
|
|
694
|
-
for (const state of this
|
|
693
|
+
for (const state of this.#domainStates.values()) {
|
|
695
694
|
state.consecutive429Count = 0;
|
|
696
695
|
state.backoffUntil = 0;
|
|
697
696
|
state.crawlDelayUntil = 0;
|
|
@@ -734,7 +733,7 @@ export class ThrottlingRequestManager {
|
|
|
734
733
|
async fetchNextRequest() {
|
|
735
734
|
await this.#ensureSubManagers();
|
|
736
735
|
for (const domain of this.#fetchableDomains()) {
|
|
737
|
-
const state = this
|
|
736
|
+
const state = this.#domainStates.get(domain);
|
|
738
737
|
// Armed while the fetch below is still suspended, so that a concurrent `fetchNextRequest` cannot
|
|
739
738
|
// find the domain fetchable and dispatch into the same window - which would pace each task
|
|
740
739
|
// rather than the domain.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crawlee/basic",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.189",
|
|
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": ">=22.0.0"
|
|
@@ -42,10 +42,10 @@
|
|
|
42
42
|
"@apify/datastructures": "^3.0.1",
|
|
43
43
|
"@apify/timeout": "^1.0.1",
|
|
44
44
|
"@apify/utilities": "^3.0.1",
|
|
45
|
-
"@crawlee/core": "4.0.0-beta.
|
|
46
|
-
"@crawlee/http-client": "4.0.0-beta.
|
|
47
|
-
"@crawlee/types": "4.0.0-beta.
|
|
48
|
-
"@crawlee/utils": "4.0.0-beta.
|
|
45
|
+
"@crawlee/core": "4.0.0-beta.189",
|
|
46
|
+
"@crawlee/http-client": "4.0.0-beta.189",
|
|
47
|
+
"@crawlee/types": "4.0.0-beta.189",
|
|
48
|
+
"@crawlee/utils": "4.0.0-beta.189",
|
|
49
49
|
"@sapphire/async-queue": "^1.5.5",
|
|
50
50
|
"@standard-schema/spec": "^1.0.0",
|
|
51
51
|
"csv-stringify": "^6.5.2",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"zod": "^4.5.4"
|
|
58
58
|
},
|
|
59
59
|
"optionalDependencies": {
|
|
60
|
-
"@crawlee/impit-client": "4.0.0-beta.
|
|
60
|
+
"@crawlee/impit-client": "4.0.0-beta.189"
|
|
61
61
|
},
|
|
62
62
|
"lerna": {
|
|
63
63
|
"command": {
|
|
@@ -66,5 +66,5 @@
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
},
|
|
69
|
-
"gitHead": "
|
|
69
|
+
"gitHead": "1dbb0f272b9da9c7a6bf3bdc1ae48f84eda3f21c"
|
|
70
70
|
}
|