@crawlee/basic 4.0.0-beta.123 → 4.0.0-beta.124

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.
@@ -137,7 +137,11 @@ export interface BasicCrawlerOptions<Context extends CrawlingContext = CrawlingC
137
137
  */
138
138
  maxRequestRetries?: number;
139
139
  /**
140
- * Indicates how much time (in seconds) to wait before crawling another same domain request.
140
+ * Indicates how much time (in seconds) to wait before crawling another same domain request. Subdomains are
141
+ * paced together with the site they belong to.
142
+ *
143
+ * Wraps the crawler's request manager in a {@link ThrottlingRequestManager}; pass one as `requestManager`
144
+ * yourself to configure it further.
141
145
  * @default 0
142
146
  */
143
147
  sameDomainDelaySecs?: number;
@@ -824,12 +828,6 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
824
828
  * Fetches the next request to process from the underlying request provider.
825
829
  */
826
830
  private fetchNextRequest;
827
- /**
828
- * Delays processing of the request based on the `sameDomainDelaySecs` option,
829
- * adding it back to the queue after the timeout passes. Returns `true` if the request
830
- * should be ignored and will be reclaimed to the queue once ready.
831
- */
832
- private delayRequest;
833
831
  /** Handles a single request - runs the request handler with retries, error handling, and lifecycle management. */
834
832
  private handleRequest;
835
833
  /**
@@ -1,6 +1,6 @@
1
1
  import { mkdir, writeFile } from 'node:fs/promises';
2
2
  import { dirname } from 'node:path';
3
- import { applyRequestTransform, AutoscaledPool, bindMethodsToServiceLocator, BLOCKED_STATUS_CODES, buildEnqueueStrategyPatterns, ConcurrencySystem, Configuration, constructUrlPatternObjects, ContextPipeline, ContextPipelineCleanupError, ContextPipelineInitializationError, ContextPipelineInterruptedError, createRequestOptions, createStorageTransaction, Request, CriticalError, currentStorageTransaction, Dataset, EnqueueStrategy, EventManager, EventType, filterRequestOptionsByPatterns, getObjectType, KeyValueStore, log, LogLevel, mergeCookies, MissingSessionError, NavigationSkippedError, NonRetryableError, OwnedOrInjected, parseArgument, purgeDefaultStorages, RequestHandlerError, parseRetryAfterHeader, RequestThrottledError, RequestManagerTandem, RequestQueue, RequestState, RetryRequestError, supportsDomainThrottling, Router, schemas, ServiceLocator, serviceLocator, Session, SessionError, SessionPool, Statistics, validateUserData, validators, withDirectStorageAccess, } from '@crawlee/core';
3
+ import { applyRequestTransform, AutoscaledPool, bindMethodsToServiceLocator, BLOCKED_STATUS_CODES, buildEnqueueStrategyPatterns, ConcurrencySystem, Configuration, constructUrlPatternObjects, ContextPipeline, ContextPipelineCleanupError, ContextPipelineInitializationError, ContextPipelineInterruptedError, createRequestOptions, createStorageTransaction, Request, CriticalError, currentStorageTransaction, Dataset, EnqueueStrategy, EventManager, EventType, filterRequestOptionsByPatterns, getObjectType, KeyValueStore, log, LogLevel, mergeCookies, MissingSessionError, NavigationSkippedError, NonRetryableError, OwnedOrInjected, parseArgument, purgeDefaultStorages, RequestHandlerError, parseRetryAfterHeader, RequestThrottledError, RequestManagerTandem, RequestQueue, RequestState, RetryRequestError, supportsDomainThrottling, Router, schemas, ServiceLocator, serviceLocator, Session, SessionError, SessionPool, Statistics, ThrottlingRequestManager, validateUserData, validators, withDirectStorageAccess, } from '@crawlee/core';
4
4
  import { BaseHttpClient, FetchHttpClient } from '@crawlee/http-client';
5
5
  import { isAsyncIterable, isIterable, ROTATE_PROXY_ERRORS } from '@crawlee/utils/internal';
6
6
  import { RobotsTxtFile } from '@crawlee/utils';
@@ -198,8 +198,7 @@ export class BasicCrawler {
198
198
  internalTimeoutMillis;
199
199
  maxRequestRetries;
200
200
  maxCrawlDepth;
201
- #sameDomainDelayMillis;
202
- #domainAccessedTime;
201
+ #sameDomainDelaySecs;
203
202
  maxRequestsPerCrawl;
204
203
  get handledRequestsCount() {
205
204
  return this.stats.state.requestsFinished + this.stats.state.requestsFailed;
@@ -335,6 +334,12 @@ export class BasicCrawler {
335
334
  if (requestList !== undefined || requestQueue !== undefined) {
336
335
  throw new Error('The `requestManager` option cannot be used in conjunction with `requestList` and/or `requestQueue`');
337
336
  }
337
+ // Both would pace the same domains, from different keys and with no idea of one another.
338
+ if (sameDomainDelaySecs > 0 && supportsDomainThrottling(requestManager)) {
339
+ throw new Error('The `sameDomainDelaySecs` option cannot be combined with a `requestManager` that throttles ' +
340
+ 'per domain on its own. Configure the delay on the manager instead, via the ' +
341
+ '`minCrawlDelaySecs` option of `ThrottlingRequestManager`.');
342
+ }
338
343
  this.requestManager = requestManager;
339
344
  }
340
345
  else if (requestList !== undefined && requestQueue !== undefined) {
@@ -355,7 +360,6 @@ export class BasicCrawler {
355
360
  this.proxyConfiguration = proxyConfiguration;
356
361
  this.#statusMessageLoggingInterval = statusMessageLoggingInterval;
357
362
  this.#statusMessageCallback = statusMessageCallback;
358
- this.#domainAccessedTime = new Map();
359
363
  this.#robotsTxtFileCache = new LruCache({ maxLength: 1000 });
360
364
  this.handleSkippedRequest = this.handleSkippedRequest.bind(this);
361
365
  this.additionalHttpErrorStatusCodes = new Set([...additionalHttpErrorStatusCodes]);
@@ -382,7 +386,7 @@ export class BasicCrawler {
382
386
  Math.max(this.requestHandlerTimeoutMillis * 2, 300e3);
383
387
  this.maxRequestRetries = maxRequestRetries;
384
388
  this.maxCrawlDepth = maxCrawlDepth;
385
- this.#sameDomainDelayMillis = sameDomainDelaySecs * 1000;
389
+ this.#sameDomainDelaySecs = sameDomainDelaySecs;
386
390
  this.#statsDep = OwnedOrInjected.resolve(statistics, () => new Statistics({
387
391
  logMessage: `${this.constructor.name} request statistics:`,
388
392
  log: this.log,
@@ -422,7 +426,7 @@ export class BasicCrawler {
422
426
  if (!source)
423
427
  throw new Error('Request provider is not initialized!');
424
428
  const request = await this.resolveRequest();
425
- if (!request || this.delayRequest(request, source)) {
429
+ if (!request) {
426
430
  return;
427
431
  }
428
432
  // Started here, rather than in `handleRequest`, so that a failure during context pipeline
@@ -767,8 +771,14 @@ export class BasicCrawler {
767
771
  // When `purgeRequestQueue` is explicitly `false`, nothing is purged.
768
772
  const shouldPurge = purgeRequestQueue !== false;
769
773
  const managerToPurge = this.#ownedRequestQueue.maybeValue ?? (purgeRequestQueue === true ? this.requestManager : undefined);
770
- if (managerToPurge?.purge && shouldPurge) {
771
- await managerToPurge.purge();
774
+ if (shouldPurge) {
775
+ await managerToPurge?.purge?.();
776
+ // The per-domain queues a `sameDomainDelaySecs` wrapper created are the crawler's own, whatever
777
+ // sits underneath them - so they are emptied even when the manager they wrap is spared. Purging
778
+ // the wrapper itself has already covered them.
779
+ if (this.requestManager instanceof ThrottlingRequestManager && managerToPurge !== this.requestManager) {
780
+ await this.requestManager.purgeDomainQueues();
781
+ }
772
782
  }
773
783
  // A supplied statistics instance keeps whatever state it was handed - only wipe a default we built.
774
784
  await this.#statsDep.ifOwned(async (stats) => {
@@ -906,6 +916,19 @@ export class BasicCrawler {
906
916
  if (!this.requestManager) {
907
917
  this.requestManager = await this.openOwnedRequestQueue();
908
918
  }
919
+ // Wrapped here rather than in the constructor, because the manager being wrapped may only be opened at
920
+ // this point - and because everything that enqueues goes through here first, so nothing slips past the
921
+ // wrapper into the queue it hides.
922
+ if (this.#sameDomainDelaySecs > 0 && !supportsDomainThrottling(this.requestManager)) {
923
+ this.requestManager = new ThrottlingRequestManager({
924
+ inner: this.requestManager,
925
+ domains: 'all',
926
+ minCrawlDelaySecs: this.#sameDomainDelaySecs,
927
+ // What `sameDomainDelaySecs` has always meant: one clock for a site, subdomains included.
928
+ throttleBy: 'registrableDomain',
929
+ persistStateKey: `CRAWLEE_THROTTLED_DOMAINS_${this.identity.id}`,
930
+ });
931
+ }
909
932
  // Apply the processing-time hint here (an async lifecycle point) rather than in the constructor,
910
933
  // now that `setExpectedRequestProcessingTimeSecs` is async. The hint is raise-only and idempotent,
911
934
  // but guard so we do not re-issue it on every call.
@@ -1431,30 +1454,6 @@ export class BasicCrawler {
1431
1454
  }
1432
1455
  return this.requestManager.fetchNextRequest();
1433
1456
  }
1434
- /**
1435
- * Delays processing of the request based on the `sameDomainDelaySecs` option,
1436
- * adding it back to the queue after the timeout passes. Returns `true` if the request
1437
- * should be ignored and will be reclaimed to the queue once ready.
1438
- */
1439
- delayRequest(request, source) {
1440
- const domain = getDomain(request.url);
1441
- if (!domain || !request) {
1442
- return false;
1443
- }
1444
- const now = Date.now();
1445
- const lastAccessTime = this.#domainAccessedTime.get(domain);
1446
- if (!lastAccessTime || now - lastAccessTime >= this.#sameDomainDelayMillis) {
1447
- this.#domainAccessedTime.set(domain, now);
1448
- return false;
1449
- }
1450
- const delay = lastAccessTime + this.#sameDomainDelayMillis - now;
1451
- this.log.debug(`Request ${request.url} (${request.id}) will be reclaimed after ${delay} milliseconds due to same domain delay`);
1452
- setTimeout(async () => {
1453
- this.log.debug(`Adding request ${request.url} (${request.id}) back to the queue`);
1454
- await source.reclaimRequest(request, { forefront: request.userData?.__crawlee?.forefront });
1455
- }, delay);
1456
- return true;
1457
- }
1458
1457
  /** Handles a single request - runs the request handler with retries, error handling, and lifecycle management. */
1459
1458
  async handleRequest(crawlingContext, requestSource, request) {
1460
1459
  // An earlier phase we cannot cancel (e.g. a slow `extendContext`) may have run past the internal timeout,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crawlee/basic",
3
- "version": "4.0.0-beta.123",
3
+ "version": "4.0.0-beta.124",
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": "^2.0.0",
43
43
  "@apify/timeout": "^0.4.4",
44
44
  "@apify/utilities": "^2.15.5",
45
- "@crawlee/core": "4.0.0-beta.123",
46
- "@crawlee/http-client": "4.0.0-beta.123",
47
- "@crawlee/types": "4.0.0-beta.123",
48
- "@crawlee/utils": "4.0.0-beta.123",
45
+ "@crawlee/core": "4.0.0-beta.124",
46
+ "@crawlee/http-client": "4.0.0-beta.124",
47
+ "@crawlee/types": "4.0.0-beta.124",
48
+ "@crawlee/utils": "4.0.0-beta.124",
49
49
  "csv-stringify": "^6.5.2",
50
50
  "tldts": "^7.0.6",
51
51
  "tslib": "^2.8.1",
@@ -53,7 +53,7 @@
53
53
  "zod": "^4.4.3"
54
54
  },
55
55
  "optionalDependencies": {
56
- "@crawlee/impit-client": "^4.0.0-beta.123"
56
+ "@crawlee/impit-client": "^4.0.0-beta.124"
57
57
  },
58
58
  "lerna": {
59
59
  "command": {
@@ -62,5 +62,5 @@
62
62
  }
63
63
  }
64
64
  },
65
- "gitHead": "f77648095c6a3f5ed8815c7620ea765db430ae44"
65
+ "gitHead": "0694ee1b94c755b98141671baa93cc363f2bf8e3"
66
66
  }