@crawlee/basic 4.0.0-beta.183 → 4.0.0-beta.185
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/index.d.ts +1 -0
- package/index.js +1 -0
- package/internals/basic-crawler.d.ts +10 -4
- package/internals/basic-crawler.js +77 -60
- package/internals/crawlers/context_pipeline.d.ts +11 -10
- package/internals/crawlers/context_pipeline.js +11 -16
- package/internals/crawlers/crawler_commons.d.ts +3 -2
- package/internals/crawling_request.d.ts +86 -0
- package/internals/crawling_request.js +127 -0
- package/internals/enqueue_links/shared.d.ts +2 -1
- package/internals/enqueue_links/shared.js +1 -2
- package/internals/errors.d.ts +0 -3
- package/internals/errors.js +0 -5
- package/internals/router.d.ts +4 -4
- package/internals/throttling_request_manager.js +1 -1
- package/package.json +7 -7
package/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export * from './internals/autoscaling/index.js';
|
|
|
3
3
|
export * from './internals/basic-crawler.js';
|
|
4
4
|
export * from './internals/cookie_utils.js';
|
|
5
5
|
export * from './internals/crawlers/index.js';
|
|
6
|
+
export * from './internals/crawling_request.js';
|
|
6
7
|
export * from './internals/enqueue_links/index.js';
|
|
7
8
|
export * from './internals/errors.js';
|
|
8
9
|
export * from './internals/http.js';
|
package/index.js
CHANGED
|
@@ -3,6 +3,7 @@ export * from './internals/autoscaling/index.js';
|
|
|
3
3
|
export * from './internals/basic-crawler.js';
|
|
4
4
|
export * from './internals/cookie_utils.js';
|
|
5
5
|
export * from './internals/crawlers/index.js';
|
|
6
|
+
export * from './internals/crawling_request.js';
|
|
6
7
|
export * from './internals/enqueue_links/index.js';
|
|
7
8
|
export * from './internals/errors.js';
|
|
8
9
|
export * from './internals/http.js';
|
|
@@ -12,6 +12,7 @@ import { ConcurrencySystem } from './autoscaling/concurrency_system.js';
|
|
|
12
12
|
import type { FinalStatistics } from './autoscaling/system_status.js';
|
|
13
13
|
import { ContextPipeline } from './crawlers/context_pipeline.js';
|
|
14
14
|
import type { CrawlingContext, TypedRequestsLike } from './crawlers/crawler_commons.js';
|
|
15
|
+
import { CrawlingRequest } from './crawling_request.js';
|
|
15
16
|
import type { IStatistics, StatisticState } from './crawlers/statistics.js';
|
|
16
17
|
import type { EnqueueUrlsOptions, SkippedRequestCallback } from './enqueue_links/index.js';
|
|
17
18
|
import type { GetUserDataFromRequest, RouterHandler, RouterRoutes } from './router.js';
|
|
@@ -54,7 +55,7 @@ export interface BasicCrawlerOptions<Context extends CrawlingContext = CrawlingC
|
|
|
54
55
|
* To make this work, we should **always**
|
|
55
56
|
* let our function throw exceptions rather than catch them.
|
|
56
57
|
* The exceptions are logged to the request using the
|
|
57
|
-
* {@link
|
|
58
|
+
* {@link CrawlingRequest.pushErrorMessage|`request.pushErrorMessage()`} function.
|
|
58
59
|
*/
|
|
59
60
|
requestHandler?: RouterHandler<ExtendedContext, Routes> | RequestHandler<ExtendedContext>;
|
|
60
61
|
/**
|
|
@@ -441,7 +442,7 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
441
442
|
* This is used e.g. in the {@link AdaptivePlaywrightCrawler|`AdaptivePlaywrightCrawler`}.
|
|
442
443
|
*/
|
|
443
444
|
get basicContextPipeline(): ContextPipeline<{
|
|
444
|
-
request:
|
|
445
|
+
request: CrawlingRequest;
|
|
445
446
|
}, CrawlingContext>;
|
|
446
447
|
get contextPipeline(): ContextPipeline<CrawlingContext, ExtendedContext>;
|
|
447
448
|
/** Whether a `run()` on this instance has already finished - a repeated one continues where it left off. */
|
|
@@ -534,6 +535,11 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
534
535
|
*/
|
|
535
536
|
protected buildContextPipeline(): ContextPipeline<CrawlingContext, CrawlingContext>;
|
|
536
537
|
private createBaseContext;
|
|
538
|
+
/**
|
|
539
|
+
* Fetches the next request and rebuilds it as a {@link CrawlingRequest}. A copy rather than the manager's
|
|
540
|
+
* instance: managers key on `uniqueKey`/`id`, and it is this copy that flows back into `reclaimRequest` /
|
|
541
|
+
* `markRequestAsHandled`, so nothing observes the difference.
|
|
542
|
+
*/
|
|
537
543
|
private resolveRequest;
|
|
538
544
|
private resolveSession;
|
|
539
545
|
private createContextHelpers;
|
|
@@ -737,7 +743,7 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
737
743
|
private isTaskReadyFunction;
|
|
738
744
|
/**
|
|
739
745
|
* Unwraps errors thrown by the context pipeline to get the actual user error.
|
|
740
|
-
* RequestHandlerError and
|
|
746
|
+
* RequestHandlerError and ContextPipelineCleanupError wrap the actual error.
|
|
741
747
|
*/
|
|
742
748
|
private unwrapError;
|
|
743
749
|
/**
|
|
@@ -780,7 +786,7 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
780
786
|
private requestMatchesEnqueueStrategy;
|
|
781
787
|
}
|
|
782
788
|
export interface CreateContextOptions {
|
|
783
|
-
request:
|
|
789
|
+
request: CrawlingRequest;
|
|
784
790
|
session: ISession;
|
|
785
791
|
proxyInfo?: ProxyInfo;
|
|
786
792
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
|
-
import { bindMethodsToServiceLocator, Configuration, createStorageTransaction, Request, CriticalError, currentStorageTransaction, Dataset, EnqueueStrategy, EventManager, EventType, getObjectType, KeyValueStore, log, NonRetryableError, OwnedOrInjected, purgeDefaultStorages, RequestManagerTandem, RequestQueue,
|
|
3
|
+
import { bindMethodsToServiceLocator, Configuration, createStorageTransaction, Request, CriticalError, currentStorageTransaction, Dataset, EnqueueStrategy, EventManager, EventType, getObjectType, KeyValueStore, log, NonRetryableError, OwnedOrInjected, purgeDefaultStorages, RequestManagerTandem, RequestQueue, ServiceLocator, serviceLocator, SessionError, validators, withDirectStorageAccess, } from '@crawlee/core';
|
|
4
4
|
import { BaseHttpClient, FetchHttpClient } from '@crawlee/http-client';
|
|
5
5
|
import { isAsyncIterable, isIterable, parseArgument, ROTATE_PROXY_ERRORS, schemas } from '@crawlee/utils/internal';
|
|
6
6
|
import { RobotsTxtFile } from '@crawlee/utils';
|
|
@@ -12,7 +12,8 @@ import { cryptoRandomObjectId } from '@apify/utilities';
|
|
|
12
12
|
import { ConcurrencySystem } from './autoscaling/concurrency_system.js';
|
|
13
13
|
import { mergeCookies } from './cookie_utils.js';
|
|
14
14
|
import { ContextPipeline } from './crawlers/context_pipeline.js';
|
|
15
|
-
import {
|
|
15
|
+
import { CrawlingRequest, RequestState } from './crawling_request.js';
|
|
16
|
+
import { ContextPipelineCleanupError, ContextPipelineInterruptedError, MissingSessionError, NavigationSkippedError, PersistentRateLimitError, RequestHandlerError, RequestThrottledError, RetryRequestError, } from './errors.js';
|
|
16
17
|
import { Statistics } from './crawlers/statistics.js';
|
|
17
18
|
import { applyRequestTransform, buildEnqueueStrategyPatterns, constructUrlPatternObjects, createRequestOptions, createSkippedRequestArgs, filterRequestOptionsByPatterns, } from './enqueue_links/index.js';
|
|
18
19
|
import { parseRetryAfterHeader } from './http.js';
|
|
@@ -53,7 +54,6 @@ class LazyDefaultHttpClient extends BaseHttpClient {
|
|
|
53
54
|
* @ignore
|
|
54
55
|
*/
|
|
55
56
|
const SAFE_MIGRATION_WAIT_MILLIS = 20000;
|
|
56
|
-
const deferredCleanupKey = Symbol('deferredCleanup');
|
|
57
57
|
// The request timeout plumbing (the window helper, the context symbols, and the race) lives in its own module.
|
|
58
58
|
export { navigationDeadlineKey, remainingNavigationWindowMillis } from './request-timeout.js';
|
|
59
59
|
const urlPatternSchema = z.union([
|
|
@@ -462,6 +462,28 @@ export class BasicCrawler {
|
|
|
462
462
|
// matching `recordRequestStart`.
|
|
463
463
|
this.statistics.recordRequestStart(request.id || request.uniqueKey);
|
|
464
464
|
const crawlingContext = { request };
|
|
465
|
+
// Runs inside the pipeline call, so the deferred cleanups registered by earlier middlewares
|
|
466
|
+
// (e.g. closing the page) only fire after the error handler has seen the partial context.
|
|
467
|
+
const handleInitializationError = async (rawError) => {
|
|
468
|
+
// Roll back *before* the error handler runs, for the same reason `handleRequest` does.
|
|
469
|
+
currentStorageTransaction()?.rollback();
|
|
470
|
+
// ContextPipelineInterruptedError means the request was intentionally skipped
|
|
471
|
+
// (e.g., doesn't match enqueue strategy after redirect). Just return gracefully.
|
|
472
|
+
if (rawError instanceof ContextPipelineInterruptedError) {
|
|
473
|
+
this.statistics.discardRequestRecord(request.id || request.uniqueKey);
|
|
474
|
+
await this.timeoutAndRetry(async () => this.requestManager?.markRequestAsHandled(request), this.internalTimeoutMillis, `Marking request ${crawlingContext.request.url} (${crawlingContext.request.id}) as handled timed out after ${this.internalTimeoutMillis / 1e3} seconds.`);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
// An error during pipeline initialization (e.g., navigation timeout, session/proxy error,
|
|
478
|
+
// i.e. not in user's requestHandler) goes through the normal error flow.
|
|
479
|
+
const error = this.unwrapError(rawError);
|
|
480
|
+
await this.requestFunctionErrorHandler(error, crawlingContext, request, this.requestManager);
|
|
481
|
+
// SessionError already retired the session in `requestFunctionErrorHandler`;
|
|
482
|
+
// skip `markBad` to avoid double-counting usage/error score.
|
|
483
|
+
if (!this.errorAbsolvesSession(error)) {
|
|
484
|
+
crawlingContext.session?.markBad();
|
|
485
|
+
}
|
|
486
|
+
};
|
|
465
487
|
try {
|
|
466
488
|
// The transaction spans the whole pipeline call, covering the navigation hooks
|
|
467
489
|
// and `extendContext` too; `handleRequest` drives its outcome explicitly.
|
|
@@ -471,27 +493,15 @@ export class BasicCrawler {
|
|
|
471
493
|
// catch-all for that - see `raceWithTimeout` for why it is a bare timer, not a timeout frame.
|
|
472
494
|
await this.withRequestTimeout(crawlingContext, this.basicContextPipeline
|
|
473
495
|
.chain(this.contextPipeline)
|
|
474
|
-
.call(crawlingContext, (ctx) => this.handleRequest(ctx, source, request))));
|
|
496
|
+
.call(crawlingContext, (ctx) => this.handleRequest(ctx, source, request), handleInitializationError)));
|
|
475
497
|
}
|
|
476
498
|
catch (error) {
|
|
477
|
-
//
|
|
478
|
-
//
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
await this.timeoutAndRetry(async () => this.requestManager?.markRequestAsHandled(request), this.internalTimeoutMillis, `Marking request ${crawlingContext.request.url} (${crawlingContext.request.id}) as handled timed out after ${this.internalTimeoutMillis / 1e3} seconds.`);
|
|
482
|
-
return;
|
|
483
|
-
}
|
|
484
|
-
// If the error happened during pipeline initialization (e.g., navigation timeout, session/proxy error,
|
|
485
|
-
// i.e. not in user's requestHandler), handle it through the normal error flow. A bare `TimeoutError`
|
|
486
|
-
// here is the internal timeout above firing - anything else thrown inside the pipeline arrives wrapped.
|
|
487
|
-
const isPipelineError = error instanceof ContextPipelineInitializationError ||
|
|
488
|
-
error instanceof SessionError ||
|
|
489
|
-
error instanceof TimeoutError;
|
|
490
|
-
if (isPipelineError) {
|
|
499
|
+
// A bare `TimeoutError` is the internal timeout above firing; a `SessionError` can only be a
|
|
500
|
+
// secondary error from `handleRequest` - both take the normal error flow. Anything else thrown
|
|
501
|
+
// inside the pipeline arrives wrapped.
|
|
502
|
+
if (error instanceof SessionError || error instanceof TimeoutError) {
|
|
491
503
|
const unwrappedError = this.unwrapError(error);
|
|
492
504
|
await this.requestFunctionErrorHandler(unwrappedError, crawlingContext, request, this.requestManager);
|
|
493
|
-
// SessionError already retired the session in `requestFunctionErrorHandler`;
|
|
494
|
-
// skip `markBad` to avoid double-counting usage/error score.
|
|
495
505
|
if (!this.errorAbsolvesSession(unwrappedError)) {
|
|
496
506
|
crawlingContext.session?.markBad();
|
|
497
507
|
}
|
|
@@ -499,11 +509,6 @@ export class BasicCrawler {
|
|
|
499
509
|
}
|
|
500
510
|
throw this.unwrapError(error);
|
|
501
511
|
}
|
|
502
|
-
finally {
|
|
503
|
-
// Run request-scoped deferred cleanups only after the whole request lifecycle - including the user's error handler - has finished.
|
|
504
|
-
const deferredCleanup = crawlingContext[deferredCleanupKey] ?? [];
|
|
505
|
-
await Promise.all(deferredCleanup.map((fn) => fn().catch((cleanupError) => this.log.debug('Error in deferred cleanup', { error: cleanupError }))));
|
|
506
|
-
}
|
|
507
512
|
},
|
|
508
513
|
isTaskReadyFunction: async () => {
|
|
509
514
|
if (isMaxPagesExceeded()) {
|
|
@@ -593,10 +598,10 @@ export class BasicCrawler {
|
|
|
593
598
|
*/
|
|
594
599
|
buildBasicContextPipeline() {
|
|
595
600
|
return ContextPipeline.create()
|
|
596
|
-
.compose(
|
|
597
|
-
.compose(
|
|
598
|
-
.compose(
|
|
599
|
-
.compose(
|
|
601
|
+
.compose(this.checkRobotsTxt.bind(this))
|
|
602
|
+
.compose(this.createBaseContext.bind(this))
|
|
603
|
+
.compose(this.resolveSession.bind(this))
|
|
604
|
+
.compose(this.createContextHelpers.bind(this));
|
|
600
605
|
}
|
|
601
606
|
async checkRobotsTxt({ request }) {
|
|
602
607
|
if (!(await this.isAllowedBasedOnRobotsTxtFile(request.url))) {
|
|
@@ -618,8 +623,7 @@ export class BasicCrawler {
|
|
|
618
623
|
buildContextPipeline() {
|
|
619
624
|
return ContextPipeline.create();
|
|
620
625
|
}
|
|
621
|
-
createBaseContext(context) {
|
|
622
|
-
const deferredCleanup = [];
|
|
626
|
+
createBaseContext(context, onCleanup) {
|
|
623
627
|
return {
|
|
624
628
|
id: cryptoRandomObjectId(10),
|
|
625
629
|
log: this.log,
|
|
@@ -627,7 +631,14 @@ export class BasicCrawler {
|
|
|
627
631
|
useState: this.useState.bind(this),
|
|
628
632
|
getKeyValueStore: async (identifier) => KeyValueStore.open(identifier),
|
|
629
633
|
registerDeferredCleanup: (cleanup) => {
|
|
630
|
-
|
|
634
|
+
onCleanup(async () => {
|
|
635
|
+
try {
|
|
636
|
+
await cleanup();
|
|
637
|
+
}
|
|
638
|
+
catch (error) {
|
|
639
|
+
this.log.debug('Error in deferred cleanup', { error });
|
|
640
|
+
}
|
|
641
|
+
});
|
|
631
642
|
},
|
|
632
643
|
afterStorageCommit: (callback) => {
|
|
633
644
|
const transaction = currentStorageTransaction();
|
|
@@ -650,22 +661,28 @@ export class BasicCrawler {
|
|
|
650
661
|
context[navigationDeadlineKey] += extraMillis;
|
|
651
662
|
}
|
|
652
663
|
},
|
|
653
|
-
[deferredCleanupKey]: deferredCleanup,
|
|
654
664
|
};
|
|
655
665
|
}
|
|
666
|
+
/**
|
|
667
|
+
* Fetches the next request and rebuilds it as a {@link CrawlingRequest}. A copy rather than the manager's
|
|
668
|
+
* instance: managers key on `uniqueKey`/`id`, and it is this copy that flows back into `reclaimRequest` /
|
|
669
|
+
* `markRequestAsHandled`, so nothing observes the difference.
|
|
670
|
+
*/
|
|
656
671
|
async resolveRequest() {
|
|
657
|
-
const
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
request.loadedUrl = undefined;
|
|
672
|
+
const fetched = await this.timeoutAndRetry(this.fetchNextRequest.bind(this), this.internalTimeoutMillis, `Fetching next request timed out after ${this.internalTimeoutMillis / 1e3} seconds.`);
|
|
673
|
+
if (!fetched) {
|
|
674
|
+
return null;
|
|
661
675
|
}
|
|
676
|
+
const request = CrawlingRequest.fromSchema(fetched);
|
|
677
|
+
// Reset loadedUrl so an old one is not carried over to retries.
|
|
678
|
+
request.loadedUrl = undefined;
|
|
662
679
|
return request;
|
|
663
680
|
}
|
|
664
681
|
async resolveSession({ request }) {
|
|
665
682
|
const session = await this.timeoutAndRetry(async () => {
|
|
666
683
|
const existingSession = await this.sessionPool.getSession(request.sessionId);
|
|
667
684
|
if (!existingSession) {
|
|
668
|
-
throw new
|
|
685
|
+
throw new MissingSessionError(request.sessionId);
|
|
669
686
|
}
|
|
670
687
|
return existingSession;
|
|
671
688
|
}, this.internalTimeoutMillis, `Fetching session timed out after ${this.internalTimeoutMillis / 1e3} seconds.`);
|
|
@@ -697,26 +714,24 @@ export class BasicCrawler {
|
|
|
697
714
|
let contextPipeline;
|
|
698
715
|
if (extendContext !== undefined) {
|
|
699
716
|
contextPipeline = ContextPipeline.create()
|
|
700
|
-
.compose(
|
|
717
|
+
.compose(async (context) => await extendContext(context))
|
|
701
718
|
.chain(subclassPipeline);
|
|
702
719
|
}
|
|
703
720
|
else {
|
|
704
721
|
contextPipeline = subclassPipeline;
|
|
705
722
|
}
|
|
706
|
-
contextPipeline = contextPipeline.compose({
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
return context;
|
|
719
|
-
},
|
|
723
|
+
contextPipeline = contextPipeline.compose(async (context) => {
|
|
724
|
+
const { request } = context;
|
|
725
|
+
if (request && !this.requestMatchesEnqueueStrategy(request)) {
|
|
726
|
+
// eslint-disable-next-line dot-notation
|
|
727
|
+
const message = `Skipping request ${request.id} (starting url: ${request.url} -> loaded url: ${request.loadedUrl}) because it does not match the enqueue strategy (${request['enqueueStrategy']}).`;
|
|
728
|
+
this.log.debug(message);
|
|
729
|
+
request.noRetry = true;
|
|
730
|
+
request.state = RequestState.SKIPPED;
|
|
731
|
+
await this.#handleSkippedRequest({ request, reason: 'redirect' });
|
|
732
|
+
throw new ContextPipelineInterruptedError(message);
|
|
733
|
+
}
|
|
734
|
+
return context;
|
|
720
735
|
});
|
|
721
736
|
return contextPipeline;
|
|
722
737
|
}
|
|
@@ -1194,7 +1209,7 @@ export class BasicCrawler {
|
|
|
1194
1209
|
}
|
|
1195
1210
|
let [finalOptions] = filtered;
|
|
1196
1211
|
if (options.transformRequestFunction) {
|
|
1197
|
-
const transformed = applyRequestTransform([finalOptions], options.transformRequestFunction, (r) => allSkipped.push({ source: keepSkippedSource(r), reason:
|
|
1212
|
+
const transformed = applyRequestTransform([finalOptions], options.transformRequestFunction, (r) => allSkipped.push({ source: keepSkippedSource(r), reason: 'transform' }));
|
|
1198
1213
|
if (transformed.length === 0) {
|
|
1199
1214
|
continue;
|
|
1200
1215
|
}
|
|
@@ -1590,6 +1605,11 @@ export class BasicCrawler {
|
|
|
1590
1605
|
if (typeof request === 'string') {
|
|
1591
1606
|
yield { url: request, crawlDepth: newRequestDepth };
|
|
1592
1607
|
}
|
|
1608
|
+
else if (request instanceof Request) {
|
|
1609
|
+
const crawlee = (request.userData.__crawlee ??= {});
|
|
1610
|
+
crawlee.crawlDepth ??= newRequestDepth;
|
|
1611
|
+
yield request;
|
|
1612
|
+
}
|
|
1593
1613
|
else {
|
|
1594
1614
|
request.crawlDepth ??= newRequestDepth;
|
|
1595
1615
|
yield request;
|
|
@@ -1629,12 +1649,10 @@ export class BasicCrawler {
|
|
|
1629
1649
|
}
|
|
1630
1650
|
/**
|
|
1631
1651
|
* Unwraps errors thrown by the context pipeline to get the actual user error.
|
|
1632
|
-
* RequestHandlerError and
|
|
1652
|
+
* RequestHandlerError and ContextPipelineCleanupError wrap the actual error.
|
|
1633
1653
|
*/
|
|
1634
1654
|
unwrapError(error) {
|
|
1635
|
-
if (error instanceof RequestHandlerError ||
|
|
1636
|
-
error instanceof ContextPipelineInitializationError ||
|
|
1637
|
-
error instanceof ContextPipelineCleanupError) {
|
|
1655
|
+
if (error instanceof RequestHandlerError || error instanceof ContextPipelineCleanupError) {
|
|
1638
1656
|
return this.unwrapError(error.cause);
|
|
1639
1657
|
}
|
|
1640
1658
|
return error;
|
|
@@ -1809,8 +1827,7 @@ export class BasicCrawler {
|
|
|
1809
1827
|
throw err;
|
|
1810
1828
|
}
|
|
1811
1829
|
const { url, loadedUrl } = request;
|
|
1812
|
-
|
|
1813
|
-
const strategy = request['enqueueStrategy'];
|
|
1830
|
+
const strategy = request.enqueueStrategy;
|
|
1814
1831
|
// No strategy set, so we assume it matches, or it was added outside of enqueueLinks
|
|
1815
1832
|
if (!strategy) {
|
|
1816
1833
|
return true;
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import type { Awaitable } from '@crawlee/types';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Registers a callback that runs after the final context consumer finishes. `error` is the consumer's failure, if any;
|
|
4
|
+
* failures of downstream middlewares are not reported. Cleanups run in reverse registration order.
|
|
5
|
+
*/
|
|
6
|
+
export type CleanupRegistrar = (cleanup: (error?: unknown) => Awaitable<void>) => void;
|
|
7
|
+
/**
|
|
8
|
+
* A middleware step in the context pipeline: receives the context built so far and returns an extension that gets
|
|
9
|
+
* merged into it. Anything that needs tearing down is registered via `onCleanup`.
|
|
4
10
|
*
|
|
5
11
|
* @template TCrawlingContext - The input context type for this middleware
|
|
6
12
|
* @template TCrawlingContextExtension - The enhanced output context type
|
|
7
13
|
*/
|
|
8
|
-
export
|
|
9
|
-
/** The main middleware function that enhances the context */
|
|
10
|
-
action: (context: TCrawlingContext) => Awaitable<TCrawlingContextExtension>;
|
|
11
|
-
/** Optional cleanup function called after the consumer finishes or fails */
|
|
12
|
-
cleanup?: (context: TCrawlingContext & TCrawlingContextExtension, error?: unknown) => Awaitable<void>;
|
|
13
|
-
}
|
|
14
|
+
export type ContextMiddleware<TCrawlingContext, TCrawlingContextExtension> = (context: TCrawlingContext, onCleanup: CleanupRegistrar) => Awaitable<TCrawlingContextExtension>;
|
|
14
15
|
/**
|
|
15
16
|
* Encapsulates the logic of gradually enhancing the crawling context with additional information and utilities.
|
|
16
17
|
*
|
|
@@ -59,12 +60,12 @@ export declare abstract class ContextPipeline<TContextBase, TCrawlingContext ext
|
|
|
59
60
|
*
|
|
60
61
|
* @param crawlingContext - The initial context to process through the pipeline
|
|
61
62
|
* @param finalContextConsumer - The function that will receive the final enhanced context
|
|
63
|
+
* @param onInitializationError - Receives a middleware failure as-is; the consumer is then skipped. Runs before
|
|
64
|
+
* the cleanups, so it still sees whatever the failed middleware's predecessors set up.
|
|
62
65
|
*
|
|
63
|
-
* @throws {ContextPipelineInitializationError} When a middleware fails during initialization
|
|
64
|
-
* @throws {ContextPipelineInterruptedError} When the pipeline is intentionally interrupted during initialization
|
|
65
66
|
* @throws {RequestHandlerError} When the final context consumer throws an exception
|
|
66
67
|
* @throws {ContextPipelineCleanupError} When cleanup operations fail
|
|
67
68
|
* @throws {SessionError} Session errors are re-thrown as-is for special handling
|
|
68
69
|
*/
|
|
69
|
-
abstract call(crawlingContext: TContextBase, finalContextConsumer: (finalContext: TCrawlingContext) => Awaitable<unknown>): Promise<void>;
|
|
70
|
+
abstract call(crawlingContext: TContextBase, finalContextConsumer: (finalContext: TCrawlingContext) => Awaitable<unknown>, onInitializationError: (error: unknown) => Awaitable<void>): Promise<void>;
|
|
70
71
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { serviceLocator, SessionError } from '@crawlee/core';
|
|
2
|
-
import { ContextPipelineCleanupError,
|
|
2
|
+
import { ContextPipelineCleanupError, RequestHandlerError } from '../errors.js';
|
|
3
3
|
/**
|
|
4
4
|
* Encapsulates the logic of gradually enhancing the crawling context with additional information and utilities.
|
|
5
5
|
*
|
|
@@ -18,7 +18,7 @@ export class ContextPipeline {
|
|
|
18
18
|
* @returns A new ContextPipeline instance with no transformations
|
|
19
19
|
*/
|
|
20
20
|
static create() {
|
|
21
|
-
return new ContextPipelineImpl(
|
|
21
|
+
return new ContextPipelineImpl(async (context) => context);
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
@@ -57,14 +57,17 @@ class ContextPipelineImpl extends ContextPipeline {
|
|
|
57
57
|
/**
|
|
58
58
|
* @inheritdoc
|
|
59
59
|
*/
|
|
60
|
-
async call(crawlingContext, finalContextConsumer) {
|
|
60
|
+
async call(crawlingContext, finalContextConsumer, onInitializationError) {
|
|
61
61
|
const middlewares = Array.from(this.middlewareChain()).reverse();
|
|
62
62
|
const cleanupStack = [];
|
|
63
|
+
const onCleanup = (cleanup) => {
|
|
64
|
+
cleanupStack.push(cleanup);
|
|
65
|
+
};
|
|
63
66
|
let consumerException;
|
|
64
67
|
try {
|
|
65
|
-
for (const
|
|
68
|
+
for (const middleware of middlewares) {
|
|
66
69
|
try {
|
|
67
|
-
const contextExtension = await
|
|
70
|
+
const contextExtension = await middleware(crawlingContext, onCleanup);
|
|
68
71
|
const extensionNames = [
|
|
69
72
|
...Object.getOwnPropertyNames(contextExtension),
|
|
70
73
|
...Object.getOwnPropertySymbols(contextExtension),
|
|
@@ -81,18 +84,10 @@ class ContextPipelineImpl extends ContextPipeline {
|
|
|
81
84
|
.debug(`Context pipeline failed to define property ${key.toString()}:`, error);
|
|
82
85
|
}
|
|
83
86
|
}
|
|
84
|
-
if (cleanup) {
|
|
85
|
-
cleanupStack.push(cleanup);
|
|
86
|
-
}
|
|
87
87
|
}
|
|
88
88
|
catch (exception) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
if (exception instanceof ContextPipelineInterruptedError) {
|
|
93
|
-
throw exception;
|
|
94
|
-
}
|
|
95
|
-
throw new ContextPipelineInitializationError(exception);
|
|
89
|
+
await onInitializationError(exception);
|
|
90
|
+
return;
|
|
96
91
|
}
|
|
97
92
|
}
|
|
98
93
|
try {
|
|
@@ -110,7 +105,7 @@ class ContextPipelineImpl extends ContextPipeline {
|
|
|
110
105
|
finally {
|
|
111
106
|
try {
|
|
112
107
|
for (const cleanup of cleanupStack.reverse()) {
|
|
113
|
-
await cleanup(
|
|
108
|
+
await cleanup(consumerException);
|
|
114
109
|
}
|
|
115
110
|
}
|
|
116
111
|
catch (exception) {
|
|
@@ -2,6 +2,7 @@ import type { Awaitable, Dictionary, HttpRequestOptions, ISession, ProxyInfo, Se
|
|
|
2
2
|
import type { ReadonlyDeep } from 'type-fest';
|
|
3
3
|
import type { AddRequestsBatchedResult, CrawleeLogger, Dataset, KeyValueStore, Request, RequestOptions, Source, StorageIdentifier } from '@crawlee/core';
|
|
4
4
|
import type { EnqueueUrlsOptions } from '../enqueue_links/index.js';
|
|
5
|
+
import type { CrawlingRequest } from '../crawling_request.js';
|
|
5
6
|
/** @internal */
|
|
6
7
|
export type IsAny<T> = 0 extends 1 & T ? true : false;
|
|
7
8
|
/**
|
|
@@ -69,9 +70,9 @@ export interface RestrictedCrawlingContext<UserData extends Dictionary = Diction
|
|
|
69
70
|
*/
|
|
70
71
|
proxyInfo?: ProxyInfo;
|
|
71
72
|
/**
|
|
72
|
-
* The
|
|
73
|
+
* The request being processed.
|
|
73
74
|
*/
|
|
74
|
-
request:
|
|
75
|
+
request: CrawlingRequest<UserData>;
|
|
75
76
|
/**
|
|
76
77
|
* This function allows you to push data to a {@link Dataset} specified by name, or the one currently used by the crawler.
|
|
77
78
|
*
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { EnqueueStrategyOption } from '@crawlee/core';
|
|
2
|
+
import { Request } from '@crawlee/core';
|
|
3
|
+
import type { Dictionary, RequestSchema } from '@crawlee/types';
|
|
4
|
+
export type SkippedRequestReason = 'robotsTxt' | 'limit' | 'enqueueLimit' | 'filters' | 'transform' | 'redirect' | 'depth';
|
|
5
|
+
export declare enum RequestState {
|
|
6
|
+
UNPROCESSED = 0,
|
|
7
|
+
BEFORE_NAV = 1,
|
|
8
|
+
AFTER_NAV = 2,
|
|
9
|
+
REQUEST_HANDLER = 3,
|
|
10
|
+
DONE = 4,
|
|
11
|
+
ERROR_HANDLER = 5,
|
|
12
|
+
ERROR = 6,
|
|
13
|
+
SKIPPED = 7
|
|
14
|
+
}
|
|
15
|
+
export interface PushErrorMessageOptions {
|
|
16
|
+
/**
|
|
17
|
+
* Only push the error message without stack trace when true.
|
|
18
|
+
* @default false
|
|
19
|
+
*/
|
|
20
|
+
omitStack?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The crawler's per-request state, persisted inside `userData.__crawlee` so every storage carries it as plain user
|
|
24
|
+
* data. The settings are written by the `Request` constructor from `RequestOptions`; `state` only by a running crawler.
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
export interface CrawlingRequestData {
|
|
28
|
+
skipNavigation?: boolean;
|
|
29
|
+
crawlDepth?: number;
|
|
30
|
+
sessionId?: string;
|
|
31
|
+
maxRetries?: number;
|
|
32
|
+
enqueueStrategy?: EnqueueStrategyOption;
|
|
33
|
+
state?: RequestState;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* A {@link Request} as seen by a crawler: the same stored record, with the crawler's own per-request state
|
|
37
|
+
* (`userData.__crawlee`) exposed as properties. This is what `request` is in every request handler.
|
|
38
|
+
*
|
|
39
|
+
* Requests fetched from a request manager are plain {@link Request}s; {@link BasicCrawler} rebuilds them
|
|
40
|
+
* as `CrawlingRequest`s before handing them to the pipeline.
|
|
41
|
+
* @category Sources
|
|
42
|
+
*/
|
|
43
|
+
export declare class CrawlingRequest<UserData extends Dictionary = Dictionary> extends Request<UserData> {
|
|
44
|
+
static fromSchema<UserData extends Dictionary = Dictionary>(schema: RequestSchema): CrawlingRequest<UserData>;
|
|
45
|
+
private get crawleeData();
|
|
46
|
+
/**
|
|
47
|
+
* Tells the crawler processing this request to skip the navigation and process the request directly.
|
|
48
|
+
*
|
|
49
|
+
* When this is set to `true`, the crawling context will not contain the results of the navigation
|
|
50
|
+
* (e.g. `response`, `body`, `contentType`, `$` or `request.loadedUrl`).
|
|
51
|
+
* Accessing these properties will throw a {@link NavigationSkippedError} at runtime.
|
|
52
|
+
*/
|
|
53
|
+
get skipNavigation(): boolean;
|
|
54
|
+
set skipNavigation(value: boolean);
|
|
55
|
+
/**
|
|
56
|
+
* Depth of the request in the current crawl tree.
|
|
57
|
+
* Note that this is dependent on the crawler setup and might produce unexpected results when used with multiple crawlers.
|
|
58
|
+
*/
|
|
59
|
+
get crawlDepth(): number;
|
|
60
|
+
set crawlDepth(value: number);
|
|
61
|
+
/** ID of a session to use for this request. When set, the crawler will fetch this session from the session pool instead of creating a new one. */
|
|
62
|
+
get sessionId(): string | undefined;
|
|
63
|
+
set sessionId(value: string | undefined);
|
|
64
|
+
/** Maximum number of retries for this request. Allows to override the global `maxRequestRetries` option of `BasicCrawler`. */
|
|
65
|
+
get maxRetries(): number | undefined;
|
|
66
|
+
set maxRetries(value: number | undefined);
|
|
67
|
+
/** Describes the request's current lifecycle state. */
|
|
68
|
+
get state(): RequestState;
|
|
69
|
+
set state(value: RequestState);
|
|
70
|
+
/** The strategy the request was enqueued under, if it came from `enqueueLinks`. @internal */
|
|
71
|
+
get enqueueStrategy(): EnqueueStrategyOption | undefined;
|
|
72
|
+
/**
|
|
73
|
+
* Stores information about an error that occurred during processing of this request.
|
|
74
|
+
*
|
|
75
|
+
* You should always use Error instances when throwing errors in JavaScript.
|
|
76
|
+
*
|
|
77
|
+
* Nevertheless, to improve the debugging experience when using third party libraries
|
|
78
|
+
* that may not always throw an Error instance, the function performs a type
|
|
79
|
+
* inspection of the passed argument and attempts to extract as much information
|
|
80
|
+
* as possible, since just throwing a bad type error makes any debugging rather difficult.
|
|
81
|
+
*
|
|
82
|
+
* @param errorOrMessage Error object or error message to be stored in the request.
|
|
83
|
+
* @param [options]
|
|
84
|
+
*/
|
|
85
|
+
pushErrorMessage(errorOrMessage: unknown, options?: PushErrorMessageOptions): void;
|
|
86
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import util from 'node:util';
|
|
2
|
+
import { Request } from '@crawlee/core';
|
|
3
|
+
export var RequestState;
|
|
4
|
+
(function (RequestState) {
|
|
5
|
+
RequestState[RequestState["UNPROCESSED"] = 0] = "UNPROCESSED";
|
|
6
|
+
RequestState[RequestState["BEFORE_NAV"] = 1] = "BEFORE_NAV";
|
|
7
|
+
RequestState[RequestState["AFTER_NAV"] = 2] = "AFTER_NAV";
|
|
8
|
+
RequestState[RequestState["REQUEST_HANDLER"] = 3] = "REQUEST_HANDLER";
|
|
9
|
+
RequestState[RequestState["DONE"] = 4] = "DONE";
|
|
10
|
+
RequestState[RequestState["ERROR_HANDLER"] = 5] = "ERROR_HANDLER";
|
|
11
|
+
RequestState[RequestState["ERROR"] = 6] = "ERROR";
|
|
12
|
+
RequestState[RequestState["SKIPPED"] = 7] = "SKIPPED";
|
|
13
|
+
})(RequestState || (RequestState = {}));
|
|
14
|
+
/**
|
|
15
|
+
* A {@link Request} as seen by a crawler: the same stored record, with the crawler's own per-request state
|
|
16
|
+
* (`userData.__crawlee`) exposed as properties. This is what `request` is in every request handler.
|
|
17
|
+
*
|
|
18
|
+
* Requests fetched from a request manager are plain {@link Request}s; {@link BasicCrawler} rebuilds them
|
|
19
|
+
* as `CrawlingRequest`s before handing them to the pipeline.
|
|
20
|
+
* @category Sources
|
|
21
|
+
*/
|
|
22
|
+
export class CrawlingRequest extends Request {
|
|
23
|
+
static fromSchema(schema) {
|
|
24
|
+
return super.fromSchema(schema);
|
|
25
|
+
}
|
|
26
|
+
get crawleeData() {
|
|
27
|
+
return (this.userData.__crawlee ??= {});
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Tells the crawler processing this request to skip the navigation and process the request directly.
|
|
31
|
+
*
|
|
32
|
+
* When this is set to `true`, the crawling context will not contain the results of the navigation
|
|
33
|
+
* (e.g. `response`, `body`, `contentType`, `$` or `request.loadedUrl`).
|
|
34
|
+
* Accessing these properties will throw a {@link NavigationSkippedError} at runtime.
|
|
35
|
+
*/
|
|
36
|
+
get skipNavigation() {
|
|
37
|
+
return this.crawleeData.skipNavigation ?? false;
|
|
38
|
+
}
|
|
39
|
+
set skipNavigation(value) {
|
|
40
|
+
this.crawleeData.skipNavigation = value;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Depth of the request in the current crawl tree.
|
|
44
|
+
* Note that this is dependent on the crawler setup and might produce unexpected results when used with multiple crawlers.
|
|
45
|
+
*/
|
|
46
|
+
get crawlDepth() {
|
|
47
|
+
return this.crawleeData.crawlDepth ?? 0;
|
|
48
|
+
}
|
|
49
|
+
set crawlDepth(value) {
|
|
50
|
+
this.crawleeData.crawlDepth = value;
|
|
51
|
+
}
|
|
52
|
+
/** ID of a session to use for this request. When set, the crawler will fetch this session from the session pool instead of creating a new one. */
|
|
53
|
+
get sessionId() {
|
|
54
|
+
return this.crawleeData.sessionId;
|
|
55
|
+
}
|
|
56
|
+
set sessionId(value) {
|
|
57
|
+
this.crawleeData.sessionId = value;
|
|
58
|
+
}
|
|
59
|
+
/** Maximum number of retries for this request. Allows to override the global `maxRequestRetries` option of `BasicCrawler`. */
|
|
60
|
+
get maxRetries() {
|
|
61
|
+
return this.crawleeData.maxRetries;
|
|
62
|
+
}
|
|
63
|
+
set maxRetries(value) {
|
|
64
|
+
this.crawleeData.maxRetries = value;
|
|
65
|
+
}
|
|
66
|
+
/** Describes the request's current lifecycle state. */
|
|
67
|
+
get state() {
|
|
68
|
+
return this.crawleeData.state ?? RequestState.UNPROCESSED;
|
|
69
|
+
}
|
|
70
|
+
set state(value) {
|
|
71
|
+
this.crawleeData.state = value;
|
|
72
|
+
}
|
|
73
|
+
/** The strategy the request was enqueued under, if it came from `enqueueLinks`. @internal */
|
|
74
|
+
get enqueueStrategy() {
|
|
75
|
+
return this.crawleeData.enqueueStrategy;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Stores information about an error that occurred during processing of this request.
|
|
79
|
+
*
|
|
80
|
+
* You should always use Error instances when throwing errors in JavaScript.
|
|
81
|
+
*
|
|
82
|
+
* Nevertheless, to improve the debugging experience when using third party libraries
|
|
83
|
+
* that may not always throw an Error instance, the function performs a type
|
|
84
|
+
* inspection of the passed argument and attempts to extract as much information
|
|
85
|
+
* as possible, since just throwing a bad type error makes any debugging rather difficult.
|
|
86
|
+
*
|
|
87
|
+
* @param errorOrMessage Error object or error message to be stored in the request.
|
|
88
|
+
* @param [options]
|
|
89
|
+
*/
|
|
90
|
+
pushErrorMessage(errorOrMessage, options = {}) {
|
|
91
|
+
const { omitStack } = options;
|
|
92
|
+
let message;
|
|
93
|
+
const type = typeof errorOrMessage;
|
|
94
|
+
if (type === 'object') {
|
|
95
|
+
if (!errorOrMessage) {
|
|
96
|
+
message = 'null';
|
|
97
|
+
}
|
|
98
|
+
else if (errorOrMessage instanceof Error) {
|
|
99
|
+
message = omitStack
|
|
100
|
+
? errorOrMessage.message
|
|
101
|
+
: // .stack includes the message
|
|
102
|
+
errorOrMessage.stack;
|
|
103
|
+
}
|
|
104
|
+
else if (Reflect.has(Object(errorOrMessage), 'message')) {
|
|
105
|
+
message = Reflect.get(Object(errorOrMessage), 'message');
|
|
106
|
+
}
|
|
107
|
+
else if (errorOrMessage.toString() !== '[object Object]') {
|
|
108
|
+
message = errorOrMessage.toString();
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
try {
|
|
112
|
+
message = util.inspect(errorOrMessage);
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
message = 'Unable to extract any message from the received object.';
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else if (type === 'undefined') {
|
|
120
|
+
message = 'undefined';
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
message = errorOrMessage.toString();
|
|
124
|
+
}
|
|
125
|
+
this.errorMessages.push(message);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { Awaitable, Dictionary } from '@crawlee/types';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import type { EnqueueStrategyOption, RequestOptions,
|
|
3
|
+
import type { EnqueueStrategyOption, RequestOptions, Source } from '@crawlee/core';
|
|
4
4
|
import { Request } from '@crawlee/core';
|
|
5
|
+
import type { SkippedRequestReason } from '../crawling_request.js';
|
|
5
6
|
export interface UrlPatternObject {
|
|
6
7
|
glob?: string;
|
|
7
8
|
regexp?: RegExp;
|
|
@@ -27,10 +27,9 @@ export const urlPatternSchema = z.union([
|
|
|
27
27
|
* @ignore
|
|
28
28
|
*/
|
|
29
29
|
export function createSkippedRequestArgs(source, reason) {
|
|
30
|
-
const sourceReason = typeof source === 'string' ? undefined : source.skippedReason;
|
|
31
30
|
let request;
|
|
32
31
|
return {
|
|
33
|
-
reason
|
|
32
|
+
reason,
|
|
34
33
|
get request() {
|
|
35
34
|
request ??=
|
|
36
35
|
source instanceof Request
|
package/internals/errors.d.ts
CHANGED
|
@@ -41,9 +41,6 @@ export declare class MissingSessionError extends Error {
|
|
|
41
41
|
export declare class ContextPipelineInterruptedError extends Error {
|
|
42
42
|
constructor(message?: string);
|
|
43
43
|
}
|
|
44
|
-
export declare class ContextPipelineInitializationError extends Error {
|
|
45
|
-
constructor(error: unknown, options?: ErrorOptions);
|
|
46
|
-
}
|
|
47
44
|
export declare class ContextPipelineCleanupError extends CriticalError {
|
|
48
45
|
constructor(error: unknown, options?: ErrorOptions);
|
|
49
46
|
}
|
package/internals/errors.js
CHANGED
|
@@ -49,11 +49,6 @@ export class ContextPipelineInterruptedError extends Error {
|
|
|
49
49
|
super(`Request handling was interrupted during context initialization ${message ? ` - ${message}` : ''}`);
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
-
export class ContextPipelineInitializationError extends Error {
|
|
53
|
-
constructor(error, options) {
|
|
54
|
-
super(undefined, { cause: error, ...options });
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
52
|
export class ContextPipelineCleanupError extends CriticalError {
|
|
58
53
|
constructor(error, options) {
|
|
59
54
|
super(undefined, { cause: error, ...options });
|
package/internals/router.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { CrawlingRequest } from './crawling_request.js';
|
|
2
2
|
import type { Awaitable, Dictionary } from '@crawlee/types';
|
|
3
3
|
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
4
4
|
import type { CrawlingContext, LoadedRequest, RestrictedCrawlingContext, TypedContextAddRequests, TypedContextEnqueueLinks } from './crawlers/crawler_commons.js';
|
|
@@ -14,7 +14,7 @@ export declare const defaultRoute: unique symbol;
|
|
|
14
14
|
* request under a declared label requires the matching `userData` shape.
|
|
15
15
|
*/
|
|
16
16
|
export type RouterHandlerContext<Context, UserData extends Dictionary, Routes extends Record<keyof Routes, Dictionary>> = Omit<Context, 'request' | 'addRequests' | 'enqueueLinks'> & {
|
|
17
|
-
request: LoadedRequest<
|
|
17
|
+
request: LoadedRequest<CrawlingRequest<UserData>>;
|
|
18
18
|
addRequests: TypedContextAddRequests<Routes>;
|
|
19
19
|
} & (Context extends {
|
|
20
20
|
enqueueLinks: infer EnqueueLinks;
|
|
@@ -68,7 +68,7 @@ export type RouterLabel<Routes extends Record<keyof Routes, Dictionary>> = strin
|
|
|
68
68
|
export interface RouterHandler<Context extends RestrictedCrawlingContext = CrawlingContext, Routes extends Record<keyof Routes, Dictionary> = Record<string, GetUserDataFromRequest<Context['request']>>> extends Router<Context, Routes> {
|
|
69
69
|
(ctx: Context): Awaitable<void>;
|
|
70
70
|
}
|
|
71
|
-
export type GetUserDataFromRequest<T> = T extends
|
|
71
|
+
export type GetUserDataFromRequest<T> = T extends CrawlingRequest<infer Y> ? Y : never;
|
|
72
72
|
/**
|
|
73
73
|
* Per-route overrides, passed as the last argument of {@link Router.addHandler|`addHandler`} and
|
|
74
74
|
* {@link Router.addDefaultHandler|`addDefaultHandler`}.
|
|
@@ -85,7 +85,7 @@ export interface RouteOptions {
|
|
|
85
85
|
}
|
|
86
86
|
export type RouterRoutes<Context, Routes extends Record<keyof Routes, Dictionary>> = {
|
|
87
87
|
[Label in keyof Routes]: (ctx: Omit<Context, 'request'> & {
|
|
88
|
-
request:
|
|
88
|
+
request: CrawlingRequest<Routes[Label]>;
|
|
89
89
|
}) => Awaitable<void>;
|
|
90
90
|
};
|
|
91
91
|
/**
|
|
@@ -799,7 +799,7 @@ export class ThrottlingRequestManager {
|
|
|
799
799
|
async #migrateToSubQueue(request, inner) {
|
|
800
800
|
try {
|
|
801
801
|
const subManager = await this.#selectManagerOrThrow(request.url);
|
|
802
|
-
await subManager.addRequest(
|
|
802
|
+
await subManager.addRequest(Request.fromSchema({ ...request, id: undefined }));
|
|
803
803
|
}
|
|
804
804
|
catch (error) {
|
|
805
805
|
// Whatever the reason, the request is out of the wrapped manager and in nobody's hands, so it
|
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.185",
|
|
4
4
|
"description": "The scalable web crawling and scraping library for JavaScript/Node.js. Enables development of data extraction and web automation jobs (not only) with headless Chrome and Puppeteer.",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22.0.0"
|
|
@@ -42,10 +42,10 @@
|
|
|
42
42
|
"@apify/datastructures": "^3.0.1",
|
|
43
43
|
"@apify/timeout": "^1.0.1",
|
|
44
44
|
"@apify/utilities": "^3.0.1",
|
|
45
|
-
"@crawlee/core": "4.0.0-beta.
|
|
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.185",
|
|
46
|
+
"@crawlee/http-client": "4.0.0-beta.185",
|
|
47
|
+
"@crawlee/types": "4.0.0-beta.185",
|
|
48
|
+
"@crawlee/utils": "4.0.0-beta.185",
|
|
49
49
|
"@sapphire/async-queue": "^1.5.5",
|
|
50
50
|
"@standard-schema/spec": "^1.0.0",
|
|
51
51
|
"csv-stringify": "^6.5.2",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"zod": "^4.5.4"
|
|
58
58
|
},
|
|
59
59
|
"optionalDependencies": {
|
|
60
|
-
"@crawlee/impit-client": "4.0.0-beta.
|
|
60
|
+
"@crawlee/impit-client": "4.0.0-beta.185"
|
|
61
61
|
},
|
|
62
62
|
"lerna": {
|
|
63
63
|
"command": {
|
|
@@ -66,5 +66,5 @@
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
},
|
|
69
|
-
"gitHead": "
|
|
69
|
+
"gitHead": "642c670956a5a60384d5ed5e67ee809927cc419b"
|
|
70
70
|
}
|