@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.
@@ -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
- private readonly maxTasksPerMinute;
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);
@@ -50,6 +50,7 @@ export class ConcurrencySystem {
50
50
  scaleDownStepRatio;
51
51
  #loggingIntervalMillis;
52
52
  #autoscaleIntervalMillis;
53
+ /** The cap on tasks started per minute, or `Infinity` when uncapped. */
53
54
  maxTasksPerMinute;
54
55
  #minConcurrency;
55
56
  #maxConcurrency;
@@ -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
- // kept as TS-private: tests reset the counter at runtime
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.instanceCount++;
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
- // Not `#private`, unlike the rest: the tests reach for these two.
109
- domainStates = new Map();
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.log = serviceLocator.getLogger().child({ prefix: 'ThrottlingRequestManager' });
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.domainStates.set(key, newDomainState(key));
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.log.warning(message);
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.domainStates.get(domain) ?? null;
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.domainStates.get(domain);
309
+ let state = this.#domainStates.get(domain);
311
310
  if (!state) {
312
311
  state = newDomainState(domain);
313
- this.domainStates.set(domain, state);
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.domainStates.values())
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.log.debug(`Crawl-delay floor for every domain set to ${(this.#minCrawlDelayMs / 1000).toFixed(1)}s`);
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.log.debug(`Applying a pacing signal scoped to "hostname" across the whole registrable domain, because that ` +
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.log.warning(`Capping ${source} delay of ${(delayMs / 1000).toFixed(1)}s for domain "${state.domain}" ` +
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.log.info(`Rate limit (429) detected for domain "${state.domain}" ` +
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.log.debug(`Set crawl-delay for domain "${state.domain}" to ${(intervalMs / 1000).toFixed(1)}s`);
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.domainStates.values()) {
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.domainStates.values()) {
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.domainStates.get(domain);
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.188",
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.188",
46
- "@crawlee/http-client": "4.0.0-beta.188",
47
- "@crawlee/types": "4.0.0-beta.188",
48
- "@crawlee/utils": "4.0.0-beta.188",
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.188"
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": "e328abb1ed78f0f4521fe12d7c1cb63a922a38f8"
69
+ "gitHead": "1dbb0f272b9da9c7a6bf3bdc1ae48f84eda3f21c"
70
70
  }