@crawlee/basic 4.0.0-beta.104 → 4.0.0-beta.106
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/internals/basic-crawler.d.ts +58 -86
- package/internals/basic-crawler.js +177 -114
- package/package.json +7 -7
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { AddRequestsBatchedOptions, AddRequestsBatchedResult, ConcurrencySystemOptions, Configuration, CrawleeLogger, CrawlingContext, DatasetExportOptions, EnqueueLinksOptions, EventManager, FinalStatistics, GetUserDataFromRequest, IConcurrencySystem, IProxyConfiguration, IRequestLoader, IRequestManager, IStatistics, Request, RequestsLike, RouterHandler, RouterRoutes, SkippedRequestCallback, Source, StatisticState, StorageIdentifier, TaskLoopPredicates, TypedRequestsLike } from '@crawlee/core';
|
|
1
|
+
import type { AddRequestsBatchedOptions, AddRequestsBatchedResult, ConcurrencySystemOptions, Configuration, CrawleeLogger, CrawlingContext, DatasetExportOptions, EnqueueLinksOptions, EventManager, FinalStatistics, GetUserDataFromRequest, IConcurrencySystem, IProxyConfiguration, IRequestLoader, IRequestManager, IStatistics, Request, RequestsLike, RouterHandler, RouterRoutes, SkippedRequestCallback, Source, StatisticState, StorageIdentifier, StorageWritePolicy, TaskLoopPredicates, TypedRequestsLike } from '@crawlee/core';
|
|
2
2
|
import { ConcurrencySystem, ContextPipeline, Dataset, RequestQueue } from '@crawlee/core';
|
|
3
3
|
import type { Awaitable, BaseHttpClient, BatchAddRequestsResult, Dictionary, ISession, ISessionPool, ProxyInfo, SetStatusMessageOptions, StorageBackend } from '@crawlee/types';
|
|
4
4
|
import { RobotsTxtFile } from '@crawlee/utils';
|
|
5
|
+
import { type BasePredicate } from 'ow';
|
|
5
6
|
import type { ReadonlyDeep, SetRequired } from 'type-fest';
|
|
6
7
|
import { TimeoutError } from '@apify/timeout';
|
|
7
8
|
export interface BasicCrawlingContext<UserData extends Dictionary = Dictionary> extends CrawlingContext<UserData> {
|
|
@@ -314,6 +315,20 @@ export interface BasicCrawlerOptions<Context extends CrawlingContext = CrawlingC
|
|
|
314
315
|
*
|
|
315
316
|
*/
|
|
316
317
|
id?: string;
|
|
318
|
+
/**
|
|
319
|
+
* Makes the storage writes performed while handling a request atomic with respect to the request
|
|
320
|
+
* succeeding: they are recorded in a {@link StorageTransaction} spanning the whole request
|
|
321
|
+
* lifecycle and only applied when the request handler succeeds, so a thrown handler leaves no partial
|
|
322
|
+
* writes behind and a retry does not double-write. Reads within the handler see its own writes.
|
|
323
|
+
*
|
|
324
|
+
* `false` disables the mechanism entirely; an object overrides the per-storage-type
|
|
325
|
+
* {@link StorageWritePolicy} (e.g. `{ requestQueue: 'deferred' }` for all-or-nothing enqueues).
|
|
326
|
+
* {@link withDirectStorageAccess} is the per-call-site escape hatch; `useState()` is deliberately
|
|
327
|
+
* *not* transactional.
|
|
328
|
+
*
|
|
329
|
+
* @default true
|
|
330
|
+
*/
|
|
331
|
+
transactionalStorage?: boolean | Partial<StorageWritePolicy>;
|
|
317
332
|
/**
|
|
318
333
|
* An array of HTTP response [Status Codes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) to be excluded from error consideration.
|
|
319
334
|
* By default, status codes >= 500 trigger errors.
|
|
@@ -418,13 +433,6 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
418
433
|
* collide.
|
|
419
434
|
*/
|
|
420
435
|
private static instanceCount;
|
|
421
|
-
/**
|
|
422
|
-
* Tracks crawler instances that accessed shared state without having an explicit id.
|
|
423
|
-
* Used to detect and warn about multiple crawlers sharing the same state.
|
|
424
|
-
*/
|
|
425
|
-
private static useStateAnonymousIndices;
|
|
426
|
-
/** Backs the {@link BasicCrawler.stats|`stats`} getter. */
|
|
427
|
-
private statsDep;
|
|
428
436
|
/**
|
|
429
437
|
* The statistics instance collecting the crawler's run statistics - either the injected `statistics` option or a
|
|
430
438
|
* crawler-built default. Typed as {@link IStatistics} so custom implementations can be plugged in.
|
|
@@ -436,33 +444,11 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
436
444
|
* via {@link BasicCrawler.getRequestManager|`getRequestManager()`}.
|
|
437
445
|
*/
|
|
438
446
|
protected requestManager?: IRequestManager;
|
|
439
|
-
/** Backs the {@link BasicCrawler.sessionPool|`sessionPool`} getter. */
|
|
440
|
-
private sessionPoolDep;
|
|
441
447
|
/**
|
|
442
448
|
* A reference to the underlying session pool that manages the crawler's {@link Session|sessions}. Typed as
|
|
443
449
|
* {@link ISessionPool} so custom implementations can be plugged in via the `sessionPool` constructor option.
|
|
444
450
|
*/
|
|
445
451
|
get sessionPool(): ISessionPool;
|
|
446
|
-
/**
|
|
447
|
-
* Tracks **only** the queue the crawler opens for itself — not the {@link RequestManagerTandem} that may wrap it
|
|
448
|
-
* around a user-supplied `requestList` — so the owned-only purge between repeated `run()` calls never reaches
|
|
449
|
-
* through to a borrowed loader. Filled lazily in {@link BasicCrawler.openOwnedRequestQueue|`openOwnedRequestQueue()`}.
|
|
450
|
-
*/
|
|
451
|
-
private ownedRequestQueue;
|
|
452
|
-
/**
|
|
453
|
-
* Whether the request-processing-time hint has already been forwarded to the request manager. The hint
|
|
454
|
-
* derives only from `requestHandlerTimeoutMillis` (constant for the crawler's lifetime) and is raise-only,
|
|
455
|
-
* so it only needs to be applied once, at the first async access of the manager.
|
|
456
|
-
*/
|
|
457
|
-
private requestManagerTimeoutsApplied;
|
|
458
|
-
/**
|
|
459
|
-
* Resolves the governor for one run: either the injected
|
|
460
|
-
* {@link BasicCrawlerOptions.concurrencySystem|`concurrencySystem`} (borrowed) or a freshly built default with
|
|
461
|
-
* the concurrency shortcuts folded in (owned, so the crawler starts and stops it).
|
|
462
|
-
*/
|
|
463
|
-
private readonly resolveConcurrencySystem;
|
|
464
|
-
/** As resolved by `_init()`. Absent until the first run, so a `teardown()` before it is a no-op. */
|
|
465
|
-
private concurrencySystemDep?;
|
|
466
452
|
/**
|
|
467
453
|
* The concurrency governor this run is booking its requests against — either the
|
|
468
454
|
* {@link BasicCrawlerOptions.concurrencySystem|`concurrencySystem`} that was injected, or the default the
|
|
@@ -476,14 +462,6 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
476
462
|
* `minConcurrency`/`maxConcurrency`/`desiredConcurrency` on your own reference.
|
|
477
463
|
*/
|
|
478
464
|
get concurrencySystem(): IConcurrencySystem | undefined;
|
|
479
|
-
/**
|
|
480
|
-
* The task loop that dispatches this run's requests. Private on purpose — it is a bare parallel task runner with
|
|
481
|
-
* no configuration left of its own (see {@link ConcurrencySystem}), and everything a caller legitimately did
|
|
482
|
-
* with it now has a crawler-level counterpart: {@link BasicCrawler.pause|`pause()`},
|
|
483
|
-
* {@link BasicCrawler.resume|`resume()`}, {@link BasicCrawler.teardown|`teardown()`} and
|
|
484
|
-
* {@link BasicCrawler.concurrencySystem|`concurrencySystem`}.
|
|
485
|
-
*/
|
|
486
|
-
private autoscaledPool?;
|
|
487
465
|
/**
|
|
488
466
|
* A reference to the underlying {@link IProxyConfiguration} instance that manages the crawler's proxies.
|
|
489
467
|
* Only available if used by the crawler.
|
|
@@ -494,7 +472,6 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
494
472
|
* See {@link Router.addHandler|`router.addHandler()`} and {@link Router.addDefaultHandler|`router.addDefaultHandler()`}.
|
|
495
473
|
*/
|
|
496
474
|
readonly router: RouterHandler<Context, Routes>;
|
|
497
|
-
private _basicContextPipeline?;
|
|
498
475
|
/**
|
|
499
476
|
* The basic part of the context pipeline. Unlike the subclass pipeline, this
|
|
500
477
|
* part has no major side effects (e.g. launching a browser). It also makes typing more explicit, as subclass
|
|
@@ -506,11 +483,9 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
506
483
|
get basicContextPipeline(): ContextPipeline<{
|
|
507
484
|
request: Request;
|
|
508
485
|
}, CrawlingContext>;
|
|
509
|
-
private _contextPipeline?;
|
|
510
486
|
get contextPipeline(): ContextPipeline<CrawlingContext, ExtendedContext>;
|
|
511
487
|
running: boolean;
|
|
512
488
|
hasFinishedBefore: boolean;
|
|
513
|
-
private unexpectedStop;
|
|
514
489
|
get log(): CrawleeLogger;
|
|
515
490
|
protected readonly requestHandler: RequestHandler<ExtendedContext>;
|
|
516
491
|
protected readonly errorHandler?: ErrorHandler<CrawlingContext, ExtendedContext>;
|
|
@@ -519,15 +494,10 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
519
494
|
protected readonly internalTimeoutMillis: number;
|
|
520
495
|
protected readonly maxRequestRetries: number;
|
|
521
496
|
protected readonly maxCrawlDepth?: number;
|
|
522
|
-
private sameDomainDelayMillis;
|
|
523
|
-
private domainAccessedTime;
|
|
524
497
|
protected readonly maxRequestsPerCrawl?: number;
|
|
525
498
|
private get handledRequestsCount();
|
|
526
|
-
private statusMessageLoggingInterval;
|
|
527
|
-
private statusMessageCallback?;
|
|
528
499
|
protected blockedStatusCodes: Set<number>;
|
|
529
500
|
protected readonly additionalHttpErrorStatusCodes: Set<number>;
|
|
530
|
-
private ignoreHttpErrorStatusCodes;
|
|
531
501
|
/**
|
|
532
502
|
* The resolved options for the crawler's own task loop — the crawler-owned `runTaskFunction`, the (possibly
|
|
533
503
|
* user-overridden) ready/finished predicates and cadence/logging. Concurrency configuration lives on the
|
|
@@ -537,50 +507,45 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
537
507
|
private taskLoopOptions;
|
|
538
508
|
protected readonly httpClient: BaseHttpClient;
|
|
539
509
|
protected readonly retryOnBlocked: boolean;
|
|
540
|
-
private respectRobotsTxtFile;
|
|
541
510
|
protected readonly onSkippedRequest?: SkippedRequestCallback;
|
|
542
|
-
private _closeEvents?;
|
|
543
|
-
private loggedPerRun;
|
|
544
|
-
private readonly robotsTxtFileCache;
|
|
545
511
|
protected readonly identity: CrawlerIdentity;
|
|
546
|
-
private readonly contextPipelineOptions;
|
|
547
512
|
protected static optionsShape: {
|
|
548
513
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
549
|
-
contextPipelineBuilder: import("ow").ObjectPredicate<object> &
|
|
514
|
+
contextPipelineBuilder: import("ow").ObjectPredicate<object> & BasePredicate<object | undefined>;
|
|
550
515
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
551
|
-
extendContext: import("ow").Predicate<Function> &
|
|
516
|
+
extendContext: import("ow").Predicate<Function> & BasePredicate<Function | undefined>;
|
|
552
517
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
553
|
-
requestList: import("ow").ObjectPredicate<object> &
|
|
518
|
+
requestList: import("ow").ObjectPredicate<object> & BasePredicate<object | undefined>;
|
|
554
519
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
555
|
-
requestQueue: import("ow").ObjectPredicate<object> &
|
|
520
|
+
requestQueue: import("ow").ObjectPredicate<object> & BasePredicate<object | undefined>;
|
|
556
521
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
557
|
-
requestHandler: import("ow").Predicate<Function> &
|
|
522
|
+
requestHandler: import("ow").Predicate<Function> & BasePredicate<Function | undefined>;
|
|
558
523
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
559
|
-
requestHandlerTimeoutSecs: import("ow").NumberPredicate &
|
|
524
|
+
requestHandlerTimeoutSecs: import("ow").NumberPredicate & BasePredicate<number | undefined>;
|
|
560
525
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
561
|
-
errorHandler: import("ow").Predicate<Function> &
|
|
526
|
+
errorHandler: import("ow").Predicate<Function> & BasePredicate<Function | undefined>;
|
|
562
527
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
563
|
-
failedRequestHandler: import("ow").Predicate<Function> &
|
|
528
|
+
failedRequestHandler: import("ow").Predicate<Function> & BasePredicate<Function | undefined>;
|
|
564
529
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
565
|
-
maxRequestRetries: import("ow").NumberPredicate &
|
|
530
|
+
maxRequestRetries: import("ow").NumberPredicate & BasePredicate<number | undefined>;
|
|
566
531
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
567
|
-
sameDomainDelaySecs: import("ow").NumberPredicate &
|
|
532
|
+
sameDomainDelaySecs: import("ow").NumberPredicate & BasePredicate<number | undefined>;
|
|
568
533
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
569
|
-
maxRequestsPerCrawl: import("ow").NumberPredicate &
|
|
534
|
+
maxRequestsPerCrawl: import("ow").NumberPredicate & BasePredicate<number | undefined>;
|
|
570
535
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
571
|
-
maxCrawlDepth: import("ow").NumberPredicate &
|
|
536
|
+
maxCrawlDepth: import("ow").NumberPredicate & BasePredicate<number | undefined>;
|
|
572
537
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
573
|
-
taskLoopOptions: import("ow").ObjectPredicate<object> &
|
|
538
|
+
taskLoopOptions: import("ow").ObjectPredicate<object> & BasePredicate<object | undefined>;
|
|
574
539
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
575
|
-
concurrencySystem: import("ow").ObjectPredicate<object> &
|
|
540
|
+
concurrencySystem: import("ow").ObjectPredicate<object> & BasePredicate<object | undefined>;
|
|
576
541
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
577
|
-
sessionPool: import("ow").ObjectPredicate<object> &
|
|
542
|
+
sessionPool: import("ow").ObjectPredicate<object> & BasePredicate<object | undefined>;
|
|
578
543
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
579
|
-
proxyConfiguration: import("ow").ObjectPredicate<object> &
|
|
544
|
+
proxyConfiguration: import("ow").ObjectPredicate<object> & BasePredicate<object | undefined>;
|
|
580
545
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
581
|
-
statusMessageLoggingInterval: import("ow").NumberPredicate &
|
|
546
|
+
statusMessageLoggingInterval: import("ow").NumberPredicate & BasePredicate<number | undefined>;
|
|
582
547
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
583
|
-
statusMessageCallback: import("ow").Predicate<Function> &
|
|
548
|
+
statusMessageCallback: import("ow").Predicate<Function> & BasePredicate<Function | undefined>;
|
|
584
549
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
585
550
|
additionalHttpErrorStatusCodes: import("ow").ArrayPredicate<number>;
|
|
586
551
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
@@ -588,33 +553,34 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
588
553
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
589
554
|
blockedStatusCodes: import("ow").ArrayPredicate<number>;
|
|
590
555
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
591
|
-
retryOnBlocked: import("ow").BooleanPredicate &
|
|
556
|
+
retryOnBlocked: import("ow").BooleanPredicate & BasePredicate<boolean | undefined>;
|
|
592
557
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
593
558
|
respectRobotsTxtFile: import("ow").AnyPredicate<boolean | object>;
|
|
559
|
+
transactionalStorage: BasePredicate<boolean | Partial<StorageWritePolicy> | undefined>;
|
|
594
560
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
595
|
-
onSkippedRequest: import("ow").Predicate<Function> &
|
|
561
|
+
onSkippedRequest: import("ow").Predicate<Function> & BasePredicate<Function | undefined>;
|
|
596
562
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
597
|
-
httpClient: import("ow").ObjectPredicate<object> &
|
|
563
|
+
httpClient: import("ow").ObjectPredicate<object> & BasePredicate<object | undefined>;
|
|
598
564
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
599
|
-
configuration: import("ow").ObjectPredicate<object> &
|
|
565
|
+
configuration: import("ow").ObjectPredicate<object> & BasePredicate<object | undefined>;
|
|
600
566
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
601
|
-
storageBackend: import("ow").ObjectPredicate<object> &
|
|
567
|
+
storageBackend: import("ow").ObjectPredicate<object> & BasePredicate<object | undefined>;
|
|
602
568
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
603
|
-
eventManager: import("ow").ObjectPredicate<object> &
|
|
569
|
+
eventManager: import("ow").ObjectPredicate<object> & BasePredicate<object | undefined>;
|
|
604
570
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
605
|
-
logger: import("ow").ObjectPredicate<object> &
|
|
571
|
+
logger: import("ow").ObjectPredicate<object> & BasePredicate<object | undefined>;
|
|
606
572
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
607
|
-
minConcurrency: import("ow").NumberPredicate &
|
|
573
|
+
minConcurrency: import("ow").NumberPredicate & BasePredicate<number | undefined>;
|
|
608
574
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
609
|
-
maxConcurrency: import("ow").NumberPredicate &
|
|
575
|
+
maxConcurrency: import("ow").NumberPredicate & BasePredicate<number | undefined>;
|
|
610
576
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
611
|
-
maxRequestsPerMinute: import("ow").NumberPredicate &
|
|
577
|
+
maxRequestsPerMinute: import("ow").NumberPredicate & BasePredicate<number | undefined>;
|
|
612
578
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
613
|
-
keepAlive: import("ow").BooleanPredicate &
|
|
579
|
+
keepAlive: import("ow").BooleanPredicate & BasePredicate<boolean | undefined>;
|
|
614
580
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
615
|
-
statistics: import("ow").ObjectPredicate<object> &
|
|
581
|
+
statistics: import("ow").ObjectPredicate<object> & BasePredicate<object | undefined>;
|
|
616
582
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
617
|
-
id: import("ow").StringPredicate &
|
|
583
|
+
id: import("ow").StringPredicate & BasePredicate<string | undefined>;
|
|
618
584
|
};
|
|
619
585
|
/**
|
|
620
586
|
* All `BasicCrawler` parameters are passed via an options object.
|
|
@@ -773,7 +739,7 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
773
739
|
/**
|
|
774
740
|
* Initializes the crawler.
|
|
775
741
|
*/
|
|
776
|
-
protected
|
|
742
|
+
protected init(): Promise<void>;
|
|
777
743
|
/**
|
|
778
744
|
* The navigation timeout (pre-navigation hooks, navigation, and post-navigation hooks) in milliseconds, used
|
|
779
745
|
* to size the internal request timeout. `BasicCrawler` has no navigation phase, so this is 0; the HTTP and
|
|
@@ -801,10 +767,16 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
801
767
|
*/
|
|
802
768
|
private getRouteTimeoutMillis;
|
|
803
769
|
protected runRequestHandler(crawlingContext: ExtendedContext): Promise<void>;
|
|
770
|
+
/**
|
|
771
|
+
* Runs `callback` inside a {@link StorageTransaction}, unless transactional storage is disabled.
|
|
772
|
+
* Deliberately does **not** commit on return - `handleRequest` swallows request handler failures, so
|
|
773
|
+
* a normal return says nothing about success. `handleRequest` owns the outcome.
|
|
774
|
+
*/
|
|
775
|
+
private runInStorageTransaction;
|
|
804
776
|
/**
|
|
805
777
|
* Handles blocked request
|
|
806
778
|
*/
|
|
807
|
-
protected
|
|
779
|
+
protected throwOnBlockedRequest(statusCode: number): void;
|
|
808
780
|
private isAllowedBasedOnRobotsTxtFile;
|
|
809
781
|
protected getRobotsTxtFileForUrl(url: string): Promise<RobotsTxtFile | undefined>;
|
|
810
782
|
private pauseOnMigration;
|
|
@@ -863,7 +835,7 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
863
835
|
* @param error The error received
|
|
864
836
|
* @returns The message to be logged
|
|
865
837
|
*/
|
|
866
|
-
protected
|
|
838
|
+
protected getMessageFromError(error: Error, forceStack?: boolean): string | TimeoutError | undefined;
|
|
867
839
|
private canRequestBeRetried;
|
|
868
840
|
/**
|
|
869
841
|
* Stops the crawler immediately.
|
|
@@ -873,7 +845,7 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
873
845
|
* To stop the crawler gracefully (waiting for all running requests to finish), use {@link BasicCrawler.stop|`crawler.stop()`} instead.
|
|
874
846
|
*/
|
|
875
847
|
teardown(): Promise<void>;
|
|
876
|
-
protected
|
|
848
|
+
protected getCookieHeaderFromRequest(request: Request): string;
|
|
877
849
|
private requestMatchesEnqueueStrategy;
|
|
878
850
|
}
|
|
879
851
|
export interface CreateContextOptions {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
|
-
import { AutoscaledPool, bindMethodsToServiceLocator, BLOCKED_STATUS_CODES, ConcurrencySystem, ContextPipeline, ContextPipelineCleanupError, ContextPipelineInitializationError, ContextPipelineInterruptedError, CriticalError, Dataset, enqueueLinks, EnqueueStrategy, getObjectType, KeyValueStore, log, LogLevel, mergeCookies, MissingSessionError, NavigationSkippedError, NonRetryableError, OwnedOrInjected, purgeDefaultStorages, RequestHandlerError, RequestManagerTandem, RequestQueue, RequestState, RetryRequestError, Router, ServiceLocator, serviceLocator, Session, SessionError, SessionPool, Statistics, validateUserData, validators, } from '@crawlee/core';
|
|
3
|
+
import { AutoscaledPool, bindMethodsToServiceLocator, BLOCKED_STATUS_CODES, ConcurrencySystem, ContextPipeline, ContextPipelineCleanupError, ContextPipelineInitializationError, ContextPipelineInterruptedError, createStorageTransaction, CriticalError, currentStorageTransaction, Dataset, enqueueLinks, EnqueueStrategy, getObjectType, KeyValueStore, log, LogLevel, mergeCookies, MissingSessionError, NavigationSkippedError, NonRetryableError, OwnedOrInjected, purgeDefaultStorages, RequestHandlerError, RequestManagerTandem, RequestQueue, RequestState, RetryRequestError, Router, ServiceLocator, serviceLocator, Session, SessionError, SessionPool, Statistics, validateUserData, validators, withDirectStorageAccess, } from '@crawlee/core';
|
|
4
4
|
import { FetchHttpClient } from '@crawlee/http-client';
|
|
5
5
|
import { isAsyncIterable, isIterable, RobotsTxtFile, ROTATE_PROXY_ERRORS } from '@crawlee/utils';
|
|
6
6
|
import { stringify } from 'csv-stringify/sync';
|
|
@@ -12,9 +12,9 @@ import { cryptoRandomObjectId } from '@apify/utilities';
|
|
|
12
12
|
import { extendTimeoutKey, navigationDeadlineKey, raceWithTimeout, timeoutExpiredKey, } from './request-timeout.js';
|
|
13
13
|
import { createSendRequest } from './send-request.js';
|
|
14
14
|
class LazyDefaultHttpClient {
|
|
15
|
-
|
|
15
|
+
#delegatePromise;
|
|
16
16
|
constructor(options) {
|
|
17
|
-
this
|
|
17
|
+
this.#delegatePromise = import('@crawlee/impit-client')
|
|
18
18
|
.then(({ ImpitHttpClient }) => new ImpitHttpClient(options))
|
|
19
19
|
.catch(() => {
|
|
20
20
|
(options?.logger ?? log).warning('Optional dependency @crawlee/impit-client is not installed. ' +
|
|
@@ -23,7 +23,7 @@ class LazyDefaultHttpClient {
|
|
|
23
23
|
});
|
|
24
24
|
}
|
|
25
25
|
async sendRequest(...args) {
|
|
26
|
-
return (await this
|
|
26
|
+
return (await this.#delegatePromise).sendRequest(...args);
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
@@ -46,20 +46,21 @@ export class BasicCrawler {
|
|
|
46
46
|
* request queue; subsequent ones get their own queue via a unique alias so they don't
|
|
47
47
|
* collide.
|
|
48
48
|
*/
|
|
49
|
+
// kept as TS-private: tests reset the counter at runtime
|
|
49
50
|
static instanceCount = 0;
|
|
50
51
|
/**
|
|
51
52
|
* Tracks crawler instances that accessed shared state without having an explicit id.
|
|
52
53
|
* Used to detect and warn about multiple crawlers sharing the same state.
|
|
53
54
|
*/
|
|
54
|
-
static useStateAnonymousIndices = new Set();
|
|
55
|
+
static #useStateAnonymousIndices = new Set();
|
|
55
56
|
/** Backs the {@link BasicCrawler.stats|`stats`} getter. */
|
|
56
|
-
statsDep;
|
|
57
|
+
#statsDep;
|
|
57
58
|
/**
|
|
58
59
|
* The statistics instance collecting the crawler's run statistics - either the injected `statistics` option or a
|
|
59
60
|
* crawler-built default. Typed as {@link IStatistics} so custom implementations can be plugged in.
|
|
60
61
|
*/
|
|
61
62
|
get stats() {
|
|
62
|
-
return this
|
|
63
|
+
return this.#statsDep.value;
|
|
63
64
|
}
|
|
64
65
|
/**
|
|
65
66
|
* The main request-handling component of the crawler. It manages the requests that the crawler processes,
|
|
@@ -68,34 +69,34 @@ export class BasicCrawler {
|
|
|
68
69
|
*/
|
|
69
70
|
requestManager;
|
|
70
71
|
/** Backs the {@link BasicCrawler.sessionPool|`sessionPool`} getter. */
|
|
71
|
-
sessionPoolDep;
|
|
72
|
+
#sessionPoolDep;
|
|
72
73
|
/**
|
|
73
74
|
* A reference to the underlying session pool that manages the crawler's {@link Session|sessions}. Typed as
|
|
74
75
|
* {@link ISessionPool} so custom implementations can be plugged in via the `sessionPool` constructor option.
|
|
75
76
|
*/
|
|
76
77
|
get sessionPool() {
|
|
77
|
-
return this
|
|
78
|
+
return this.#sessionPoolDep.value;
|
|
78
79
|
}
|
|
79
80
|
/**
|
|
80
81
|
* Tracks **only** the queue the crawler opens for itself — not the {@link RequestManagerTandem} that may wrap it
|
|
81
82
|
* around a user-supplied `requestList` — so the owned-only purge between repeated `run()` calls never reaches
|
|
82
83
|
* through to a borrowed loader. Filled lazily in {@link BasicCrawler.openOwnedRequestQueue|`openOwnedRequestQueue()`}.
|
|
83
84
|
*/
|
|
84
|
-
ownedRequestQueue = OwnedOrInjected.resolve();
|
|
85
|
+
#ownedRequestQueue = OwnedOrInjected.resolve();
|
|
85
86
|
/**
|
|
86
87
|
* Whether the request-processing-time hint has already been forwarded to the request manager. The hint
|
|
87
88
|
* derives only from `requestHandlerTimeoutMillis` (constant for the crawler's lifetime) and is raise-only,
|
|
88
89
|
* so it only needs to be applied once, at the first async access of the manager.
|
|
89
90
|
*/
|
|
90
|
-
requestManagerTimeoutsApplied = false;
|
|
91
|
+
#requestManagerTimeoutsApplied = false;
|
|
91
92
|
/**
|
|
92
93
|
* Resolves the governor for one run: either the injected
|
|
93
94
|
* {@link BasicCrawlerOptions.concurrencySystem|`concurrencySystem`} (borrowed) or a freshly built default with
|
|
94
95
|
* the concurrency shortcuts folded in (owned, so the crawler starts and stops it).
|
|
95
96
|
*/
|
|
96
|
-
resolveConcurrencySystem;
|
|
97
|
-
/** As resolved by `
|
|
98
|
-
concurrencySystemDep;
|
|
97
|
+
#resolveConcurrencySystem;
|
|
98
|
+
/** As resolved by `init()`. Absent until the first run, so a `teardown()` before it is a no-op. */
|
|
99
|
+
#concurrencySystemDep;
|
|
99
100
|
/**
|
|
100
101
|
* The concurrency governor this run is booking its requests against — either the
|
|
101
102
|
* {@link BasicCrawlerOptions.concurrencySystem|`concurrencySystem`} that was injected, or the default the
|
|
@@ -109,7 +110,7 @@ export class BasicCrawler {
|
|
|
109
110
|
* `minConcurrency`/`maxConcurrency`/`desiredConcurrency` on your own reference.
|
|
110
111
|
*/
|
|
111
112
|
get concurrencySystem() {
|
|
112
|
-
return this
|
|
113
|
+
return this.#concurrencySystemDep?.maybeValue;
|
|
113
114
|
}
|
|
114
115
|
/**
|
|
115
116
|
* The task loop that dispatches this run's requests. Private on purpose — it is a bare parallel task runner with
|
|
@@ -118,7 +119,7 @@ export class BasicCrawler {
|
|
|
118
119
|
* {@link BasicCrawler.resume|`resume()`}, {@link BasicCrawler.teardown|`teardown()`} and
|
|
119
120
|
* {@link BasicCrawler.concurrencySystem|`concurrencySystem`}.
|
|
120
121
|
*/
|
|
121
|
-
autoscaledPool;
|
|
122
|
+
#autoscaledPool;
|
|
122
123
|
/**
|
|
123
124
|
* A reference to the underlying {@link IProxyConfiguration} instance that manages the crawler's proxies.
|
|
124
125
|
* Only available if used by the crawler.
|
|
@@ -129,7 +130,7 @@ export class BasicCrawler {
|
|
|
129
130
|
* See {@link Router.addHandler|`router.addHandler()`} and {@link Router.addDefaultHandler|`router.addDefaultHandler()`}.
|
|
130
131
|
*/
|
|
131
132
|
router = Router.create();
|
|
132
|
-
|
|
133
|
+
#basicContextPipeline;
|
|
133
134
|
/**
|
|
134
135
|
* The basic part of the context pipeline. Unlike the subclass pipeline, this
|
|
135
136
|
* part has no major side effects (e.g. launching a browser). It also makes typing more explicit, as subclass
|
|
@@ -139,21 +140,21 @@ export class BasicCrawler {
|
|
|
139
140
|
* This is used e.g. in the {@link AdaptivePlaywrightCrawler|`AdaptivePlaywrightCrawler`}.
|
|
140
141
|
*/
|
|
141
142
|
get basicContextPipeline() {
|
|
142
|
-
if (this
|
|
143
|
-
this
|
|
143
|
+
if (this.#basicContextPipeline === undefined) {
|
|
144
|
+
this.#basicContextPipeline = this.buildBasicContextPipeline();
|
|
144
145
|
}
|
|
145
|
-
return this
|
|
146
|
+
return this.#basicContextPipeline;
|
|
146
147
|
}
|
|
147
|
-
|
|
148
|
+
#contextPipeline;
|
|
148
149
|
get contextPipeline() {
|
|
149
|
-
if (this
|
|
150
|
-
this
|
|
150
|
+
if (this.#contextPipeline === undefined) {
|
|
151
|
+
this.#contextPipeline = this.buildFinalContextPipeline();
|
|
151
152
|
}
|
|
152
|
-
return this
|
|
153
|
+
return this.#contextPipeline;
|
|
153
154
|
}
|
|
154
155
|
running = false;
|
|
155
156
|
hasFinishedBefore = false;
|
|
156
|
-
unexpectedStop = false;
|
|
157
|
+
#unexpectedStop = false;
|
|
157
158
|
#log;
|
|
158
159
|
get log() {
|
|
159
160
|
return this.#log;
|
|
@@ -161,37 +162,43 @@ export class BasicCrawler {
|
|
|
161
162
|
requestHandler;
|
|
162
163
|
errorHandler;
|
|
163
164
|
failedRequestHandler;
|
|
165
|
+
// kept as TS-private: tests read it at runtime
|
|
164
166
|
requestHandlerTimeoutMillis;
|
|
165
167
|
internalTimeoutMillis;
|
|
166
168
|
maxRequestRetries;
|
|
167
169
|
maxCrawlDepth;
|
|
168
|
-
sameDomainDelayMillis;
|
|
169
|
-
domainAccessedTime;
|
|
170
|
+
#sameDomainDelayMillis;
|
|
171
|
+
#domainAccessedTime;
|
|
170
172
|
maxRequestsPerCrawl;
|
|
171
173
|
get handledRequestsCount() {
|
|
172
174
|
return this.stats.state.requestsFinished + this.stats.state.requestsFailed;
|
|
173
175
|
}
|
|
174
|
-
statusMessageLoggingInterval;
|
|
175
|
-
statusMessageCallback;
|
|
176
|
+
#statusMessageLoggingInterval;
|
|
177
|
+
#statusMessageCallback;
|
|
176
178
|
blockedStatusCodes = new Set();
|
|
177
179
|
additionalHttpErrorStatusCodes;
|
|
178
|
-
ignoreHttpErrorStatusCodes;
|
|
180
|
+
#ignoreHttpErrorStatusCodes;
|
|
179
181
|
/**
|
|
180
182
|
* The resolved options for the crawler's own task loop — the crawler-owned `runTaskFunction`, the (possibly
|
|
181
183
|
* user-overridden) ready/finished predicates and cadence/logging. Concurrency configuration lives on the
|
|
182
184
|
* {@link ConcurrencySystem} instead, and the loop's `consumer` identity is the crawler's own, so neither is
|
|
183
185
|
* settable here.
|
|
184
186
|
*/
|
|
187
|
+
// kept as TS-private: tests mutate it at runtime
|
|
185
188
|
taskLoopOptions;
|
|
186
189
|
httpClient;
|
|
187
190
|
retryOnBlocked;
|
|
188
|
-
respectRobotsTxtFile;
|
|
191
|
+
#respectRobotsTxtFile;
|
|
192
|
+
/** Whether `runInStorageTransaction()` opens a transaction at all. */
|
|
193
|
+
#transactionalStorageEnabled;
|
|
194
|
+
/** The resolved per-storage-type write policy overrides forwarded to each request's transaction. */
|
|
195
|
+
#storageWritePolicy;
|
|
189
196
|
onSkippedRequest;
|
|
190
|
-
|
|
191
|
-
loggedPerRun = new Set();
|
|
192
|
-
robotsTxtFileCache;
|
|
197
|
+
#closeEvents;
|
|
198
|
+
#loggedPerRun = new Set();
|
|
199
|
+
#robotsTxtFileCache;
|
|
193
200
|
identity;
|
|
194
|
-
contextPipelineOptions;
|
|
201
|
+
#contextPipelineOptions;
|
|
195
202
|
static optionsShape = {
|
|
196
203
|
contextPipelineBuilder: ow.optional.object,
|
|
197
204
|
extendContext: ow.optional.function,
|
|
@@ -219,6 +226,9 @@ export class BasicCrawler {
|
|
|
219
226
|
blockedStatusCodes: ow.optional.array.ofType(ow.number),
|
|
220
227
|
retryOnBlocked: ow.optional.boolean,
|
|
221
228
|
respectRobotsTxtFile: ow.optional.any(ow.boolean, ow.object),
|
|
229
|
+
transactionalStorage: ow.optional.any(ow.boolean, ow.object.exactShape({
|
|
230
|
+
requestQueue: ow.optional.string.oneOf(['deferred', 'writeThrough']),
|
|
231
|
+
})),
|
|
222
232
|
onSkippedRequest: ow.optional.function,
|
|
223
233
|
httpClient: ow.optional.object,
|
|
224
234
|
configuration: ow.optional.object,
|
|
@@ -246,7 +256,7 @@ export class BasicCrawler {
|
|
|
246
256
|
// Service locator options
|
|
247
257
|
configuration, storageBackend, eventManager, logger,
|
|
248
258
|
// AutoscaledPool shorthands
|
|
249
|
-
minConcurrency, maxConcurrency, maxRequestsPerMinute, blockedStatusCodes: blockedStatusCodesInput, retryOnBlocked = false, respectRobotsTxtFile = false, onSkippedRequest, requestHandler, requestHandlerTimeoutSecs, errorHandler, failedRequestHandler, statusMessageLoggingInterval = 10, statusMessageCallback, statistics, httpClient, id, } = options;
|
|
259
|
+
minConcurrency, maxConcurrency, maxRequestsPerMinute, blockedStatusCodes: blockedStatusCodesInput, retryOnBlocked = false, respectRobotsTxtFile = false, transactionalStorage, onSkippedRequest, requestHandler, requestHandlerTimeoutSecs, errorHandler, failedRequestHandler, statusMessageLoggingInterval = 10, statusMessageCallback, statistics, httpClient, id, } = options;
|
|
250
260
|
// All concurrency configuration lives on the `ConcurrencySystem`, so the shortcuts have nowhere to go once
|
|
251
261
|
// one is supplied - and silently dropping a `maxConcurrency` the user asked for is how crawls end up
|
|
252
262
|
// hammering a site.
|
|
@@ -271,7 +281,7 @@ export class BasicCrawler {
|
|
|
271
281
|
}
|
|
272
282
|
try {
|
|
273
283
|
serviceLocatorScope.enterScope();
|
|
274
|
-
this
|
|
284
|
+
this.#contextPipelineOptions = {
|
|
275
285
|
contextPipelineBuilder: options.contextPipelineBuilder,
|
|
276
286
|
extendContext: options.extendContext,
|
|
277
287
|
};
|
|
@@ -302,13 +312,13 @@ export class BasicCrawler {
|
|
|
302
312
|
}
|
|
303
313
|
this.httpClient = httpClient ?? new LazyDefaultHttpClient({ logger: this.log });
|
|
304
314
|
this.proxyConfiguration = proxyConfiguration;
|
|
305
|
-
this
|
|
306
|
-
this
|
|
307
|
-
this
|
|
308
|
-
this
|
|
315
|
+
this.#statusMessageLoggingInterval = statusMessageLoggingInterval;
|
|
316
|
+
this.#statusMessageCallback = statusMessageCallback;
|
|
317
|
+
this.#domainAccessedTime = new Map();
|
|
318
|
+
this.#robotsTxtFileCache = new LruCache({ maxLength: 1000 });
|
|
309
319
|
this.handleSkippedRequest = this.handleSkippedRequest.bind(this);
|
|
310
320
|
this.additionalHttpErrorStatusCodes = new Set([...additionalHttpErrorStatusCodes]);
|
|
311
|
-
this
|
|
321
|
+
this.#ignoreHttpErrorStatusCodes = new Set([...ignoreHttpErrorStatusCodes]);
|
|
312
322
|
this.requestHandler = requestHandler ?? this.router;
|
|
313
323
|
this.failedRequestHandler = failedRequestHandler;
|
|
314
324
|
this.errorHandler = errorHandler;
|
|
@@ -319,7 +329,11 @@ export class BasicCrawler {
|
|
|
319
329
|
this.requestHandlerTimeoutMillis = 60_000;
|
|
320
330
|
}
|
|
321
331
|
this.retryOnBlocked = retryOnBlocked;
|
|
322
|
-
this
|
|
332
|
+
this.#respectRobotsTxtFile = respectRobotsTxtFile;
|
|
333
|
+
// The cast undoes ow's assertion signature, which mangles `boolean | object` unions.
|
|
334
|
+
const transactionalStorageOption = transactionalStorage;
|
|
335
|
+
this.#transactionalStorageEnabled = transactionalStorageOption !== false;
|
|
336
|
+
this.#storageWritePolicy = typeof transactionalStorageOption === 'object' ? transactionalStorageOption : {};
|
|
323
337
|
this.onSkippedRequest = onSkippedRequest;
|
|
324
338
|
// allow at least 5min for internal timeouts
|
|
325
339
|
this.internalTimeoutMillis =
|
|
@@ -327,8 +341,8 @@ export class BasicCrawler {
|
|
|
327
341
|
Math.max(this.requestHandlerTimeoutMillis * 2, 300e3);
|
|
328
342
|
this.maxRequestRetries = maxRequestRetries;
|
|
329
343
|
this.maxCrawlDepth = maxCrawlDepth;
|
|
330
|
-
this
|
|
331
|
-
this
|
|
344
|
+
this.#sameDomainDelayMillis = sameDomainDelaySecs * 1000;
|
|
345
|
+
this.#statsDep = OwnedOrInjected.resolve(statistics, () => new Statistics({
|
|
332
346
|
logMessage: `${this.constructor.name} request statistics:`,
|
|
333
347
|
log: this.log,
|
|
334
348
|
id: this.identity.id,
|
|
@@ -339,7 +353,7 @@ export class BasicCrawler {
|
|
|
339
353
|
'`proxyInfo` they were created with. Configure proxies on the pool instead, ' +
|
|
340
354
|
'e.g. via `addSession({ proxyInfo })` or a custom `createSessionFunction`.');
|
|
341
355
|
}
|
|
342
|
-
this
|
|
356
|
+
this.#sessionPoolDep = OwnedOrInjected.resolve(sessionPool, () => new SessionPool({
|
|
343
357
|
createSessionFunction: async (opts) => new Session({
|
|
344
358
|
...opts?.sessionOptions,
|
|
345
359
|
proxyInfo: opts?.sessionOptions?.proxyInfo ?? (await this.proxyConfiguration?.newProxyInfo()),
|
|
@@ -376,12 +390,15 @@ export class BasicCrawler {
|
|
|
376
390
|
this.stats.startJob(request.id || request.uniqueKey);
|
|
377
391
|
const crawlingContext = { request };
|
|
378
392
|
try {
|
|
393
|
+
// The transaction spans the whole pipeline call, covering the navigation hooks
|
|
394
|
+
// and `extendContext` too; `handleRequest` drives its outcome explicitly.
|
|
395
|
+
await this.runInStorageTransaction(async () =>
|
|
379
396
|
// Navigation, the navigation hooks and the request handler are timed individually, but the
|
|
380
397
|
// phases between them are not, so a request could still get stuck indefinitely. This is the
|
|
381
398
|
// catch-all for that - see `raceWithTimeout` for why it is a bare timer, not a timeout frame.
|
|
382
399
|
await this.withRequestTimeout(crawlingContext, this.basicContextPipeline
|
|
383
400
|
.chain(this.contextPipeline)
|
|
384
|
-
.call(crawlingContext, (ctx) => this.handleRequest(ctx, source, request)));
|
|
401
|
+
.call(crawlingContext, (ctx) => this.handleRequest(ctx, source, request))));
|
|
385
402
|
}
|
|
386
403
|
catch (error) {
|
|
387
404
|
// ContextPipelineInterruptedError means the request was intentionally skipped
|
|
@@ -421,7 +438,7 @@ export class BasicCrawler {
|
|
|
421
438
|
`${this.maxRequestsPerCrawl} requests and will shut down soon. Requests that are in progress will be allowed to finish.`);
|
|
422
439
|
return false;
|
|
423
440
|
}
|
|
424
|
-
if (this
|
|
441
|
+
if (this.#unexpectedStop) {
|
|
425
442
|
this.logOncePerRun('shuttingDown', 'No new requests are allowed because the `stop()` method has been called. ' +
|
|
426
443
|
'Ongoing requests will be allowed to complete.');
|
|
427
444
|
return false;
|
|
@@ -435,7 +452,7 @@ export class BasicCrawler {
|
|
|
435
452
|
`In total, the crawler processed ${this.handledRequestsCount} requests and will shut down.`);
|
|
436
453
|
return true;
|
|
437
454
|
}
|
|
438
|
-
if (this
|
|
455
|
+
if (this.#unexpectedStop) {
|
|
439
456
|
this.log.info('The crawler has finished all the remaining ongoing requests and will shut down now.');
|
|
440
457
|
return true;
|
|
441
458
|
}
|
|
@@ -453,7 +470,7 @@ export class BasicCrawler {
|
|
|
453
470
|
log: this.log,
|
|
454
471
|
};
|
|
455
472
|
this.taskLoopOptions = { ...taskLoopOptions, ...crawlerOwnedTaskLoopConfiguration };
|
|
456
|
-
this
|
|
473
|
+
this.#resolveConcurrencySystem = () => OwnedOrInjected.resolve(concurrencySystem, () => this.createDefaultConcurrencySystem({
|
|
457
474
|
minConcurrency,
|
|
458
475
|
maxConcurrency,
|
|
459
476
|
maxTasksPerMinute: maxRequestsPerMinute,
|
|
@@ -482,7 +499,7 @@ export class BasicCrawler {
|
|
|
482
499
|
* @returns `true` if the status code is considered an error, `false` otherwise
|
|
483
500
|
*/
|
|
484
501
|
isErrorStatusCode(status) {
|
|
485
|
-
const excludeError = this
|
|
502
|
+
const excludeError = this.#ignoreHttpErrorStatusCodes.has(status);
|
|
486
503
|
const includeError = this.additionalHttpErrorStatusCodes.has(status);
|
|
487
504
|
return (status >= 500 && !excludeError) || includeError;
|
|
488
505
|
}
|
|
@@ -575,7 +592,7 @@ export class BasicCrawler {
|
|
|
575
592
|
return { enqueueLinks: enqueueLinksWrapper, addRequests, sendRequest };
|
|
576
593
|
}
|
|
577
594
|
buildFinalContextPipeline() {
|
|
578
|
-
const subclassPipeline = (this
|
|
595
|
+
const subclassPipeline = (this.#contextPipelineOptions.contextPipelineBuilder?.() ??
|
|
579
596
|
this.buildContextPipeline());
|
|
580
597
|
// `extendContext` runs *before* the subclass navigation pipeline (which includes the
|
|
581
598
|
// pre/post-navigation hooks). This makes the extension visible to those hooks and to the
|
|
@@ -587,7 +604,7 @@ export class BasicCrawler {
|
|
|
587
604
|
// TypeScript cannot express that `Context` transitively includes `ContextExtension` here. The
|
|
588
605
|
// casts below are sound because `buildFinalContextPipeline` is declared to return the fully
|
|
589
606
|
// resolved `ExtendedContext` (= `Context & ContextExtension`).
|
|
590
|
-
const { extendContext } = this
|
|
607
|
+
const { extendContext } = this.#contextPipelineOptions;
|
|
591
608
|
let contextPipeline;
|
|
592
609
|
if (extendContext !== undefined) {
|
|
593
610
|
contextPipeline = ContextPipeline.create()
|
|
@@ -621,7 +638,7 @@ export class BasicCrawler {
|
|
|
621
638
|
* @param error The error to check.
|
|
622
639
|
*/
|
|
623
640
|
isProxyError(error) {
|
|
624
|
-
return ROTATE_PROXY_ERRORS.some((x) => this.
|
|
641
|
+
return ROTATE_PROXY_ERRORS.some((x) => this.getMessageFromError(error)?.includes(x));
|
|
625
642
|
}
|
|
626
643
|
/**
|
|
627
644
|
* Sets the status message for the current crawler run.
|
|
@@ -662,14 +679,14 @@ export class BasicCrawler {
|
|
|
662
679
|
const { mode: operationMode, failedDelta } = getOperationMode();
|
|
663
680
|
let message;
|
|
664
681
|
if (operationMode === 'ERROR') {
|
|
665
|
-
message = `Experiencing problems, ${failedDelta} failed requests in the past ${this
|
|
682
|
+
message = `Experiencing problems, ${failedDelta} failed requests in the past ${this.#statusMessageLoggingInterval} seconds.`;
|
|
666
683
|
}
|
|
667
684
|
else {
|
|
668
685
|
const total = await this.requestManager?.getTotalCount();
|
|
669
686
|
message = `Crawled ${this.stats.state.requestsFinished}${total ? `/${total}` : ''} pages, ${this.stats.state.requestsFailed} failed requests, desired concurrency ${this.concurrencySystem?.desiredConcurrency ?? 0}.`;
|
|
670
687
|
}
|
|
671
|
-
if (this
|
|
672
|
-
await this
|
|
688
|
+
if (this.#statusMessageCallback) {
|
|
689
|
+
await this.#statusMessageCallback({
|
|
673
690
|
crawler: this,
|
|
674
691
|
state: this.stats.state,
|
|
675
692
|
previousState,
|
|
@@ -679,7 +696,7 @@ export class BasicCrawler {
|
|
|
679
696
|
}
|
|
680
697
|
this.setStatusMessage(message);
|
|
681
698
|
};
|
|
682
|
-
const interval = setInterval(log, this
|
|
699
|
+
const interval = setInterval(log, this.#statusMessageLoggingInterval * 1e3);
|
|
683
700
|
return { log, stop: () => clearInterval(interval) };
|
|
684
701
|
}
|
|
685
702
|
/**
|
|
@@ -707,20 +724,20 @@ export class BasicCrawler {
|
|
|
707
724
|
// When `purgeRequestQueue` is explicitly `true`, we also purge a user-supplied manager.
|
|
708
725
|
// When `purgeRequestQueue` is explicitly `false`, nothing is purged.
|
|
709
726
|
const shouldPurge = purgeRequestQueue !== false;
|
|
710
|
-
const managerToPurge = this
|
|
727
|
+
const managerToPurge = this.#ownedRequestQueue.maybeValue ?? (purgeRequestQueue === true ? this.requestManager : undefined);
|
|
711
728
|
if (managerToPurge?.purge && shouldPurge) {
|
|
712
729
|
await managerToPurge.purge();
|
|
713
730
|
}
|
|
714
731
|
// A supplied statistics instance keeps whatever state it was handed - only wipe a default we built.
|
|
715
|
-
await this
|
|
732
|
+
await this.#statsDep.ifOwned(async (stats) => {
|
|
716
733
|
stats.reset();
|
|
717
734
|
await stats.resetStore();
|
|
718
735
|
});
|
|
719
|
-
await this
|
|
736
|
+
await this.#sessionPoolDep.ifOwned((pool) => pool.resetStore());
|
|
720
737
|
}
|
|
721
|
-
this
|
|
738
|
+
this.#unexpectedStop = false;
|
|
722
739
|
this.running = true;
|
|
723
|
-
this
|
|
740
|
+
this.#loggedPerRun.clear();
|
|
724
741
|
await purgeDefaultStorages({
|
|
725
742
|
onlyPurgeOnce: true,
|
|
726
743
|
storageBackend: serviceLocator.getStorageBackend(),
|
|
@@ -730,7 +747,7 @@ export class BasicCrawler {
|
|
|
730
747
|
await this.addRequests(requests, addRequestsOptions);
|
|
731
748
|
}
|
|
732
749
|
try {
|
|
733
|
-
await this.
|
|
750
|
+
await this.init();
|
|
734
751
|
await this.stats.startCapturing();
|
|
735
752
|
}
|
|
736
753
|
catch (error) {
|
|
@@ -747,7 +764,7 @@ export class BasicCrawler {
|
|
|
747
764
|
const sigintHandler = async () => {
|
|
748
765
|
this.log.warning('Pausing... Press CTRL+C again to force exit. To resume, do: CRAWLEE_PURGE_ON_START=0 npm start');
|
|
749
766
|
await this.pauseOnMigration();
|
|
750
|
-
await this
|
|
767
|
+
await this.#autoscaledPool.abort();
|
|
751
768
|
};
|
|
752
769
|
// Attach a listener to handle migration and aborting events gracefully.
|
|
753
770
|
const boundPauseOnMigration = this.pauseOnMigration.bind(this);
|
|
@@ -757,7 +774,7 @@ export class BasicCrawler {
|
|
|
757
774
|
eventManager.on("aborting" /* EventType.ABORTING */, boundPauseOnMigration);
|
|
758
775
|
let stats = {};
|
|
759
776
|
try {
|
|
760
|
-
await this
|
|
777
|
+
await this.#autoscaledPool.run();
|
|
761
778
|
}
|
|
762
779
|
finally {
|
|
763
780
|
await this.teardown();
|
|
@@ -807,11 +824,11 @@ export class BasicCrawler {
|
|
|
807
824
|
* To stop the crawler immediately, use {@link BasicCrawler.teardown|`crawler.teardown()`} instead.
|
|
808
825
|
*/
|
|
809
826
|
stop(reason = 'The crawler has been gracefully stopped.') {
|
|
810
|
-
if (this
|
|
827
|
+
if (this.#unexpectedStop) {
|
|
811
828
|
return;
|
|
812
829
|
}
|
|
813
830
|
this.log.info(reason);
|
|
814
|
-
this
|
|
831
|
+
this.#unexpectedStop = true;
|
|
815
832
|
}
|
|
816
833
|
/**
|
|
817
834
|
* Stops dispatching new requests, letting the in-progress ones finish. Resolves once they have settled, or rejects
|
|
@@ -822,22 +839,22 @@ export class BasicCrawler {
|
|
|
822
839
|
* throughout, since a shared one may still be serving other crawlers.
|
|
823
840
|
*/
|
|
824
841
|
async pause(timeoutSecs) {
|
|
825
|
-
if (!this
|
|
842
|
+
if (!this.#autoscaledPool) {
|
|
826
843
|
this.log.warning('Cannot pause a crawler that is not running.');
|
|
827
844
|
return;
|
|
828
845
|
}
|
|
829
|
-
await this
|
|
846
|
+
await this.#autoscaledPool.pause(timeoutSecs);
|
|
830
847
|
}
|
|
831
848
|
/**
|
|
832
849
|
* Resumes a run suspended with {@link BasicCrawler.pause|`pause()`}, letting the crawler dispatch requests
|
|
833
850
|
* again. A no-op on a crawler that is not paused.
|
|
834
851
|
*/
|
|
835
852
|
resume() {
|
|
836
|
-
if (!this
|
|
853
|
+
if (!this.#autoscaledPool) {
|
|
837
854
|
this.log.warning('Cannot resume a crawler that is not running.');
|
|
838
855
|
return;
|
|
839
856
|
}
|
|
840
|
-
this
|
|
857
|
+
this.#autoscaledPool.resume();
|
|
841
858
|
}
|
|
842
859
|
/**
|
|
843
860
|
* Returns the crawler's {@link IRequestManager|request manager}, opening the default {@link RequestQueue}
|
|
@@ -850,8 +867,8 @@ export class BasicCrawler {
|
|
|
850
867
|
// Apply the processing-time hint here (an async lifecycle point) rather than in the constructor,
|
|
851
868
|
// now that `setExpectedRequestProcessingTimeSecs` is async. The hint is raise-only and idempotent,
|
|
852
869
|
// but guard so we do not re-issue it on every call.
|
|
853
|
-
if (!this
|
|
854
|
-
this
|
|
870
|
+
if (!this.#requestManagerTimeoutsApplied) {
|
|
871
|
+
this.#requestManagerTimeoutsApplied = true;
|
|
855
872
|
await this.applyRequestManagerTimeouts(this.requestManager);
|
|
856
873
|
}
|
|
857
874
|
return this.requestManager;
|
|
@@ -873,7 +890,7 @@ export class BasicCrawler {
|
|
|
873
890
|
// subsequent instances get their own queue via a unique alias so they don't collide.
|
|
874
891
|
const identifier = this.identity.instanceIndex === 0 ? null : { alias: `__default_${this.identity.id}__` };
|
|
875
892
|
const requestQueue = await RequestQueue.open(identifier, { configuration: serviceLocator.getConfiguration() });
|
|
876
|
-
return this
|
|
893
|
+
return this.#ownedRequestQueue.set(requestQueue);
|
|
877
894
|
}
|
|
878
895
|
/**
|
|
879
896
|
* Tells a request manager how long we expect to hold a fetched request, so that one backed by a
|
|
@@ -922,8 +939,8 @@ export class BasicCrawler {
|
|
|
922
939
|
const stateKey = `${BasicCrawler.CRAWLEE_STATE_KEY}_${this.identity.id}`;
|
|
923
940
|
return kvs.getAutoSavedValue(stateKey, defaultValue);
|
|
924
941
|
}
|
|
925
|
-
BasicCrawler
|
|
926
|
-
if (BasicCrawler
|
|
942
|
+
BasicCrawler.#useStateAnonymousIndices.add(this.identity.instanceIndex);
|
|
943
|
+
if (BasicCrawler.#useStateAnonymousIndices.size > 1) {
|
|
927
944
|
serviceLocator
|
|
928
945
|
.getLogger()
|
|
929
946
|
.warningOnce('Multiple crawler instances are calling useState() without an explicit `id` option. \n' +
|
|
@@ -944,19 +961,23 @@ export class BasicCrawler {
|
|
|
944
961
|
return Math.min(limit, explicitLimit ?? Infinity);
|
|
945
962
|
}
|
|
946
963
|
async handleSkippedRequest(options) {
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
964
|
+
// A skipped request is a *successful* outcome, but the interrupt still unwinds through the
|
|
965
|
+
// transaction scope, which rolls back - so the skip bookkeeping must write directly.
|
|
966
|
+
await withDirectStorageAccess(async () => {
|
|
967
|
+
if (options.reason === 'limit') {
|
|
968
|
+
this.logOncePerRun('maxRequestsPerCrawl', 'The number of requests enqueued by the crawler reached the maxRequestsPerCrawl limit of ' +
|
|
969
|
+
`${this.maxRequestsPerCrawl} requests and no further requests will be added.`);
|
|
970
|
+
}
|
|
971
|
+
if (options.reason === 'depth') {
|
|
972
|
+
this.logOncePerRun('maxCrawlDepth', `The crawler reached the maxCrawlDepth limit of ${this.maxCrawlDepth} and no further requests will be enqueued.`);
|
|
973
|
+
}
|
|
974
|
+
await this.onSkippedRequest?.(options);
|
|
975
|
+
});
|
|
955
976
|
}
|
|
956
977
|
logOncePerRun(key, message) {
|
|
957
|
-
if (!this
|
|
978
|
+
if (!this.#loggedPerRun.has(key)) {
|
|
958
979
|
this.log.info(message);
|
|
959
|
-
this
|
|
980
|
+
this.#loggedPerRun.add(key);
|
|
960
981
|
}
|
|
961
982
|
}
|
|
962
983
|
/**
|
|
@@ -1094,11 +1115,11 @@ export class BasicCrawler {
|
|
|
1094
1115
|
/**
|
|
1095
1116
|
* Initializes the crawler.
|
|
1096
1117
|
*/
|
|
1097
|
-
async
|
|
1118
|
+
async init() {
|
|
1098
1119
|
const eventManager = serviceLocator.getEventManager();
|
|
1099
1120
|
if (!eventManager.isInitialized()) {
|
|
1100
1121
|
await eventManager.init();
|
|
1101
|
-
this
|
|
1122
|
+
this.#closeEvents = true;
|
|
1102
1123
|
}
|
|
1103
1124
|
// Warn once at startup if the internal timeout is shorter than the phases it is meant to outlast. It is
|
|
1104
1125
|
// floored per request so it will not actually cut them short, but the configured value is then effectively
|
|
@@ -1113,11 +1134,11 @@ export class BasicCrawler {
|
|
|
1113
1134
|
// An owned governor is rebuilt (and started) for every run, so it always starts from a clean slate — stale
|
|
1114
1135
|
// resource snapshots or a previous run's scaled desired concurrency would otherwise distort this run's
|
|
1115
1136
|
// scaling. An injected one is long-lived and its lifecycle belongs to the caller.
|
|
1116
|
-
this
|
|
1117
|
-
await this
|
|
1118
|
-
this
|
|
1137
|
+
this.#concurrencySystemDep = this.#resolveConcurrencySystem();
|
|
1138
|
+
await this.#concurrencySystemDep.ifOwned((system) => system.start());
|
|
1139
|
+
this.#autoscaledPool = new AutoscaledPool({
|
|
1119
1140
|
...this.taskLoopOptions,
|
|
1120
|
-
concurrencySystem: this
|
|
1141
|
+
concurrencySystem: this.#concurrencySystemDep.value,
|
|
1121
1142
|
consumer: this.identity,
|
|
1122
1143
|
});
|
|
1123
1144
|
await this.getRequestManager();
|
|
@@ -1168,10 +1189,44 @@ export class BasicCrawler {
|
|
|
1168
1189
|
const timeoutMillis = this.resolveRequestHandlerTimeoutMillis(crawlingContext.request.label);
|
|
1169
1190
|
await addTimeoutToPromise(async () => this.requestHandler(crawlingContext), timeoutMillis, `requestHandler timed out after ${timeoutMillis / 1000} seconds (${crawlingContext.request.id}).`);
|
|
1170
1191
|
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Runs `callback` inside a {@link StorageTransaction}, unless transactional storage is disabled.
|
|
1194
|
+
* Deliberately does **not** commit on return - `handleRequest` swallows request handler failures, so
|
|
1195
|
+
* a normal return says nothing about success. `handleRequest` owns the outcome.
|
|
1196
|
+
*/
|
|
1197
|
+
async runInStorageTransaction(callback) {
|
|
1198
|
+
if (!this.#transactionalStorageEnabled) {
|
|
1199
|
+
return callback();
|
|
1200
|
+
}
|
|
1201
|
+
const transaction = createStorageTransaction({
|
|
1202
|
+
policy: this.#storageWritePolicy,
|
|
1203
|
+
commitTimeoutMillis: this.internalTimeoutMillis,
|
|
1204
|
+
});
|
|
1205
|
+
let threw = true;
|
|
1206
|
+
try {
|
|
1207
|
+
const result = await transaction.run(callback);
|
|
1208
|
+
threw = false;
|
|
1209
|
+
return result;
|
|
1210
|
+
}
|
|
1211
|
+
finally {
|
|
1212
|
+
if (transaction.state === 'open') {
|
|
1213
|
+
// `handleRequest` commits or rolls back on every normal path, so an open transaction on
|
|
1214
|
+
// a normal return is a wiring bug; on a propagating throw (a pipeline-level failure) it is
|
|
1215
|
+
// expected. Either way, discard the unvalidated writes; only the former is worth flagging.
|
|
1216
|
+
if (!threw) {
|
|
1217
|
+
this.log.error('Internal error: a storage transaction was still open after the request pipeline ' +
|
|
1218
|
+
'returned normally. Its writes are being discarded. Please report this.');
|
|
1219
|
+
}
|
|
1220
|
+
transaction.rollback();
|
|
1221
|
+
}
|
|
1222
|
+
// Unconditional: `failed` is a terminal state that the branch above never reaches.
|
|
1223
|
+
transaction.dispose();
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1171
1226
|
/**
|
|
1172
1227
|
* Handles blocked request
|
|
1173
1228
|
*/
|
|
1174
|
-
|
|
1229
|
+
throwOnBlockedRequest(statusCode) {
|
|
1175
1230
|
if (this.retryOnBlocked)
|
|
1176
1231
|
return;
|
|
1177
1232
|
if (this.blockedStatusCodes.has(statusCode)) {
|
|
@@ -1179,25 +1234,25 @@ export class BasicCrawler {
|
|
|
1179
1234
|
}
|
|
1180
1235
|
}
|
|
1181
1236
|
async isAllowedBasedOnRobotsTxtFile(url) {
|
|
1182
|
-
if (!this
|
|
1237
|
+
if (!this.#respectRobotsTxtFile) {
|
|
1183
1238
|
return true;
|
|
1184
1239
|
}
|
|
1185
1240
|
const robotsTxtFile = await this.getRobotsTxtFileForUrl(url);
|
|
1186
|
-
const userAgent = typeof this
|
|
1241
|
+
const userAgent = typeof this.#respectRobotsTxtFile === 'object' ? this.#respectRobotsTxtFile?.userAgent : '*';
|
|
1187
1242
|
return !robotsTxtFile || robotsTxtFile.isAllowed(url, userAgent);
|
|
1188
1243
|
}
|
|
1189
1244
|
async getRobotsTxtFileForUrl(url) {
|
|
1190
|
-
if (!this
|
|
1245
|
+
if (!this.#respectRobotsTxtFile) {
|
|
1191
1246
|
return undefined;
|
|
1192
1247
|
}
|
|
1193
1248
|
try {
|
|
1194
1249
|
const origin = new URL(url).origin;
|
|
1195
|
-
const cachedRobotsTxtFile = this
|
|
1250
|
+
const cachedRobotsTxtFile = this.#robotsTxtFileCache.get(origin);
|
|
1196
1251
|
if (cachedRobotsTxtFile) {
|
|
1197
1252
|
return cachedRobotsTxtFile;
|
|
1198
1253
|
}
|
|
1199
1254
|
const robotsTxtFile = await RobotsTxtFile.find(url, { logger: this.log });
|
|
1200
|
-
this
|
|
1255
|
+
this.#robotsTxtFileCache.add(origin, robotsTxtFile);
|
|
1201
1256
|
return robotsTxtFile;
|
|
1202
1257
|
}
|
|
1203
1258
|
catch (e) {
|
|
@@ -1206,9 +1261,9 @@ export class BasicCrawler {
|
|
|
1206
1261
|
}
|
|
1207
1262
|
}
|
|
1208
1263
|
async pauseOnMigration() {
|
|
1209
|
-
if (this
|
|
1264
|
+
if (this.#autoscaledPool) {
|
|
1210
1265
|
// if run wasn't called, this is going to crash
|
|
1211
|
-
await this
|
|
1266
|
+
await this.#autoscaledPool.pause(SAFE_MIGRATION_WAIT_MILLIS).catch((err) => {
|
|
1212
1267
|
if (err.message.includes('running tasks did not finish')) {
|
|
1213
1268
|
this.log.error('The crawler was paused due to migration to another host, ' +
|
|
1214
1269
|
"but some requests did not finish in time. Those requests' results may be duplicated.");
|
|
@@ -1259,12 +1314,12 @@ export class BasicCrawler {
|
|
|
1259
1314
|
return false;
|
|
1260
1315
|
}
|
|
1261
1316
|
const now = Date.now();
|
|
1262
|
-
const lastAccessTime = this
|
|
1263
|
-
if (!lastAccessTime || now - lastAccessTime >= this
|
|
1264
|
-
this
|
|
1317
|
+
const lastAccessTime = this.#domainAccessedTime.get(domain);
|
|
1318
|
+
if (!lastAccessTime || now - lastAccessTime >= this.#sameDomainDelayMillis) {
|
|
1319
|
+
this.#domainAccessedTime.set(domain, now);
|
|
1265
1320
|
return false;
|
|
1266
1321
|
}
|
|
1267
|
-
const delay = lastAccessTime + this
|
|
1322
|
+
const delay = lastAccessTime + this.#sameDomainDelayMillis - now;
|
|
1268
1323
|
this.log.debug(`Request ${request.url} (${request.id}) will be reclaimed after ${delay} milliseconds due to same domain delay`);
|
|
1269
1324
|
setTimeout(async () => {
|
|
1270
1325
|
this.log.debug(`Adding request ${request.url} (${request.id}) back to the queue`);
|
|
@@ -1281,10 +1336,15 @@ export class BasicCrawler {
|
|
|
1281
1336
|
return;
|
|
1282
1337
|
}
|
|
1283
1338
|
const statisticsId = request.id || request.uniqueKey;
|
|
1339
|
+
// Opened by `runInStorageTransaction`; absent when disabled or when the subclass opens its own.
|
|
1340
|
+
const transaction = currentStorageTransaction();
|
|
1284
1341
|
let isRequestLocked = true;
|
|
1285
1342
|
try {
|
|
1286
1343
|
request.state = RequestState.REQUEST_HANDLER;
|
|
1287
1344
|
await this.runRequestHandler(crawlingContext);
|
|
1345
|
+
// Commit *before* marking the request as handled, so a commit failure fails the request and
|
|
1346
|
+
// it is retried. This also closes the transaction, so everything below passes through.
|
|
1347
|
+
await transaction?.commit();
|
|
1288
1348
|
await this.timeoutAndRetry(async () => requestSource.markRequestAsHandled(request), this.internalTimeoutMillis, `Marking request ${request.url} (${request.id}) as handled timed out after ${this.internalTimeoutMillis / 1e3} seconds.`);
|
|
1289
1349
|
isRequestLocked = false; // markRequestAsHandled succeeded and unlocked the request
|
|
1290
1350
|
this.stats.finishJob(statisticsId, request.retryCount);
|
|
@@ -1293,6 +1353,9 @@ export class BasicCrawler {
|
|
|
1293
1353
|
crawlingContext.session.markGood();
|
|
1294
1354
|
}
|
|
1295
1355
|
catch (rawError) {
|
|
1356
|
+
// Roll back *before* any error handler runs - error handlers write to real storage precisely
|
|
1357
|
+
// because the transaction is already closed. A no-op when the commit above succeeded.
|
|
1358
|
+
transaction?.rollback();
|
|
1296
1359
|
const err = this.unwrapError(rawError);
|
|
1297
1360
|
try {
|
|
1298
1361
|
request.state = RequestState.ERROR_HANDLER;
|
|
@@ -1374,7 +1437,7 @@ export class BasicCrawler {
|
|
|
1374
1437
|
return await enqueueLinks({
|
|
1375
1438
|
requestManager,
|
|
1376
1439
|
robotsTxtFile: await this.getRobotsTxtFileForUrl(request.url),
|
|
1377
|
-
respectRobotsTxtFile: this
|
|
1440
|
+
respectRobotsTxtFile: this.#respectRobotsTxtFile,
|
|
1378
1441
|
onSkippedRequest,
|
|
1379
1442
|
limit: await this.calculateEnqueuedRequestLimit(options.limit),
|
|
1380
1443
|
// Allow user options to override defaults set above ⤴
|
|
@@ -1461,7 +1524,7 @@ export class BasicCrawler {
|
|
|
1461
1524
|
const { url, retryCount, id } = request;
|
|
1462
1525
|
// We don't want to see the stack trace in the logs by default, when we are going to retry the request.
|
|
1463
1526
|
// Thus, we print the full stack trace only when CRAWLEE_VERBOSE_LOG environment variable is set to true.
|
|
1464
|
-
const message = this.
|
|
1527
|
+
const message = this.getMessageFromError(error);
|
|
1465
1528
|
this.log.warning(`Reclaiming failed request back to the list or queue. ${message}`, {
|
|
1466
1529
|
id,
|
|
1467
1530
|
url,
|
|
@@ -1494,7 +1557,7 @@ export class BasicCrawler {
|
|
|
1494
1557
|
async handleFailedRequestHandler(crawlingContext, error) {
|
|
1495
1558
|
// Always log the last error regardless if the user provided a failedRequestHandler
|
|
1496
1559
|
const { id, url, method, uniqueKey } = crawlingContext.request;
|
|
1497
|
-
const message = this.
|
|
1560
|
+
const message = this.getMessageFromError(error, true);
|
|
1498
1561
|
this.log.error(`Request failed and reached maximum retries. ${message}`, { id, url, method, uniqueKey });
|
|
1499
1562
|
if (this.failedRequestHandler) {
|
|
1500
1563
|
await this.failedRequestHandler?.(crawlingContext, // valid cast - ExtendedContext transitively extends CrawlingContext
|
|
@@ -1506,7 +1569,7 @@ export class BasicCrawler {
|
|
|
1506
1569
|
* @param error The error received
|
|
1507
1570
|
* @returns The message to be logged
|
|
1508
1571
|
*/
|
|
1509
|
-
|
|
1572
|
+
getMessageFromError(error, forceStack = false) {
|
|
1510
1573
|
if ([TypeError, SyntaxError, ReferenceError].some((type) => error instanceof type)) {
|
|
1511
1574
|
forceStack = true;
|
|
1512
1575
|
}
|
|
@@ -1542,14 +1605,14 @@ export class BasicCrawler {
|
|
|
1542
1605
|
*/
|
|
1543
1606
|
async teardown() {
|
|
1544
1607
|
serviceLocator.getEventManager().emit("persistState" /* EventType.PERSIST_STATE */, { isMigrating: false });
|
|
1545
|
-
if (this
|
|
1608
|
+
if (this.#closeEvents) {
|
|
1546
1609
|
await serviceLocator.getEventManager().close();
|
|
1547
1610
|
}
|
|
1548
|
-
await this
|
|
1549
|
-
await this
|
|
1550
|
-
await this
|
|
1611
|
+
await this.#sessionPoolDep.ifOwned((pool) => pool.teardown());
|
|
1612
|
+
await this.#autoscaledPool?.abort();
|
|
1613
|
+
await this.#concurrencySystemDep?.ifOwned((system) => system.stop());
|
|
1551
1614
|
}
|
|
1552
|
-
|
|
1615
|
+
getCookieHeaderFromRequest(request) {
|
|
1553
1616
|
if (request.headers?.Cookie && request.headers?.cookie) {
|
|
1554
1617
|
this.log.warning(`Encountered mixed casing for the cookie headers for request ${request.url} (${request.id}). Their values will be merged.`);
|
|
1555
1618
|
return mergeCookies(request.url, [request.headers.cookie, request.headers.Cookie]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crawlee/basic",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.106",
|
|
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.
|
|
46
|
-
"@crawlee/http-client": "4.0.0-beta.
|
|
47
|
-
"@crawlee/types": "4.0.0-beta.
|
|
48
|
-
"@crawlee/utils": "4.0.0-beta.
|
|
45
|
+
"@crawlee/core": "4.0.0-beta.106",
|
|
46
|
+
"@crawlee/http-client": "4.0.0-beta.106",
|
|
47
|
+
"@crawlee/types": "4.0.0-beta.106",
|
|
48
|
+
"@crawlee/utils": "4.0.0-beta.106",
|
|
49
49
|
"csv-stringify": "^6.5.2",
|
|
50
50
|
"ow": "^2.0.0",
|
|
51
51
|
"tldts": "^7.0.6",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"type-fest": "^4.41.0"
|
|
54
54
|
},
|
|
55
55
|
"optionalDependencies": {
|
|
56
|
-
"@crawlee/impit-client": "^4.0.0-beta.
|
|
56
|
+
"@crawlee/impit-client": "^4.0.0-beta.106"
|
|
57
57
|
},
|
|
58
58
|
"lerna": {
|
|
59
59
|
"command": {
|
|
@@ -62,5 +62,5 @@
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
},
|
|
65
|
-
"gitHead": "
|
|
65
|
+
"gitHead": "c622f1fc65e65221ea245817c58ecc0ffb4a5cb0"
|
|
66
66
|
}
|