@crawlee/basic 4.0.0-beta.211 → 4.0.0-beta.212

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.
@@ -441,7 +441,6 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
441
441
  get log(): CrawleeLogger;
442
442
  protected readonly requestHandler: RequestHandler<ExtendedContext>;
443
443
  protected readonly internalTimeoutMillis: number;
444
- private get handledRequestsCount();
445
444
  protected blockedStatusCodes: Set<number>;
446
445
  protected readonly additionalHttpErrorStatusCodes: Set<number>;
447
446
  protected readonly httpClient: BaseHttpClient;
@@ -514,22 +513,6 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
514
513
  * @returns `true` if the status code is considered an error, `false` otherwise
515
514
  */
516
515
  protected isErrorStatusCode(status: number): boolean;
517
- /**
518
- * Builds the basic context pipeline that transforms `{ request }` into a full `CrawlingContext`.
519
- * This handles base context creation, session resolution, and context helpers.
520
- */
521
- private buildBasicContextPipeline;
522
- private checkRobotsTxt;
523
- private createBaseContext;
524
- /**
525
- * Fetches the next request and rebuilds it as a {@link CrawlingRequest}. A copy rather than the manager's
526
- * instance: managers key on `uniqueKey`/`id`, and it is this copy that flows back into `reclaimRequest` /
527
- * `markRequestAsHandled`, so nothing observes the difference.
528
- */
529
- private resolveRequest;
530
- private resolveSession;
531
- private createContextHelpers;
532
- private buildFinalContextPipeline;
533
516
  /**
534
517
  * Checks if the given error is a proxy error by comparing its message to a list of known proxy error messages.
535
518
  * Used for retrying requests that failed due to proxy errors.
@@ -547,7 +530,6 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
547
530
  * their status-reporting backend (e.g. the Apify platform).
548
531
  */
549
532
  setStatusMessage(message: string, options?: SetStatusMessageOptions): void;
550
- private getPeriodicLogger;
551
533
  /**
552
534
  * Runs the crawler. Returns a promise that resolves once every request has been processed and the crawler's
553
535
  * finished-check ({@link BasicCrawlerOptions.taskLoopOptions|`taskLoopOptions.isFinishedFunction`}, or the
@@ -596,26 +578,6 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
596
578
  * crawler's request manager, which is no longer guaranteed to be a {@link RequestQueue}.
597
579
  */
598
580
  getRequestQueue(): Promise<IRequestManager>;
599
- /**
600
- * Opens the default {@link RequestQueue} — the crawler's own, read from when the caller supplied nothing.
601
- */
602
- private openOwnedRequestQueue;
603
- /**
604
- * Tells a request manager how long we expect to hold a fetched request, so that one backed by a
605
- * locking storage backend keeps it reserved for slightly longer than the request handler timeout
606
- * (with some padding for overhead), but never for less than a minute. This prevents a long-running
607
- * request from being handed out a second time while it is still being processed — and it works
608
- * regardless of whether the manager is a plain {@link RequestQueue} or a `RequestManagerTandem`.
609
- */
610
- private applyRequestManagerTimeouts;
611
- /**
612
- * Validates a request source's `userData` against the {@link RouteSchemas|Standard Schema} registered
613
- * for its label on the crawler's schema-router (if any), throwing a {@link RequestValidationError} on
614
- * mismatch. A no-op when the user's request handler is not a schema-router, or no schema is registered for
615
- * the request's label. Applied by the crawler on the add paths it owns — `crawler.addRequests`,
616
- * `crawler.run`, `context.addRequests` and `context.enqueueLinks`.
617
- */
618
- private validateRequestUserData;
619
581
  useState<State extends Dictionary = Dictionary>(defaultValue?: State): Promise<State>;
620
582
  /**
621
583
  * Adds requests to the queue in batches. By default, it will resolve after the initial batch is added, and continue
@@ -661,26 +623,6 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
661
623
  * browser crawlers override it with their `navigationTimeoutSecs`.
662
624
  */
663
625
  protected getNavigationTimeoutMillis(): number;
664
- /**
665
- * Races the request against the internal timeout (see {@link raceWithTimeout}), sized to outlast the phases
666
- * that have their own timeout - the navigation, its hooks, and the request handler - so a legitimately slow
667
- * request, a per-route override, or a low `CRAWLEE_INTERNAL_TIMEOUT` is not cut short mid-phase. It takes
668
- * whichever is larger: the configured internal timeout, or this request's combined phase budget.
669
- */
670
- private withRequestTimeout;
671
- /**
672
- * The request handler timeout for a request with the given route label. A router route may override the
673
- * crawler's own `requestHandlerTimeoutSecs`; anything else falls back to `fallbackMillis`.
674
- *
675
- * @param label The request's route label, or `undefined` for the default route / no specific request.
676
- * @param fallbackMillis Timeout to use when no route overrides it.
677
- */
678
- private resolveRequestHandlerTimeoutMillis;
679
- /**
680
- * The timeout the router route with the given label asked for, or `undefined` when it did not override one
681
- * (or the request handler is not a router at all).
682
- */
683
- private getRouteTimeoutMillis;
684
626
  protected runRequestHandler(crawlingContext: ExtendedContext): Promise<void>;
685
627
  /**
686
628
  * Runs `callback` inside a {@link StorageTransaction}, unless transactional storage is disabled.
@@ -692,7 +634,6 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
692
634
  * Handles blocked request
693
635
  */
694
636
  protected throwOnBlockedRequest(statusCode: number): void;
695
- private isAllowedBasedOnRobotsTxtFile;
696
637
  /**
697
638
  * Records an HTTP 429 against the URL's domain so the request manager can hold the retry back.
698
639
  *
@@ -701,15 +642,8 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
701
642
  * {@link RequestThrottledError} rather than treating the response as a blocked session.
702
643
  */
703
644
  protected recordDomainRateLimit(url: string, retryAfterHeader?: string | null): boolean;
704
- /** Hands a robots.txt `Crawl-delay` to the request manager, warning if it will not be honoured. */
705
- private applyCrawlDelay;
706
645
  /** @internal */
707
646
  protected getRobotsTxtFileForUrl(url: string): Promise<RobotsTxtFile | undefined>;
708
- private pauseOnMigration;
709
- /**
710
- * Fetches the next request to process from the underlying request provider.
711
- */
712
- private fetchNextRequest;
713
647
  /** Runs the request handler and marks the request as handled. Failures are left to `handleRequestFailure`. */
714
648
  private handleRequest;
715
649
  /**
@@ -717,21 +651,6 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
717
651
  * @internal
718
652
  */
719
653
  protected addCrawlDepthRequestGenerator(requests: RequestsLike, newRequestDepth: number): AsyncGenerator<Source, void, undefined>;
720
- /**
721
- * Run async callback with given timeout and retry. Returns the result of the callback.
722
- * @ignore
723
- */
724
- private timeoutAndRetry;
725
- /**
726
- * Whether the request manager has a request ready for processing. A manager that is only `waiting` also gets a
727
- * wake-up scheduled, so a paced crawl resumes on its clock rather than on the task loop's polling interval.
728
- */
729
- private isTaskReadyFunction;
730
- /**
731
- * Unwraps errors thrown by the context pipeline to get the actual user error.
732
- * RequestHandlerError, ContextPipelineInitializationError and ContextPipelineCleanupError wrap the actual error.
733
- */
734
- private unwrapError;
735
654
  /**
736
655
  * Handles errors thrown by user provided requestHandler()
737
656
  *
@@ -745,12 +664,6 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
745
664
  * @returns The message to be logged
746
665
  */
747
666
  protected getMessageFromError(error: Error, forceStack?: boolean): string;
748
- /**
749
- * Whether the session should be spared for this error - either because it was already retired, or because the
750
- * failure says nothing about the session (a rate limit is a property of the domain).
751
- */
752
- private errorAbsolvesSession;
753
- private canRequestBeRetried;
754
667
  /**
755
668
  * Ends the run in progress immediately, without waiting for the requests already in flight.
756
669
  *
@@ -769,7 +682,6 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
769
682
  destroy(): Promise<void>;
770
683
  [Symbol.asyncDispose](): Promise<void>;
771
684
  protected getCookieHeaderFromRequest(request: Request): string;
772
- private requestMatchesEnqueueStrategy;
773
685
  }
774
686
  export interface CrawlerAddRequestsOptions extends AddRequestsBatchedOptions, EnqueueUrlsOptions {
775
687
  }
@@ -184,14 +184,14 @@ export class BasicCrawler {
184
184
  * Context built with this pipeline can be passed into multiple crawler pipelines at once.
185
185
  */
186
186
  get #basicPipeline() {
187
- this.#basicContextPipeline ??= this.buildBasicContextPipeline();
187
+ this.#basicContextPipeline ??= this.#buildBasicContextPipeline();
188
188
  return this.#basicContextPipeline;
189
189
  }
190
190
  #contextPipeline;
191
191
  /** @internal */
192
192
  get contextPipeline() {
193
193
  if (this.#contextPipeline === undefined) {
194
- this.#contextPipeline = this.buildFinalContextPipeline();
194
+ this.#contextPipeline = this.#buildFinalContextPipeline();
195
195
  }
196
196
  return this.#contextPipeline;
197
197
  }
@@ -212,7 +212,7 @@ export class BasicCrawler {
212
212
  #maxRequestRetries;
213
213
  #maxCrawlDepth;
214
214
  #maxRequestsPerCrawl;
215
- get handledRequestsCount() {
215
+ get #handledRequestsCount() {
216
216
  return this.statistics.state.requestsSucceeded + this.statistics.state.requestsFailed;
217
217
  }
218
218
  #statusMessageLoggingInterval;
@@ -370,12 +370,12 @@ export class BasicCrawler {
370
370
  throttleBy: 'registrableDomain',
371
371
  persistStateKey: `CRAWLEE_THROTTLED_DOMAINS_${this.#identity.id}`,
372
372
  // A factory, because the default queue is only opened on first use.
373
- inner: suppliedManager ?? (() => this.openOwnedRequestQueue()),
373
+ inner: suppliedManager ?? (() => this.#openOwnedRequestQueue()),
374
374
  })
375
375
  : suppliedManager;
376
376
  if (requestList !== undefined) {
377
377
  // The list is read first, while new requests still have somewhere writable to go.
378
- this.#requestManager = new RequestManagerTandem(requestList, writableManager ?? (() => this.openOwnedRequestQueue()));
378
+ this.#requestManager = new RequestManagerTandem(requestList, writableManager ?? (() => this.#openOwnedRequestQueue()));
379
379
  }
380
380
  else if (writableManager !== undefined) {
381
381
  // A RequestQueue is itself a request manager.
@@ -440,7 +440,7 @@ export class BasicCrawler {
440
440
  }
441
441
  this.internalTimeoutMillis = Math.min(this.internalTimeoutMillis, maxSignedInteger);
442
442
  this.#maxRequestsPerCrawl = maxRequestsPerCrawl;
443
- const isMaxPagesExceeded = () => this.#maxRequestsPerCrawl && this.#maxRequestsPerCrawl <= this.handledRequestsCount;
443
+ const isMaxPagesExceeded = () => this.#maxRequestsPerCrawl && this.#maxRequestsPerCrawl <= this.#handledRequestsCount;
444
444
  // eslint-disable-next-line prefer-const
445
445
  let { isFinishedFunction, isTaskReadyFunction } = taskLoopOptions;
446
446
  // override even if `isFinishedFunction` provided by user - `keepAlive` has higher priority
@@ -452,7 +452,7 @@ export class BasicCrawler {
452
452
  const source = this.requestManager;
453
453
  if (!source)
454
454
  throw new Error('Request provider is not initialized!');
455
- const request = await this.resolveRequest();
455
+ const request = await this.#resolveRequest();
456
456
  if (!request) {
457
457
  return;
458
458
  }
@@ -469,20 +469,20 @@ export class BasicCrawler {
469
469
  // Navigation, the navigation hooks and the request handler are timed individually, but the
470
470
  // phases between them are not, so a request could still get stuck indefinitely. This is the
471
471
  // catch-all for that - see `raceWithTimeout` for why it is a bare timer, not a timeout frame.
472
- await this.withRequestTimeout(crawlingContext, this.#basicPipeline.chain(this.contextPipeline).call(crawlingContext, (ctx) => this.handleRequest(ctx, source, request), (error) => this.#handleRequestFailure(error, crawlingContext, source, request))));
472
+ await this.#withRequestTimeout(crawlingContext, this.#basicPipeline.chain(this.contextPipeline).call(crawlingContext, (ctx) => this.handleRequest(ctx, source, request), (error) => this.#handleRequestFailure(error, crawlingContext, source, request))));
473
473
  }
474
474
  catch (error) {
475
475
  // The internal timeout above fired, or `handleRequestFailure` threw a `SessionError` - both
476
476
  // take the normal error flow. Anything else is fatal.
477
477
  if (error instanceof SessionError || crawlingContext[timeoutExpiredKey]?.()) {
478
- const unwrappedError = this.unwrapError(error);
478
+ const unwrappedError = this.#unwrapError(error);
479
479
  await this.requestFunctionErrorHandler(unwrappedError, crawlingContext, request, this.requestManager);
480
- if (!this.errorAbsolvesSession(unwrappedError)) {
480
+ if (!this.#errorAbsolvesSession(unwrappedError)) {
481
481
  crawlingContext.session?.markBad();
482
482
  }
483
483
  return;
484
484
  }
485
- throw this.unwrapError(error);
485
+ throw this.#unwrapError(error);
486
486
  }
487
487
  },
488
488
  isTaskReadyFunction: async () => {
@@ -496,13 +496,13 @@ export class BasicCrawler {
496
496
  'Ongoing requests will be allowed to complete.');
497
497
  return false;
498
498
  }
499
- return isTaskReadyFunction ? await isTaskReadyFunction() : await this.isTaskReadyFunction();
499
+ return isTaskReadyFunction ? await isTaskReadyFunction() : await this.#isTaskReadyFunction();
500
500
  },
501
501
  isFinishedFunction: async () => {
502
502
  if (isMaxPagesExceeded()) {
503
503
  this.log.info(`Earlier, the crawler reached the maxRequestsPerCrawl limit of ${this.#maxRequestsPerCrawl} requests ` +
504
504
  'and all requests that were in progress at that time have now finished. ' +
505
- `In total, the crawler processed ${this.handledRequestsCount} requests and will shut down.`);
505
+ `In total, the crawler processed ${this.#handledRequestsCount} requests and will shut down.`);
506
506
  return true;
507
507
  }
508
508
  if (this.#run?.stopRequested) {
@@ -571,15 +571,15 @@ export class BasicCrawler {
571
571
  * Builds the basic context pipeline that transforms `{ request }` into a full `CrawlingContext`.
572
572
  * This handles base context creation, session resolution, and context helpers.
573
573
  */
574
- buildBasicContextPipeline() {
574
+ #buildBasicContextPipeline() {
575
575
  return ContextPipeline.create()
576
- .compose(this.checkRobotsTxt.bind(this))
577
- .compose(this.createBaseContext.bind(this))
578
- .compose(this.resolveSession.bind(this))
579
- .compose(this.createContextHelpers.bind(this));
576
+ .compose(this.#checkRobotsTxt.bind(this))
577
+ .compose(this.#createBaseContext.bind(this))
578
+ .compose(this.#resolveSession.bind(this))
579
+ .compose(this.#createContextHelpers.bind(this));
580
580
  }
581
- async checkRobotsTxt({ request }) {
582
- if (!(await this.isAllowedBasedOnRobotsTxtFile(request.url))) {
581
+ async #checkRobotsTxt({ request }) {
582
+ if (!(await this.#isAllowedBasedOnRobotsTxtFile(request.url))) {
583
583
  this.log.warning(`Skipping request ${request.url} (${request.id}) because it is disallowed based on robots.txt`);
584
584
  request.state = RequestState.SKIPPED;
585
585
  request.noRetry = true;
@@ -591,7 +591,7 @@ export class BasicCrawler {
591
591
  }
592
592
  return {};
593
593
  }
594
- createBaseContext(context, onCleanup) {
594
+ #createBaseContext(context, onCleanup) {
595
595
  return {
596
596
  id: cryptoRandomObjectId(10),
597
597
  log: this.log,
@@ -640,8 +640,8 @@ export class BasicCrawler {
640
640
  * instance: managers key on `uniqueKey`/`id`, and it is this copy that flows back into `reclaimRequest` /
641
641
  * `markRequestAsHandled`, so nothing observes the difference.
642
642
  */
643
- async resolveRequest() {
644
- const fetched = await this.timeoutAndRetry(this.fetchNextRequest.bind(this), this.internalTimeoutMillis, `Fetching next request timed out after ${this.internalTimeoutMillis / 1e3} seconds.`);
643
+ async #resolveRequest() {
644
+ const fetched = await this.#timeoutAndRetry(this.#fetchNextRequest.bind(this), this.internalTimeoutMillis, `Fetching next request timed out after ${this.internalTimeoutMillis / 1e3} seconds.`);
645
645
  if (!fetched) {
646
646
  return null;
647
647
  }
@@ -650,8 +650,8 @@ export class BasicCrawler {
650
650
  request.loadedUrl = undefined;
651
651
  return request;
652
652
  }
653
- async resolveSession({ request }) {
654
- const session = await this.timeoutAndRetry(async () => {
653
+ async #resolveSession({ request }) {
654
+ const session = await this.#timeoutAndRetry(async () => {
655
655
  const existingSession = await this.sessionPool.getSession(request.sessionId);
656
656
  if (!existingSession) {
657
657
  throw new MissingSessionError(request.sessionId);
@@ -660,7 +660,7 @@ export class BasicCrawler {
660
660
  }, this.internalTimeoutMillis, `Fetching session timed out after ${this.internalTimeoutMillis / 1e3} seconds.`);
661
661
  return { session, proxyInfo: session?.proxyInfo };
662
662
  }
663
- async createContextHelpers({ request, session }) {
663
+ async #createContextHelpers({ request, session }) {
664
664
  const addRequests = async (requests, options = {}) => {
665
665
  const newCrawlDepth = request.crawlDepth + 1;
666
666
  const requestsGenerator = this.addCrawlDepthRequestGenerator(requests, newCrawlDepth);
@@ -669,7 +669,7 @@ export class BasicCrawler {
669
669
  const sendRequest = createSendRequest(this.httpClient, request, session);
670
670
  return { addRequests, sendRequest };
671
671
  }
672
- buildFinalContextPipeline() {
672
+ #buildFinalContextPipeline() {
673
673
  const subclassPipeline = (this.#contextPipelineOptions.contextPipelineBuilder?.() ??
674
674
  ContextPipeline.create());
675
675
  // `extendContext` runs *before* the subclass navigation pipeline (which includes the
@@ -694,7 +694,7 @@ export class BasicCrawler {
694
694
  }
695
695
  contextPipeline = contextPipeline.compose(async (context) => {
696
696
  const { request } = context;
697
- if (request && !this.requestMatchesEnqueueStrategy(request)) {
697
+ if (request && !this.#requestMatchesEnqueueStrategy(request)) {
698
698
  // eslint-disable-next-line dot-notation
699
699
  const message = `Skipping request ${request.id} (starting url: ${request.url} -> loaded url: ${request.loadedUrl}) because it does not match the enqueue strategy (${request['enqueueStrategy']}).`;
700
700
  this.log.debug(message);
@@ -741,7 +741,7 @@ export class BasicCrawler {
741
741
  level: options.level,
742
742
  });
743
743
  }
744
- getPeriodicLogger() {
744
+ #getPeriodicLogger() {
745
745
  let previousState = { ...this.statistics.state };
746
746
  const getOperationMode = () => {
747
747
  const { requestsFailed } = this.statistics.state;
@@ -846,15 +846,15 @@ export class BasicCrawler {
846
846
  await this.#concurrencySystem.ifOwned((system) => system.stop());
847
847
  throw error;
848
848
  }
849
- const periodicLogger = this.getPeriodicLogger();
849
+ const periodicLogger = this.#getPeriodicLogger();
850
850
  this.setStatusMessage('Starting the crawler.', { level: 'INFO' });
851
851
  const sigintHandler = async () => {
852
852
  this.log.warning('Pausing... Press CTRL+C again to force exit. To resume, do: CRAWLEE_PURGE_ON_START=0 npm start');
853
- await this.pauseOnMigration();
853
+ await this.#pauseOnMigration();
854
854
  await run.abort();
855
855
  };
856
856
  // Attach a listener to handle migration and aborting events gracefully.
857
- const boundPauseOnMigration = this.pauseOnMigration.bind(this);
857
+ const boundPauseOnMigration = this.#pauseOnMigration.bind(this);
858
858
  process.once('SIGINT', sigintHandler);
859
859
  const eventManager = serviceLocator.getEventManager();
860
860
  eventManager.on(EventType.MIGRATING, boundPauseOnMigration);
@@ -970,14 +970,14 @@ export class BasicCrawler {
970
970
  */
971
971
  async getRequestManager() {
972
972
  if (!this.#requestManager) {
973
- this.#requestManager = await this.openOwnedRequestQueue();
973
+ this.#requestManager = await this.#openOwnedRequestQueue();
974
974
  }
975
975
  // Apply the processing-time hint here (an async lifecycle point) rather than in the constructor,
976
976
  // now that `setExpectedRequestProcessingTimeSecs` is async. The hint is raise-only and idempotent,
977
977
  // but guard so we do not re-issue it on every call.
978
978
  if (!this.#requestManagerTimeoutsApplied) {
979
979
  this.#requestManagerTimeoutsApplied = true;
980
- await this.applyRequestManagerTimeouts(this.#requestManager);
980
+ await this.#applyRequestManagerTimeouts(this.#requestManager);
981
981
  }
982
982
  return this.#requestManager;
983
983
  }
@@ -991,7 +991,7 @@ export class BasicCrawler {
991
991
  /**
992
992
  * Opens the default {@link RequestQueue} — the crawler's own, read from when the caller supplied nothing.
993
993
  */
994
- async openOwnedRequestQueue() {
994
+ async #openOwnedRequestQueue() {
995
995
  // The first crawler instance uses the default queue (null identifier);
996
996
  // subsequent instances get their own queue via a unique alias so they don't collide.
997
997
  const identifier = this.#identity.instanceIndex === 0 ? null : { alias: `__default_${this.#identity.id}__` };
@@ -1004,7 +1004,7 @@ export class BasicCrawler {
1004
1004
  * request from being handed out a second time while it is still being processed — and it works
1005
1005
  * regardless of whether the manager is a plain {@link RequestQueue} or a `RequestManagerTandem`.
1006
1006
  */
1007
- async applyRequestManagerTimeouts(requestManager) {
1007
+ async #applyRequestManagerTimeouts(requestManager) {
1008
1008
  // A router route may hold a request for longer than the crawler's own timeout, and we cannot know
1009
1009
  // which routes a run will hit, so reserve for the longest one any route asked for. The hint is
1010
1010
  // raise-only, so erring high here is safe.
@@ -1019,7 +1019,7 @@ export class BasicCrawler {
1019
1019
  * the request's label. Applied by the crawler on the add paths it owns — `crawler.addRequests`,
1020
1020
  * `crawler.run`, `context.addRequests` and `context.enqueueLinks`.
1021
1021
  */
1022
- async validateRequestUserData(source) {
1022
+ async #validateRequestUserData(source) {
1023
1023
  if (typeof source === 'string') {
1024
1024
  return;
1025
1025
  }
@@ -1062,7 +1062,9 @@ export class BasicCrawler {
1062
1062
  if (this.#maxRequestsPerCrawl === undefined) {
1063
1063
  return explicitLimit;
1064
1064
  }
1065
- const limit = Math.max(0, this.#maxRequestsPerCrawl - this.handledRequestsCount - (await this.#getPendingRequestCountApproximation()));
1065
+ const limit = Math.max(0, this.#maxRequestsPerCrawl -
1066
+ this.#handledRequestsCount -
1067
+ (await this.#getPendingRequestCountApproximation()));
1066
1068
  return Math.min(limit, explicitLimit ?? Infinity);
1067
1069
  }
1068
1070
  async #handleSkippedRequest(options) {
@@ -1109,7 +1111,7 @@ export class BasicCrawler {
1109
1111
  // its own per-item validation below, and validating an absent label/userData here would spuriously
1110
1112
  // check them against a registered default-route schema.
1111
1113
  if (options.label !== undefined || options.userData !== undefined) {
1112
- await this.validateRequestUserData({ label: options.label, userData: options.userData });
1114
+ await this.#validateRequestUserData({ label: options.label, userData: options.userData });
1113
1115
  }
1114
1116
  const requestLimit = await this.#calculateEnqueuedRequestLimit(options.limit);
1115
1117
  const strategy = options.strategy ?? EnqueueStrategy.All;
@@ -1118,9 +1120,9 @@ export class BasicCrawler {
1118
1120
  // The strategy always applies, even when `include` patterns are provided - the two are AND-ed together
1119
1121
  // (a URL must match an `include` pattern *and* satisfy the strategy). This mirrors crawlee-python.
1120
1122
  const enqueueStrategyPatterns = options.baseUrl ? buildEnqueueStrategyPatterns(options.baseUrl, strategy) : [];
1121
- const isAllowedBasedOnRobotsTxtFile = this.isAllowedBasedOnRobotsTxtFile.bind(this);
1123
+ const isAllowedBasedOnRobotsTxtFile = this.#isAllowedBasedOnRobotsTxtFile.bind(this);
1122
1124
  const maxCrawlDepth = this.#maxCrawlDepth;
1123
- const validateRequestUserData = this.validateRequestUserData.bind(this);
1125
+ const validateRequestUserData = this.#validateRequestUserData.bind(this);
1124
1126
  const allSkipped = [];
1125
1127
  // A skipped source (which can carry arbitrary userData) is only retained if something reads it -
1126
1128
  // otherwise the URL alone is enough to build the callback argument and to log with.
@@ -1292,7 +1294,7 @@ export class BasicCrawler {
1292
1294
  // floored per request so it will not actually cut them short, but the configured value is then effectively
1293
1295
  // ignored, which is worth flagging. Checked here (not in the constructor) because a subclass sets its
1294
1296
  // navigation timeout only after `super()`.
1295
- const phasesMillis = this.getNavigationTimeoutMillis() + this.resolveRequestHandlerTimeoutMillis(undefined);
1297
+ const phasesMillis = this.getNavigationTimeoutMillis() + this.#resolveRequestHandlerTimeoutMillis(undefined);
1296
1298
  if (this.internalTimeoutMillis < phasesMillis) {
1297
1299
  this.log.warning(`CRAWLEE_INTERNAL_TIMEOUT (${this.internalTimeoutMillis / 1000}s) is shorter than the navigation ` +
1298
1300
  `and request handler timeouts combined (${phasesMillis / 1000}s); it will be raised per request ` +
@@ -1314,9 +1316,9 @@ export class BasicCrawler {
1314
1316
  * request, a per-route override, or a low `CRAWLEE_INTERNAL_TIMEOUT` is not cut short mid-phase. It takes
1315
1317
  * whichever is larger: the configured internal timeout, or this request's combined phase budget.
1316
1318
  */
1317
- async withRequestTimeout(crawlingContext, work) {
1319
+ async #withRequestTimeout(crawlingContext, work) {
1318
1320
  const { request } = crawlingContext;
1319
- const phasesMillis = this.getNavigationTimeoutMillis() + this.resolveRequestHandlerTimeoutMillis(request.label);
1321
+ const phasesMillis = this.getNavigationTimeoutMillis() + this.#resolveRequestHandlerTimeoutMillis(request.label);
1320
1322
  const timeoutMillis = Math.max(this.internalTimeoutMillis, phasesMillis);
1321
1323
  await raceWithTimeout(crawlingContext, work, { timeoutMillis, requestId: request.id });
1322
1324
  }
@@ -1327,14 +1329,14 @@ export class BasicCrawler {
1327
1329
  * @param label The request's route label, or `undefined` for the default route / no specific request.
1328
1330
  * @param fallbackMillis Timeout to use when no route overrides it.
1329
1331
  */
1330
- resolveRequestHandlerTimeoutMillis(label, fallbackMillis = this.#requestHandlerTimeoutMillis) {
1331
- return this.getRouteTimeoutMillis(label) ?? fallbackMillis;
1332
+ #resolveRequestHandlerTimeoutMillis(label, fallbackMillis = this.#requestHandlerTimeoutMillis) {
1333
+ return this.#getRouteTimeoutMillis(label) ?? fallbackMillis;
1332
1334
  }
1333
1335
  /**
1334
1336
  * The timeout the router route with the given label asked for, or `undefined` when it did not override one
1335
1337
  * (or the request handler is not a router at all).
1336
1338
  */
1337
- getRouteTimeoutMillis(label) {
1339
+ #getRouteTimeoutMillis(label) {
1338
1340
  const getTimeoutSecs = this.requestHandler.getTimeoutSecs;
1339
1341
  if (typeof getTimeoutSecs !== 'function') {
1340
1342
  return undefined;
@@ -1343,7 +1345,7 @@ export class BasicCrawler {
1343
1345
  return timeoutSecs === undefined ? undefined : timeoutSecs * 1000;
1344
1346
  }
1345
1347
  async runRequestHandler(crawlingContext) {
1346
- const timeoutMillis = this.resolveRequestHandlerTimeoutMillis(crawlingContext.request.label);
1348
+ const timeoutMillis = this.#resolveRequestHandlerTimeoutMillis(crawlingContext.request.label);
1347
1349
  await addTimeoutToPromise(async () => this.requestHandler(crawlingContext), timeoutMillis, `requestHandler timed out after ${timeoutMillis / 1000} seconds (${crawlingContext.request.id}).`);
1348
1350
  }
1349
1351
  /**
@@ -1351,6 +1353,7 @@ export class BasicCrawler {
1351
1353
  * Deliberately does **not** commit on return - the pipeline hands failures to `handleRequestFailure`, so a
1352
1354
  * normal return says nothing about success. `handleRequest` commits on success.
1353
1355
  */
1356
+ // oxlint-disable-next-line crawlee/prefer-private-fields -- accessed by tests
1354
1357
  async runInStorageTransaction(callback) {
1355
1358
  if (!this.#transactionalStorageEnabled) {
1356
1359
  return callback();
@@ -1390,7 +1393,7 @@ export class BasicCrawler {
1390
1393
  throw new SessionError(`Request blocked - received ${statusCode} status code.`);
1391
1394
  }
1392
1395
  }
1393
- async isAllowedBasedOnRobotsTxtFile(url) {
1396
+ async #isAllowedBasedOnRobotsTxtFile(url) {
1394
1397
  if (!this.#respectRobotsTxtFile) {
1395
1398
  return true;
1396
1399
  }
@@ -1399,7 +1402,7 @@ export class BasicCrawler {
1399
1402
  if (robotsTxtFile) {
1400
1403
  const crawlDelay = robotsTxtFile.getCrawlDelay(userAgent);
1401
1404
  if (crawlDelay !== undefined) {
1402
- this.applyCrawlDelay(url, crawlDelay);
1405
+ this.#applyCrawlDelay(url, crawlDelay);
1403
1406
  }
1404
1407
  }
1405
1408
  return !robotsTxtFile || robotsTxtFile.isAllowed(url, userAgent);
@@ -1427,7 +1430,7 @@ export class BasicCrawler {
1427
1430
  return false;
1428
1431
  }
1429
1432
  /** Hands a robots.txt `Crawl-delay` to the request manager, warning if it will not be honoured. */
1430
- applyCrawlDelay(url, delaySeconds) {
1433
+ #applyCrawlDelay(url, delaySeconds) {
1431
1434
  // robots.txt is per-origin; `hostname` is the closest pacing scope and errs wide (http and https to one
1432
1435
  // host share a clock).
1433
1436
  if (this.requestManager?.recordPacingSignal({
@@ -1464,7 +1467,7 @@ export class BasicCrawler {
1464
1467
  return undefined;
1465
1468
  }
1466
1469
  }
1467
- async pauseOnMigration() {
1470
+ async #pauseOnMigration() {
1468
1471
  const run = this.#dispatchingRun;
1469
1472
  if (run) {
1470
1473
  await run.pause(SAFE_MIGRATION_WAIT_MILLIS).catch((err) => {
@@ -1484,7 +1487,7 @@ export class BasicCrawler {
1484
1487
  /**
1485
1488
  * Fetches the next request to process from the underlying request provider.
1486
1489
  */
1487
- async fetchNextRequest() {
1490
+ async #fetchNextRequest() {
1488
1491
  if (this.requestManager === undefined) {
1489
1492
  throw new Error(`fetchNextRequest called on an uninitialized crawler`);
1490
1493
  }
@@ -1505,7 +1508,7 @@ export class BasicCrawler {
1505
1508
  // it is retried. This also closes the transaction, so everything below passes through.
1506
1509
  // Opened by `runInStorageTransaction`; absent when disabled or when the subclass opens its own.
1507
1510
  await currentStorageTransaction()?.commit();
1508
- await this.timeoutAndRetry(async () => requestSource.markRequestAsHandled(request), this.internalTimeoutMillis, `Marking request ${request.url} (${request.id}) as handled timed out after ${this.internalTimeoutMillis / 1e3} seconds.`);
1511
+ await this.#timeoutAndRetry(async () => requestSource.markRequestAsHandled(request), this.internalTimeoutMillis, `Marking request ${request.url} (${request.id}) as handled timed out after ${this.internalTimeoutMillis / 1e3} seconds.`);
1509
1512
  this.statistics.recordRequestSuccess(request.id || request.uniqueKey, request.retryCount);
1510
1513
  // reclaim session if request finishes successfully
1511
1514
  request.state = RequestState.DONE;
@@ -1522,7 +1525,7 @@ export class BasicCrawler {
1522
1525
  currentStorageTransaction()?.rollback();
1523
1526
  const skipped = rawError instanceof ContextPipelineInitializationError &&
1524
1527
  rawError.cause instanceof ContextPipelineInterruptedError;
1525
- const err = this.unwrapError(rawError);
1528
+ const err = this.#unwrapError(rawError);
1526
1529
  const context = crawlingContext;
1527
1530
  const retryCountBefore = request.retryCount;
1528
1531
  const resumeRequestTimeout = crawlingContext[suspendTimeoutKey]?.();
@@ -1531,7 +1534,7 @@ export class BasicCrawler {
1531
1534
  // (e.g., doesn't match enqueue strategy after redirect). Just return gracefully.
1532
1535
  if (skipped) {
1533
1536
  this.statistics.discardRequestRecord(request.id || request.uniqueKey);
1534
- await this.timeoutAndRetry(async () => requestSource.markRequestAsHandled(request), this.internalTimeoutMillis, `Marking request ${request.url} (${request.id}) as handled timed out after ${this.internalTimeoutMillis / 1e3} seconds.`);
1537
+ await this.#timeoutAndRetry(async () => requestSource.markRequestAsHandled(request), this.internalTimeoutMillis, `Marking request ${request.url} (${request.id}) as handled timed out after ${this.internalTimeoutMillis / 1e3} seconds.`);
1535
1538
  return;
1536
1539
  }
1537
1540
  request.state = RequestState.ERROR_HANDLER;
@@ -1539,7 +1542,7 @@ export class BasicCrawler {
1539
1542
  request.state = RequestState.DONE;
1540
1543
  }
1541
1544
  catch (secondaryError) {
1542
- const unwrappedSecondaryError = this.unwrapError(secondaryError);
1545
+ const unwrappedSecondaryError = this.#unwrapError(secondaryError);
1543
1546
  const timedOut = unwrappedSecondaryError instanceof TimeoutError;
1544
1547
  request.state = RequestState.ERROR;
1545
1548
  if (timedOut) {
@@ -1575,7 +1578,7 @@ export class BasicCrawler {
1575
1578
  }
1576
1579
  // decrease the session score if the request fails (but the error handler did not throw);
1577
1580
  // skip when the error is a SessionError, which already retired the session
1578
- if (!skipped && !this.errorAbsolvesSession(err)) {
1581
+ if (!skipped && !this.#errorAbsolvesSession(err)) {
1579
1582
  context.session?.markBad();
1580
1583
  }
1581
1584
  }
@@ -1603,7 +1606,7 @@ export class BasicCrawler {
1603
1606
  * Run async callback with given timeout and retry. Returns the result of the callback.
1604
1607
  * @ignore
1605
1608
  */
1606
- async timeoutAndRetry(handler, timeout, error, maxRetries = 3, retried = 1) {
1609
+ async #timeoutAndRetry(handler, timeout, error, maxRetries = 3, retried = 1) {
1607
1610
  try {
1608
1611
  return await addTimeoutToPromise(handler, timeout, error);
1609
1612
  }
@@ -1611,7 +1614,7 @@ export class BasicCrawler {
1611
1614
  if (retried <= maxRetries) {
1612
1615
  // we retry on any error, not just timeout
1613
1616
  this.log.warning(`${e.message} (retrying ${retried}/${maxRetries})`);
1614
- return this.timeoutAndRetry(handler, timeout, error, maxRetries, retried + 1);
1617
+ return this.#timeoutAndRetry(handler, timeout, error, maxRetries, retried + 1);
1615
1618
  }
1616
1619
  throw e;
1617
1620
  }
@@ -1620,7 +1623,7 @@ export class BasicCrawler {
1620
1623
  * Whether the request manager has a request ready for processing. A manager that is only `waiting` also gets a
1621
1624
  * wake-up scheduled, so a paced crawl resumes on its clock rather than on the task loop's polling interval.
1622
1625
  */
1623
- async isTaskReadyFunction() {
1626
+ async #isTaskReadyFunction() {
1624
1627
  if (this.requestManager === undefined) {
1625
1628
  return false;
1626
1629
  }
@@ -1634,11 +1637,11 @@ export class BasicCrawler {
1634
1637
  * Unwraps errors thrown by the context pipeline to get the actual user error.
1635
1638
  * RequestHandlerError, ContextPipelineInitializationError and ContextPipelineCleanupError wrap the actual error.
1636
1639
  */
1637
- unwrapError(error) {
1640
+ #unwrapError(error) {
1638
1641
  if (error instanceof RequestHandlerError ||
1639
1642
  error instanceof ContextPipelineInitializationError ||
1640
1643
  error instanceof ContextPipelineCleanupError) {
1641
- return this.unwrapError(error.cause);
1644
+ return this.#unwrapError(error.cause);
1642
1645
  }
1643
1646
  return error;
1644
1647
  }
@@ -1663,7 +1666,7 @@ export class BasicCrawler {
1663
1666
  if (error instanceof CriticalError) {
1664
1667
  throw error;
1665
1668
  }
1666
- const shouldRetryRequest = this.canRequestBeRetried(request, error);
1669
+ const shouldRetryRequest = this.#canRequestBeRetried(request, error);
1667
1670
  if (shouldRetryRequest) {
1668
1671
  await this.statistics.errorTrackerRetry.addAsync(error, crawlingContext);
1669
1672
  await this.#errorHandler?.(crawlingContext, // valid cast - ExtendedContext transitively extends CrawlingContext
@@ -1741,10 +1744,10 @@ export class BasicCrawler {
1741
1744
  * Whether the session should be spared for this error - either because it was already retired, or because the
1742
1745
  * failure says nothing about the session (a rate limit is a property of the domain).
1743
1746
  */
1744
- errorAbsolvesSession(error) {
1747
+ #errorAbsolvesSession(error) {
1745
1748
  return error instanceof SessionError || error instanceof RequestThrottledError;
1746
1749
  }
1747
- canRequestBeRetried(request, error) {
1750
+ #canRequestBeRetried(request, error) {
1748
1751
  // Request should never be retried, or the error encountered makes it not able to be retried.
1749
1752
  if (request.noRetry || error instanceof NonRetryableError) {
1750
1753
  return false;
@@ -1802,7 +1805,7 @@ export class BasicCrawler {
1802
1805
  }
1803
1806
  return request.headers?.Cookie || request.headers?.cookie || '';
1804
1807
  }
1805
- requestMatchesEnqueueStrategy(request) {
1808
+ #requestMatchesEnqueueStrategy(request) {
1806
1809
  // If `skipNavigation` was used, just return `true`
1807
1810
  try {
1808
1811
  // eslint-disable-next-line @typescript-eslint/no-unused-expressions