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

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.
@@ -428,7 +428,9 @@ export class BasicCrawler {
428
428
  this.#sessionPoolDep = OwnedOrInjected.resolve(sessionPool, () => new SessionPool({
429
429
  createSessionFunction: async (opts) => new Session({
430
430
  ...opts?.sessionOptions,
431
- proxyInfo: opts?.sessionOptions?.proxyInfo ?? (await this.proxyConfiguration?.newProxyInfo()),
431
+ proxyInfo: this.proxyConfiguration
432
+ ? await this.proxyConfiguration.newProxyInfo(opts?.sessionOptions?.proxyInfo)
433
+ : opts?.sessionOptions?.proxyInfo,
432
434
  }),
433
435
  }));
434
436
  this.blockedStatusCodes = new Set(blockedStatusCodesInput ?? BLOCKED_STATUS_CODES);
@@ -440,7 +442,7 @@ export class BasicCrawler {
440
442
  }
441
443
  this.internalTimeoutMillis = Math.min(this.internalTimeoutMillis, maxSignedInteger);
442
444
  this.#maxRequestsPerCrawl = maxRequestsPerCrawl;
443
- const isMaxPagesExceeded = () => this.#maxRequestsPerCrawl && this.#maxRequestsPerCrawl <= this.handledRequestsCount;
445
+ const isMaxPagesExceeded = () => this.#maxRequestsPerCrawl && this.#maxRequestsPerCrawl <= this.#handledRequestsCount;
444
446
  // eslint-disable-next-line prefer-const
445
447
  let { isFinishedFunction, isTaskReadyFunction } = taskLoopOptions;
446
448
  // override even if `isFinishedFunction` provided by user - `keepAlive` has higher priority
@@ -452,7 +454,7 @@ export class BasicCrawler {
452
454
  const source = this.requestManager;
453
455
  if (!source)
454
456
  throw new Error('Request provider is not initialized!');
455
- const request = await this.resolveRequest();
457
+ const request = await this.#resolveRequest();
456
458
  if (!request) {
457
459
  return;
458
460
  }
@@ -469,20 +471,20 @@ export class BasicCrawler {
469
471
  // Navigation, the navigation hooks and the request handler are timed individually, but the
470
472
  // phases between them are not, so a request could still get stuck indefinitely. This is the
471
473
  // 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))));
474
+ 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
475
  }
474
476
  catch (error) {
475
477
  // The internal timeout above fired, or `handleRequestFailure` threw a `SessionError` - both
476
478
  // take the normal error flow. Anything else is fatal.
477
479
  if (error instanceof SessionError || crawlingContext[timeoutExpiredKey]?.()) {
478
- const unwrappedError = this.unwrapError(error);
480
+ const unwrappedError = this.#unwrapError(error);
479
481
  await this.requestFunctionErrorHandler(unwrappedError, crawlingContext, request, this.requestManager);
480
- if (!this.errorAbsolvesSession(unwrappedError)) {
482
+ if (!this.#errorAbsolvesSession(unwrappedError)) {
481
483
  crawlingContext.session?.markBad();
482
484
  }
483
485
  return;
484
486
  }
485
- throw this.unwrapError(error);
487
+ throw this.#unwrapError(error);
486
488
  }
487
489
  },
488
490
  isTaskReadyFunction: async () => {
@@ -496,13 +498,13 @@ export class BasicCrawler {
496
498
  'Ongoing requests will be allowed to complete.');
497
499
  return false;
498
500
  }
499
- return isTaskReadyFunction ? await isTaskReadyFunction() : await this.isTaskReadyFunction();
501
+ return isTaskReadyFunction ? await isTaskReadyFunction() : await this.#isTaskReadyFunction();
500
502
  },
501
503
  isFinishedFunction: async () => {
502
504
  if (isMaxPagesExceeded()) {
503
505
  this.log.info(`Earlier, the crawler reached the maxRequestsPerCrawl limit of ${this.#maxRequestsPerCrawl} requests ` +
504
506
  '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.`);
507
+ `In total, the crawler processed ${this.#handledRequestsCount} requests and will shut down.`);
506
508
  return true;
507
509
  }
508
510
  if (this.#run?.stopRequested) {
@@ -571,15 +573,15 @@ export class BasicCrawler {
571
573
  * Builds the basic context pipeline that transforms `{ request }` into a full `CrawlingContext`.
572
574
  * This handles base context creation, session resolution, and context helpers.
573
575
  */
574
- buildBasicContextPipeline() {
576
+ #buildBasicContextPipeline() {
575
577
  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));
578
+ .compose(this.#checkRobotsTxt.bind(this))
579
+ .compose(this.#createBaseContext.bind(this))
580
+ .compose(this.#resolveSession.bind(this))
581
+ .compose(this.#createContextHelpers.bind(this));
580
582
  }
581
- async checkRobotsTxt({ request }) {
582
- if (!(await this.isAllowedBasedOnRobotsTxtFile(request.url))) {
583
+ async #checkRobotsTxt({ request }) {
584
+ if (!(await this.#isAllowedBasedOnRobotsTxtFile(request.url))) {
583
585
  this.log.warning(`Skipping request ${request.url} (${request.id}) because it is disallowed based on robots.txt`);
584
586
  request.state = RequestState.SKIPPED;
585
587
  request.noRetry = true;
@@ -591,7 +593,7 @@ export class BasicCrawler {
591
593
  }
592
594
  return {};
593
595
  }
594
- createBaseContext(context, onCleanup) {
596
+ #createBaseContext(context, onCleanup) {
595
597
  return {
596
598
  id: cryptoRandomObjectId(10),
597
599
  log: this.log,
@@ -640,8 +642,8 @@ export class BasicCrawler {
640
642
  * instance: managers key on `uniqueKey`/`id`, and it is this copy that flows back into `reclaimRequest` /
641
643
  * `markRequestAsHandled`, so nothing observes the difference.
642
644
  */
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
+ async #resolveRequest() {
646
+ const fetched = await this.#timeoutAndRetry(this.#fetchNextRequest.bind(this), this.internalTimeoutMillis, `Fetching next request timed out after ${this.internalTimeoutMillis / 1e3} seconds.`);
645
647
  if (!fetched) {
646
648
  return null;
647
649
  }
@@ -650,8 +652,8 @@ export class BasicCrawler {
650
652
  request.loadedUrl = undefined;
651
653
  return request;
652
654
  }
653
- async resolveSession({ request }) {
654
- const session = await this.timeoutAndRetry(async () => {
655
+ async #resolveSession({ request }) {
656
+ const session = await this.#timeoutAndRetry(async () => {
655
657
  const existingSession = await this.sessionPool.getSession(request.sessionId);
656
658
  if (!existingSession) {
657
659
  throw new MissingSessionError(request.sessionId);
@@ -660,7 +662,7 @@ export class BasicCrawler {
660
662
  }, this.internalTimeoutMillis, `Fetching session timed out after ${this.internalTimeoutMillis / 1e3} seconds.`);
661
663
  return { session, proxyInfo: session?.proxyInfo };
662
664
  }
663
- async createContextHelpers({ request, session }) {
665
+ async #createContextHelpers({ request, session }) {
664
666
  const addRequests = async (requests, options = {}) => {
665
667
  const newCrawlDepth = request.crawlDepth + 1;
666
668
  const requestsGenerator = this.addCrawlDepthRequestGenerator(requests, newCrawlDepth);
@@ -669,7 +671,7 @@ export class BasicCrawler {
669
671
  const sendRequest = createSendRequest(this.httpClient, request, session);
670
672
  return { addRequests, sendRequest };
671
673
  }
672
- buildFinalContextPipeline() {
674
+ #buildFinalContextPipeline() {
673
675
  const subclassPipeline = (this.#contextPipelineOptions.contextPipelineBuilder?.() ??
674
676
  ContextPipeline.create());
675
677
  // `extendContext` runs *before* the subclass navigation pipeline (which includes the
@@ -694,7 +696,7 @@ export class BasicCrawler {
694
696
  }
695
697
  contextPipeline = contextPipeline.compose(async (context) => {
696
698
  const { request } = context;
697
- if (request && !this.requestMatchesEnqueueStrategy(request)) {
699
+ if (request && !this.#requestMatchesEnqueueStrategy(request)) {
698
700
  // eslint-disable-next-line dot-notation
699
701
  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
702
  this.log.debug(message);
@@ -741,7 +743,7 @@ export class BasicCrawler {
741
743
  level: options.level,
742
744
  });
743
745
  }
744
- getPeriodicLogger() {
746
+ #getPeriodicLogger() {
745
747
  let previousState = { ...this.statistics.state };
746
748
  const getOperationMode = () => {
747
749
  const { requestsFailed } = this.statistics.state;
@@ -846,15 +848,15 @@ export class BasicCrawler {
846
848
  await this.#concurrencySystem.ifOwned((system) => system.stop());
847
849
  throw error;
848
850
  }
849
- const periodicLogger = this.getPeriodicLogger();
851
+ const periodicLogger = this.#getPeriodicLogger();
850
852
  this.setStatusMessage('Starting the crawler.', { level: 'INFO' });
851
853
  const sigintHandler = async () => {
852
854
  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();
855
+ await this.#pauseOnMigration();
854
856
  await run.abort();
855
857
  };
856
858
  // Attach a listener to handle migration and aborting events gracefully.
857
- const boundPauseOnMigration = this.pauseOnMigration.bind(this);
859
+ const boundPauseOnMigration = this.#pauseOnMigration.bind(this);
858
860
  process.once('SIGINT', sigintHandler);
859
861
  const eventManager = serviceLocator.getEventManager();
860
862
  eventManager.on(EventType.MIGRATING, boundPauseOnMigration);
@@ -970,14 +972,14 @@ export class BasicCrawler {
970
972
  */
971
973
  async getRequestManager() {
972
974
  if (!this.#requestManager) {
973
- this.#requestManager = await this.openOwnedRequestQueue();
975
+ this.#requestManager = await this.#openOwnedRequestQueue();
974
976
  }
975
977
  // Apply the processing-time hint here (an async lifecycle point) rather than in the constructor,
976
978
  // now that `setExpectedRequestProcessingTimeSecs` is async. The hint is raise-only and idempotent,
977
979
  // but guard so we do not re-issue it on every call.
978
980
  if (!this.#requestManagerTimeoutsApplied) {
979
981
  this.#requestManagerTimeoutsApplied = true;
980
- await this.applyRequestManagerTimeouts(this.#requestManager);
982
+ await this.#applyRequestManagerTimeouts(this.#requestManager);
981
983
  }
982
984
  return this.#requestManager;
983
985
  }
@@ -991,7 +993,7 @@ export class BasicCrawler {
991
993
  /**
992
994
  * Opens the default {@link RequestQueue} — the crawler's own, read from when the caller supplied nothing.
993
995
  */
994
- async openOwnedRequestQueue() {
996
+ async #openOwnedRequestQueue() {
995
997
  // The first crawler instance uses the default queue (null identifier);
996
998
  // subsequent instances get their own queue via a unique alias so they don't collide.
997
999
  const identifier = this.#identity.instanceIndex === 0 ? null : { alias: `__default_${this.#identity.id}__` };
@@ -1004,7 +1006,7 @@ export class BasicCrawler {
1004
1006
  * request from being handed out a second time while it is still being processed — and it works
1005
1007
  * regardless of whether the manager is a plain {@link RequestQueue} or a `RequestManagerTandem`.
1006
1008
  */
1007
- async applyRequestManagerTimeouts(requestManager) {
1009
+ async #applyRequestManagerTimeouts(requestManager) {
1008
1010
  // A router route may hold a request for longer than the crawler's own timeout, and we cannot know
1009
1011
  // which routes a run will hit, so reserve for the longest one any route asked for. The hint is
1010
1012
  // raise-only, so erring high here is safe.
@@ -1019,7 +1021,7 @@ export class BasicCrawler {
1019
1021
  * the request's label. Applied by the crawler on the add paths it owns — `crawler.addRequests`,
1020
1022
  * `crawler.run`, `context.addRequests` and `context.enqueueLinks`.
1021
1023
  */
1022
- async validateRequestUserData(source) {
1024
+ async #validateRequestUserData(source) {
1023
1025
  if (typeof source === 'string') {
1024
1026
  return;
1025
1027
  }
@@ -1062,7 +1064,9 @@ export class BasicCrawler {
1062
1064
  if (this.#maxRequestsPerCrawl === undefined) {
1063
1065
  return explicitLimit;
1064
1066
  }
1065
- const limit = Math.max(0, this.#maxRequestsPerCrawl - this.handledRequestsCount - (await this.#getPendingRequestCountApproximation()));
1067
+ const limit = Math.max(0, this.#maxRequestsPerCrawl -
1068
+ this.#handledRequestsCount -
1069
+ (await this.#getPendingRequestCountApproximation()));
1066
1070
  return Math.min(limit, explicitLimit ?? Infinity);
1067
1071
  }
1068
1072
  async #handleSkippedRequest(options) {
@@ -1109,7 +1113,7 @@ export class BasicCrawler {
1109
1113
  // its own per-item validation below, and validating an absent label/userData here would spuriously
1110
1114
  // check them against a registered default-route schema.
1111
1115
  if (options.label !== undefined || options.userData !== undefined) {
1112
- await this.validateRequestUserData({ label: options.label, userData: options.userData });
1116
+ await this.#validateRequestUserData({ label: options.label, userData: options.userData });
1113
1117
  }
1114
1118
  const requestLimit = await this.#calculateEnqueuedRequestLimit(options.limit);
1115
1119
  const strategy = options.strategy ?? EnqueueStrategy.All;
@@ -1118,9 +1122,9 @@ export class BasicCrawler {
1118
1122
  // The strategy always applies, even when `include` patterns are provided - the two are AND-ed together
1119
1123
  // (a URL must match an `include` pattern *and* satisfy the strategy). This mirrors crawlee-python.
1120
1124
  const enqueueStrategyPatterns = options.baseUrl ? buildEnqueueStrategyPatterns(options.baseUrl, strategy) : [];
1121
- const isAllowedBasedOnRobotsTxtFile = this.isAllowedBasedOnRobotsTxtFile.bind(this);
1125
+ const isAllowedBasedOnRobotsTxtFile = this.#isAllowedBasedOnRobotsTxtFile.bind(this);
1122
1126
  const maxCrawlDepth = this.#maxCrawlDepth;
1123
- const validateRequestUserData = this.validateRequestUserData.bind(this);
1127
+ const validateRequestUserData = this.#validateRequestUserData.bind(this);
1124
1128
  const allSkipped = [];
1125
1129
  // A skipped source (which can carry arbitrary userData) is only retained if something reads it -
1126
1130
  // otherwise the URL alone is enough to build the callback argument and to log with.
@@ -1292,7 +1296,7 @@ export class BasicCrawler {
1292
1296
  // floored per request so it will not actually cut them short, but the configured value is then effectively
1293
1297
  // ignored, which is worth flagging. Checked here (not in the constructor) because a subclass sets its
1294
1298
  // navigation timeout only after `super()`.
1295
- const phasesMillis = this.getNavigationTimeoutMillis() + this.resolveRequestHandlerTimeoutMillis(undefined);
1299
+ const phasesMillis = this.getNavigationTimeoutMillis() + this.#resolveRequestHandlerTimeoutMillis(undefined);
1296
1300
  if (this.internalTimeoutMillis < phasesMillis) {
1297
1301
  this.log.warning(`CRAWLEE_INTERNAL_TIMEOUT (${this.internalTimeoutMillis / 1000}s) is shorter than the navigation ` +
1298
1302
  `and request handler timeouts combined (${phasesMillis / 1000}s); it will be raised per request ` +
@@ -1314,9 +1318,9 @@ export class BasicCrawler {
1314
1318
  * request, a per-route override, or a low `CRAWLEE_INTERNAL_TIMEOUT` is not cut short mid-phase. It takes
1315
1319
  * whichever is larger: the configured internal timeout, or this request's combined phase budget.
1316
1320
  */
1317
- async withRequestTimeout(crawlingContext, work) {
1321
+ async #withRequestTimeout(crawlingContext, work) {
1318
1322
  const { request } = crawlingContext;
1319
- const phasesMillis = this.getNavigationTimeoutMillis() + this.resolveRequestHandlerTimeoutMillis(request.label);
1323
+ const phasesMillis = this.getNavigationTimeoutMillis() + this.#resolveRequestHandlerTimeoutMillis(request.label);
1320
1324
  const timeoutMillis = Math.max(this.internalTimeoutMillis, phasesMillis);
1321
1325
  await raceWithTimeout(crawlingContext, work, { timeoutMillis, requestId: request.id });
1322
1326
  }
@@ -1327,14 +1331,14 @@ export class BasicCrawler {
1327
1331
  * @param label The request's route label, or `undefined` for the default route / no specific request.
1328
1332
  * @param fallbackMillis Timeout to use when no route overrides it.
1329
1333
  */
1330
- resolveRequestHandlerTimeoutMillis(label, fallbackMillis = this.#requestHandlerTimeoutMillis) {
1331
- return this.getRouteTimeoutMillis(label) ?? fallbackMillis;
1334
+ #resolveRequestHandlerTimeoutMillis(label, fallbackMillis = this.#requestHandlerTimeoutMillis) {
1335
+ return this.#getRouteTimeoutMillis(label) ?? fallbackMillis;
1332
1336
  }
1333
1337
  /**
1334
1338
  * The timeout the router route with the given label asked for, or `undefined` when it did not override one
1335
1339
  * (or the request handler is not a router at all).
1336
1340
  */
1337
- getRouteTimeoutMillis(label) {
1341
+ #getRouteTimeoutMillis(label) {
1338
1342
  const getTimeoutSecs = this.requestHandler.getTimeoutSecs;
1339
1343
  if (typeof getTimeoutSecs !== 'function') {
1340
1344
  return undefined;
@@ -1343,7 +1347,7 @@ export class BasicCrawler {
1343
1347
  return timeoutSecs === undefined ? undefined : timeoutSecs * 1000;
1344
1348
  }
1345
1349
  async runRequestHandler(crawlingContext) {
1346
- const timeoutMillis = this.resolveRequestHandlerTimeoutMillis(crawlingContext.request.label);
1350
+ const timeoutMillis = this.#resolveRequestHandlerTimeoutMillis(crawlingContext.request.label);
1347
1351
  await addTimeoutToPromise(async () => this.requestHandler(crawlingContext), timeoutMillis, `requestHandler timed out after ${timeoutMillis / 1000} seconds (${crawlingContext.request.id}).`);
1348
1352
  }
1349
1353
  /**
@@ -1351,6 +1355,7 @@ export class BasicCrawler {
1351
1355
  * Deliberately does **not** commit on return - the pipeline hands failures to `handleRequestFailure`, so a
1352
1356
  * normal return says nothing about success. `handleRequest` commits on success.
1353
1357
  */
1358
+ // oxlint-disable-next-line crawlee/prefer-private-fields -- accessed by tests
1354
1359
  async runInStorageTransaction(callback) {
1355
1360
  if (!this.#transactionalStorageEnabled) {
1356
1361
  return callback();
@@ -1390,7 +1395,7 @@ export class BasicCrawler {
1390
1395
  throw new SessionError(`Request blocked - received ${statusCode} status code.`);
1391
1396
  }
1392
1397
  }
1393
- async isAllowedBasedOnRobotsTxtFile(url) {
1398
+ async #isAllowedBasedOnRobotsTxtFile(url) {
1394
1399
  if (!this.#respectRobotsTxtFile) {
1395
1400
  return true;
1396
1401
  }
@@ -1399,7 +1404,7 @@ export class BasicCrawler {
1399
1404
  if (robotsTxtFile) {
1400
1405
  const crawlDelay = robotsTxtFile.getCrawlDelay(userAgent);
1401
1406
  if (crawlDelay !== undefined) {
1402
- this.applyCrawlDelay(url, crawlDelay);
1407
+ this.#applyCrawlDelay(url, crawlDelay);
1403
1408
  }
1404
1409
  }
1405
1410
  return !robotsTxtFile || robotsTxtFile.isAllowed(url, userAgent);
@@ -1427,7 +1432,7 @@ export class BasicCrawler {
1427
1432
  return false;
1428
1433
  }
1429
1434
  /** Hands a robots.txt `Crawl-delay` to the request manager, warning if it will not be honoured. */
1430
- applyCrawlDelay(url, delaySeconds) {
1435
+ #applyCrawlDelay(url, delaySeconds) {
1431
1436
  // robots.txt is per-origin; `hostname` is the closest pacing scope and errs wide (http and https to one
1432
1437
  // host share a clock).
1433
1438
  if (this.requestManager?.recordPacingSignal({
@@ -1464,7 +1469,7 @@ export class BasicCrawler {
1464
1469
  return undefined;
1465
1470
  }
1466
1471
  }
1467
- async pauseOnMigration() {
1472
+ async #pauseOnMigration() {
1468
1473
  const run = this.#dispatchingRun;
1469
1474
  if (run) {
1470
1475
  await run.pause(SAFE_MIGRATION_WAIT_MILLIS).catch((err) => {
@@ -1484,7 +1489,7 @@ export class BasicCrawler {
1484
1489
  /**
1485
1490
  * Fetches the next request to process from the underlying request provider.
1486
1491
  */
1487
- async fetchNextRequest() {
1492
+ async #fetchNextRequest() {
1488
1493
  if (this.requestManager === undefined) {
1489
1494
  throw new Error(`fetchNextRequest called on an uninitialized crawler`);
1490
1495
  }
@@ -1505,7 +1510,7 @@ export class BasicCrawler {
1505
1510
  // it is retried. This also closes the transaction, so everything below passes through.
1506
1511
  // Opened by `runInStorageTransaction`; absent when disabled or when the subclass opens its own.
1507
1512
  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.`);
1513
+ 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
1514
  this.statistics.recordRequestSuccess(request.id || request.uniqueKey, request.retryCount);
1510
1515
  // reclaim session if request finishes successfully
1511
1516
  request.state = RequestState.DONE;
@@ -1522,7 +1527,7 @@ export class BasicCrawler {
1522
1527
  currentStorageTransaction()?.rollback();
1523
1528
  const skipped = rawError instanceof ContextPipelineInitializationError &&
1524
1529
  rawError.cause instanceof ContextPipelineInterruptedError;
1525
- const err = this.unwrapError(rawError);
1530
+ const err = this.#unwrapError(rawError);
1526
1531
  const context = crawlingContext;
1527
1532
  const retryCountBefore = request.retryCount;
1528
1533
  const resumeRequestTimeout = crawlingContext[suspendTimeoutKey]?.();
@@ -1531,7 +1536,7 @@ export class BasicCrawler {
1531
1536
  // (e.g., doesn't match enqueue strategy after redirect). Just return gracefully.
1532
1537
  if (skipped) {
1533
1538
  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.`);
1539
+ 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
1540
  return;
1536
1541
  }
1537
1542
  request.state = RequestState.ERROR_HANDLER;
@@ -1539,7 +1544,7 @@ export class BasicCrawler {
1539
1544
  request.state = RequestState.DONE;
1540
1545
  }
1541
1546
  catch (secondaryError) {
1542
- const unwrappedSecondaryError = this.unwrapError(secondaryError);
1547
+ const unwrappedSecondaryError = this.#unwrapError(secondaryError);
1543
1548
  const timedOut = unwrappedSecondaryError instanceof TimeoutError;
1544
1549
  request.state = RequestState.ERROR;
1545
1550
  if (timedOut) {
@@ -1575,7 +1580,7 @@ export class BasicCrawler {
1575
1580
  }
1576
1581
  // decrease the session score if the request fails (but the error handler did not throw);
1577
1582
  // skip when the error is a SessionError, which already retired the session
1578
- if (!skipped && !this.errorAbsolvesSession(err)) {
1583
+ if (!skipped && !this.#errorAbsolvesSession(err)) {
1579
1584
  context.session?.markBad();
1580
1585
  }
1581
1586
  }
@@ -1603,7 +1608,7 @@ export class BasicCrawler {
1603
1608
  * Run async callback with given timeout and retry. Returns the result of the callback.
1604
1609
  * @ignore
1605
1610
  */
1606
- async timeoutAndRetry(handler, timeout, error, maxRetries = 3, retried = 1) {
1611
+ async #timeoutAndRetry(handler, timeout, error, maxRetries = 3, retried = 1) {
1607
1612
  try {
1608
1613
  return await addTimeoutToPromise(handler, timeout, error);
1609
1614
  }
@@ -1611,7 +1616,7 @@ export class BasicCrawler {
1611
1616
  if (retried <= maxRetries) {
1612
1617
  // we retry on any error, not just timeout
1613
1618
  this.log.warning(`${e.message} (retrying ${retried}/${maxRetries})`);
1614
- return this.timeoutAndRetry(handler, timeout, error, maxRetries, retried + 1);
1619
+ return this.#timeoutAndRetry(handler, timeout, error, maxRetries, retried + 1);
1615
1620
  }
1616
1621
  throw e;
1617
1622
  }
@@ -1620,7 +1625,7 @@ export class BasicCrawler {
1620
1625
  * Whether the request manager has a request ready for processing. A manager that is only `waiting` also gets a
1621
1626
  * wake-up scheduled, so a paced crawl resumes on its clock rather than on the task loop's polling interval.
1622
1627
  */
1623
- async isTaskReadyFunction() {
1628
+ async #isTaskReadyFunction() {
1624
1629
  if (this.requestManager === undefined) {
1625
1630
  return false;
1626
1631
  }
@@ -1634,11 +1639,11 @@ export class BasicCrawler {
1634
1639
  * Unwraps errors thrown by the context pipeline to get the actual user error.
1635
1640
  * RequestHandlerError, ContextPipelineInitializationError and ContextPipelineCleanupError wrap the actual error.
1636
1641
  */
1637
- unwrapError(error) {
1642
+ #unwrapError(error) {
1638
1643
  if (error instanceof RequestHandlerError ||
1639
1644
  error instanceof ContextPipelineInitializationError ||
1640
1645
  error instanceof ContextPipelineCleanupError) {
1641
- return this.unwrapError(error.cause);
1646
+ return this.#unwrapError(error.cause);
1642
1647
  }
1643
1648
  return error;
1644
1649
  }
@@ -1663,7 +1668,7 @@ export class BasicCrawler {
1663
1668
  if (error instanceof CriticalError) {
1664
1669
  throw error;
1665
1670
  }
1666
- const shouldRetryRequest = this.canRequestBeRetried(request, error);
1671
+ const shouldRetryRequest = this.#canRequestBeRetried(request, error);
1667
1672
  if (shouldRetryRequest) {
1668
1673
  await this.statistics.errorTrackerRetry.addAsync(error, crawlingContext);
1669
1674
  await this.#errorHandler?.(crawlingContext, // valid cast - ExtendedContext transitively extends CrawlingContext
@@ -1741,10 +1746,10 @@ export class BasicCrawler {
1741
1746
  * Whether the session should be spared for this error - either because it was already retired, or because the
1742
1747
  * failure says nothing about the session (a rate limit is a property of the domain).
1743
1748
  */
1744
- errorAbsolvesSession(error) {
1749
+ #errorAbsolvesSession(error) {
1745
1750
  return error instanceof SessionError || error instanceof RequestThrottledError;
1746
1751
  }
1747
- canRequestBeRetried(request, error) {
1752
+ #canRequestBeRetried(request, error) {
1748
1753
  // Request should never be retried, or the error encountered makes it not able to be retried.
1749
1754
  if (request.noRetry || error instanceof NonRetryableError) {
1750
1755
  return false;
@@ -1802,7 +1807,7 @@ export class BasicCrawler {
1802
1807
  }
1803
1808
  return request.headers?.Cookie || request.headers?.cookie || '';
1804
1809
  }
1805
- requestMatchesEnqueueStrategy(request) {
1810
+ #requestMatchesEnqueueStrategy(request) {
1806
1811
  // If `skipNavigation` was used, just return `true`
1807
1812
  try {
1808
1813
  // eslint-disable-next-line @typescript-eslint/no-unused-expressions