@crawlee/basic 4.0.0-beta.104 → 4.0.0-beta.105
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 -32
- package/internals/basic-crawler.js +71 -11
- 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.
|
|
@@ -538,6 +553,10 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
538
553
|
protected readonly httpClient: BaseHttpClient;
|
|
539
554
|
protected readonly retryOnBlocked: boolean;
|
|
540
555
|
private respectRobotsTxtFile;
|
|
556
|
+
/** Whether `runInStorageTransaction()` opens a transaction at all. */
|
|
557
|
+
private readonly transactionalStorageEnabled;
|
|
558
|
+
/** The resolved per-storage-type write policy overrides forwarded to each request's transaction. */
|
|
559
|
+
private readonly storageWritePolicy;
|
|
541
560
|
protected readonly onSkippedRequest?: SkippedRequestCallback;
|
|
542
561
|
private _closeEvents?;
|
|
543
562
|
private loggedPerRun;
|
|
@@ -546,41 +565,41 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
546
565
|
private readonly contextPipelineOptions;
|
|
547
566
|
protected static optionsShape: {
|
|
548
567
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
549
|
-
contextPipelineBuilder: import("ow").ObjectPredicate<object> &
|
|
568
|
+
contextPipelineBuilder: import("ow").ObjectPredicate<object> & BasePredicate<object | undefined>;
|
|
550
569
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
551
|
-
extendContext: import("ow").Predicate<Function> &
|
|
570
|
+
extendContext: import("ow").Predicate<Function> & BasePredicate<Function | undefined>;
|
|
552
571
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
553
|
-
requestList: import("ow").ObjectPredicate<object> &
|
|
572
|
+
requestList: import("ow").ObjectPredicate<object> & BasePredicate<object | undefined>;
|
|
554
573
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
555
|
-
requestQueue: import("ow").ObjectPredicate<object> &
|
|
574
|
+
requestQueue: import("ow").ObjectPredicate<object> & BasePredicate<object | undefined>;
|
|
556
575
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
557
|
-
requestHandler: import("ow").Predicate<Function> &
|
|
576
|
+
requestHandler: import("ow").Predicate<Function> & BasePredicate<Function | undefined>;
|
|
558
577
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
559
|
-
requestHandlerTimeoutSecs: import("ow").NumberPredicate &
|
|
578
|
+
requestHandlerTimeoutSecs: import("ow").NumberPredicate & BasePredicate<number | undefined>;
|
|
560
579
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
561
|
-
errorHandler: import("ow").Predicate<Function> &
|
|
580
|
+
errorHandler: import("ow").Predicate<Function> & BasePredicate<Function | undefined>;
|
|
562
581
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
563
|
-
failedRequestHandler: import("ow").Predicate<Function> &
|
|
582
|
+
failedRequestHandler: import("ow").Predicate<Function> & BasePredicate<Function | undefined>;
|
|
564
583
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
565
|
-
maxRequestRetries: import("ow").NumberPredicate &
|
|
584
|
+
maxRequestRetries: import("ow").NumberPredicate & BasePredicate<number | undefined>;
|
|
566
585
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
567
|
-
sameDomainDelaySecs: import("ow").NumberPredicate &
|
|
586
|
+
sameDomainDelaySecs: import("ow").NumberPredicate & BasePredicate<number | undefined>;
|
|
568
587
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
569
|
-
maxRequestsPerCrawl: import("ow").NumberPredicate &
|
|
588
|
+
maxRequestsPerCrawl: import("ow").NumberPredicate & BasePredicate<number | undefined>;
|
|
570
589
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
571
|
-
maxCrawlDepth: import("ow").NumberPredicate &
|
|
590
|
+
maxCrawlDepth: import("ow").NumberPredicate & BasePredicate<number | undefined>;
|
|
572
591
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
573
|
-
taskLoopOptions: import("ow").ObjectPredicate<object> &
|
|
592
|
+
taskLoopOptions: import("ow").ObjectPredicate<object> & BasePredicate<object | undefined>;
|
|
574
593
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
575
|
-
concurrencySystem: import("ow").ObjectPredicate<object> &
|
|
594
|
+
concurrencySystem: import("ow").ObjectPredicate<object> & BasePredicate<object | undefined>;
|
|
576
595
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
577
|
-
sessionPool: import("ow").ObjectPredicate<object> &
|
|
596
|
+
sessionPool: import("ow").ObjectPredicate<object> & BasePredicate<object | undefined>;
|
|
578
597
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
579
|
-
proxyConfiguration: import("ow").ObjectPredicate<object> &
|
|
598
|
+
proxyConfiguration: import("ow").ObjectPredicate<object> & BasePredicate<object | undefined>;
|
|
580
599
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
581
|
-
statusMessageLoggingInterval: import("ow").NumberPredicate &
|
|
600
|
+
statusMessageLoggingInterval: import("ow").NumberPredicate & BasePredicate<number | undefined>;
|
|
582
601
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
583
|
-
statusMessageCallback: import("ow").Predicate<Function> &
|
|
602
|
+
statusMessageCallback: import("ow").Predicate<Function> & BasePredicate<Function | undefined>;
|
|
584
603
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
585
604
|
additionalHttpErrorStatusCodes: import("ow").ArrayPredicate<number>;
|
|
586
605
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
@@ -588,33 +607,34 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
588
607
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
589
608
|
blockedStatusCodes: import("ow").ArrayPredicate<number>;
|
|
590
609
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
591
|
-
retryOnBlocked: import("ow").BooleanPredicate &
|
|
610
|
+
retryOnBlocked: import("ow").BooleanPredicate & BasePredicate<boolean | undefined>;
|
|
592
611
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
593
612
|
respectRobotsTxtFile: import("ow").AnyPredicate<boolean | object>;
|
|
613
|
+
transactionalStorage: BasePredicate<boolean | Partial<StorageWritePolicy> | undefined>;
|
|
594
614
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
595
|
-
onSkippedRequest: import("ow").Predicate<Function> &
|
|
615
|
+
onSkippedRequest: import("ow").Predicate<Function> & BasePredicate<Function | undefined>;
|
|
596
616
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
597
|
-
httpClient: import("ow").ObjectPredicate<object> &
|
|
617
|
+
httpClient: import("ow").ObjectPredicate<object> & BasePredicate<object | undefined>;
|
|
598
618
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
599
|
-
configuration: import("ow").ObjectPredicate<object> &
|
|
619
|
+
configuration: import("ow").ObjectPredicate<object> & BasePredicate<object | undefined>;
|
|
600
620
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
601
|
-
storageBackend: import("ow").ObjectPredicate<object> &
|
|
621
|
+
storageBackend: import("ow").ObjectPredicate<object> & BasePredicate<object | undefined>;
|
|
602
622
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
603
|
-
eventManager: import("ow").ObjectPredicate<object> &
|
|
623
|
+
eventManager: import("ow").ObjectPredicate<object> & BasePredicate<object | undefined>;
|
|
604
624
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
605
|
-
logger: import("ow").ObjectPredicate<object> &
|
|
625
|
+
logger: import("ow").ObjectPredicate<object> & BasePredicate<object | undefined>;
|
|
606
626
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
607
|
-
minConcurrency: import("ow").NumberPredicate &
|
|
627
|
+
minConcurrency: import("ow").NumberPredicate & BasePredicate<number | undefined>;
|
|
608
628
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
609
|
-
maxConcurrency: import("ow").NumberPredicate &
|
|
629
|
+
maxConcurrency: import("ow").NumberPredicate & BasePredicate<number | undefined>;
|
|
610
630
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
611
|
-
maxRequestsPerMinute: import("ow").NumberPredicate &
|
|
631
|
+
maxRequestsPerMinute: import("ow").NumberPredicate & BasePredicate<number | undefined>;
|
|
612
632
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
613
|
-
keepAlive: import("ow").BooleanPredicate &
|
|
633
|
+
keepAlive: import("ow").BooleanPredicate & BasePredicate<boolean | undefined>;
|
|
614
634
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
615
|
-
statistics: import("ow").ObjectPredicate<object> &
|
|
635
|
+
statistics: import("ow").ObjectPredicate<object> & BasePredicate<object | undefined>;
|
|
616
636
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
617
|
-
id: import("ow").StringPredicate &
|
|
637
|
+
id: import("ow").StringPredicate & BasePredicate<string | undefined>;
|
|
618
638
|
};
|
|
619
639
|
/**
|
|
620
640
|
* All `BasicCrawler` parameters are passed via an options object.
|
|
@@ -801,6 +821,12 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
801
821
|
*/
|
|
802
822
|
private getRouteTimeoutMillis;
|
|
803
823
|
protected runRequestHandler(crawlingContext: ExtendedContext): Promise<void>;
|
|
824
|
+
/**
|
|
825
|
+
* Runs `callback` inside a {@link StorageTransaction}, unless transactional storage is disabled.
|
|
826
|
+
* Deliberately does **not** commit on return - `handleRequest` swallows request handler failures, so
|
|
827
|
+
* a normal return says nothing about success. `handleRequest` owns the outcome.
|
|
828
|
+
*/
|
|
829
|
+
private runInStorageTransaction;
|
|
804
830
|
/**
|
|
805
831
|
* Handles blocked request
|
|
806
832
|
*/
|
|
@@ -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';
|
|
@@ -186,6 +186,10 @@ export class BasicCrawler {
|
|
|
186
186
|
httpClient;
|
|
187
187
|
retryOnBlocked;
|
|
188
188
|
respectRobotsTxtFile;
|
|
189
|
+
/** Whether `runInStorageTransaction()` opens a transaction at all. */
|
|
190
|
+
transactionalStorageEnabled;
|
|
191
|
+
/** The resolved per-storage-type write policy overrides forwarded to each request's transaction. */
|
|
192
|
+
storageWritePolicy;
|
|
189
193
|
onSkippedRequest;
|
|
190
194
|
_closeEvents;
|
|
191
195
|
loggedPerRun = new Set();
|
|
@@ -219,6 +223,9 @@ export class BasicCrawler {
|
|
|
219
223
|
blockedStatusCodes: ow.optional.array.ofType(ow.number),
|
|
220
224
|
retryOnBlocked: ow.optional.boolean,
|
|
221
225
|
respectRobotsTxtFile: ow.optional.any(ow.boolean, ow.object),
|
|
226
|
+
transactionalStorage: ow.optional.any(ow.boolean, ow.object.exactShape({
|
|
227
|
+
requestQueue: ow.optional.string.oneOf(['deferred', 'writeThrough']),
|
|
228
|
+
})),
|
|
222
229
|
onSkippedRequest: ow.optional.function,
|
|
223
230
|
httpClient: ow.optional.object,
|
|
224
231
|
configuration: ow.optional.object,
|
|
@@ -246,7 +253,7 @@ export class BasicCrawler {
|
|
|
246
253
|
// Service locator options
|
|
247
254
|
configuration, storageBackend, eventManager, logger,
|
|
248
255
|
// AutoscaledPool shorthands
|
|
249
|
-
minConcurrency, maxConcurrency, maxRequestsPerMinute, blockedStatusCodes: blockedStatusCodesInput, retryOnBlocked = false, respectRobotsTxtFile = false, onSkippedRequest, requestHandler, requestHandlerTimeoutSecs, errorHandler, failedRequestHandler, statusMessageLoggingInterval = 10, statusMessageCallback, statistics, httpClient, id, } = options;
|
|
256
|
+
minConcurrency, maxConcurrency, maxRequestsPerMinute, blockedStatusCodes: blockedStatusCodesInput, retryOnBlocked = false, respectRobotsTxtFile = false, transactionalStorage, onSkippedRequest, requestHandler, requestHandlerTimeoutSecs, errorHandler, failedRequestHandler, statusMessageLoggingInterval = 10, statusMessageCallback, statistics, httpClient, id, } = options;
|
|
250
257
|
// All concurrency configuration lives on the `ConcurrencySystem`, so the shortcuts have nowhere to go once
|
|
251
258
|
// one is supplied - and silently dropping a `maxConcurrency` the user asked for is how crawls end up
|
|
252
259
|
// hammering a site.
|
|
@@ -320,6 +327,10 @@ export class BasicCrawler {
|
|
|
320
327
|
}
|
|
321
328
|
this.retryOnBlocked = retryOnBlocked;
|
|
322
329
|
this.respectRobotsTxtFile = respectRobotsTxtFile;
|
|
330
|
+
// The cast undoes ow's assertion signature, which mangles `boolean | object` unions.
|
|
331
|
+
const transactionalStorageOption = transactionalStorage;
|
|
332
|
+
this.transactionalStorageEnabled = transactionalStorageOption !== false;
|
|
333
|
+
this.storageWritePolicy = typeof transactionalStorageOption === 'object' ? transactionalStorageOption : {};
|
|
323
334
|
this.onSkippedRequest = onSkippedRequest;
|
|
324
335
|
// allow at least 5min for internal timeouts
|
|
325
336
|
this.internalTimeoutMillis =
|
|
@@ -376,12 +387,15 @@ export class BasicCrawler {
|
|
|
376
387
|
this.stats.startJob(request.id || request.uniqueKey);
|
|
377
388
|
const crawlingContext = { request };
|
|
378
389
|
try {
|
|
390
|
+
// The transaction spans the whole pipeline call, covering the navigation hooks
|
|
391
|
+
// and `extendContext` too; `handleRequest` drives its outcome explicitly.
|
|
392
|
+
await this.runInStorageTransaction(async () =>
|
|
379
393
|
// Navigation, the navigation hooks and the request handler are timed individually, but the
|
|
380
394
|
// phases between them are not, so a request could still get stuck indefinitely. This is the
|
|
381
395
|
// catch-all for that - see `raceWithTimeout` for why it is a bare timer, not a timeout frame.
|
|
382
396
|
await this.withRequestTimeout(crawlingContext, this.basicContextPipeline
|
|
383
397
|
.chain(this.contextPipeline)
|
|
384
|
-
.call(crawlingContext, (ctx) => this.handleRequest(ctx, source, request)));
|
|
398
|
+
.call(crawlingContext, (ctx) => this.handleRequest(ctx, source, request))));
|
|
385
399
|
}
|
|
386
400
|
catch (error) {
|
|
387
401
|
// ContextPipelineInterruptedError means the request was intentionally skipped
|
|
@@ -944,14 +958,18 @@ export class BasicCrawler {
|
|
|
944
958
|
return Math.min(limit, explicitLimit ?? Infinity);
|
|
945
959
|
}
|
|
946
960
|
async handleSkippedRequest(options) {
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
961
|
+
// A skipped request is a *successful* outcome, but the interrupt still unwinds through the
|
|
962
|
+
// transaction scope, which rolls back - so the skip bookkeeping must write directly.
|
|
963
|
+
await withDirectStorageAccess(async () => {
|
|
964
|
+
if (options.reason === 'limit') {
|
|
965
|
+
this.logOncePerRun('maxRequestsPerCrawl', 'The number of requests enqueued by the crawler reached the maxRequestsPerCrawl limit of ' +
|
|
966
|
+
`${this.maxRequestsPerCrawl} requests and no further requests will be added.`);
|
|
967
|
+
}
|
|
968
|
+
if (options.reason === 'depth') {
|
|
969
|
+
this.logOncePerRun('maxCrawlDepth', `The crawler reached the maxCrawlDepth limit of ${this.maxCrawlDepth} and no further requests will be enqueued.`);
|
|
970
|
+
}
|
|
971
|
+
await this.onSkippedRequest?.(options);
|
|
972
|
+
});
|
|
955
973
|
}
|
|
956
974
|
logOncePerRun(key, message) {
|
|
957
975
|
if (!this.loggedPerRun.has(key)) {
|
|
@@ -1168,6 +1186,40 @@ export class BasicCrawler {
|
|
|
1168
1186
|
const timeoutMillis = this.resolveRequestHandlerTimeoutMillis(crawlingContext.request.label);
|
|
1169
1187
|
await addTimeoutToPromise(async () => this.requestHandler(crawlingContext), timeoutMillis, `requestHandler timed out after ${timeoutMillis / 1000} seconds (${crawlingContext.request.id}).`);
|
|
1170
1188
|
}
|
|
1189
|
+
/**
|
|
1190
|
+
* Runs `callback` inside a {@link StorageTransaction}, unless transactional storage is disabled.
|
|
1191
|
+
* Deliberately does **not** commit on return - `handleRequest` swallows request handler failures, so
|
|
1192
|
+
* a normal return says nothing about success. `handleRequest` owns the outcome.
|
|
1193
|
+
*/
|
|
1194
|
+
async runInStorageTransaction(callback) {
|
|
1195
|
+
if (!this.transactionalStorageEnabled) {
|
|
1196
|
+
return callback();
|
|
1197
|
+
}
|
|
1198
|
+
const transaction = createStorageTransaction({
|
|
1199
|
+
policy: this.storageWritePolicy,
|
|
1200
|
+
commitTimeoutMillis: this.internalTimeoutMillis,
|
|
1201
|
+
});
|
|
1202
|
+
let threw = true;
|
|
1203
|
+
try {
|
|
1204
|
+
const result = await transaction.run(callback);
|
|
1205
|
+
threw = false;
|
|
1206
|
+
return result;
|
|
1207
|
+
}
|
|
1208
|
+
finally {
|
|
1209
|
+
if (transaction.state === 'open') {
|
|
1210
|
+
// `handleRequest` commits or rolls back on every normal path, so an open transaction on
|
|
1211
|
+
// a normal return is a wiring bug; on a propagating throw (a pipeline-level failure) it is
|
|
1212
|
+
// expected. Either way, discard the unvalidated writes; only the former is worth flagging.
|
|
1213
|
+
if (!threw) {
|
|
1214
|
+
this.log.error('Internal error: a storage transaction was still open after the request pipeline ' +
|
|
1215
|
+
'returned normally. Its writes are being discarded. Please report this.');
|
|
1216
|
+
}
|
|
1217
|
+
transaction.rollback();
|
|
1218
|
+
}
|
|
1219
|
+
// Unconditional: `failed` is a terminal state that the branch above never reaches.
|
|
1220
|
+
transaction.dispose();
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1171
1223
|
/**
|
|
1172
1224
|
* Handles blocked request
|
|
1173
1225
|
*/
|
|
@@ -1281,10 +1333,15 @@ export class BasicCrawler {
|
|
|
1281
1333
|
return;
|
|
1282
1334
|
}
|
|
1283
1335
|
const statisticsId = request.id || request.uniqueKey;
|
|
1336
|
+
// Opened by `runInStorageTransaction`; absent when disabled or when the subclass opens its own.
|
|
1337
|
+
const transaction = currentStorageTransaction();
|
|
1284
1338
|
let isRequestLocked = true;
|
|
1285
1339
|
try {
|
|
1286
1340
|
request.state = RequestState.REQUEST_HANDLER;
|
|
1287
1341
|
await this.runRequestHandler(crawlingContext);
|
|
1342
|
+
// Commit *before* marking the request as handled, so a commit failure fails the request and
|
|
1343
|
+
// it is retried. This also closes the transaction, so everything below passes through.
|
|
1344
|
+
await transaction?.commit();
|
|
1288
1345
|
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
1346
|
isRequestLocked = false; // markRequestAsHandled succeeded and unlocked the request
|
|
1290
1347
|
this.stats.finishJob(statisticsId, request.retryCount);
|
|
@@ -1293,6 +1350,9 @@ export class BasicCrawler {
|
|
|
1293
1350
|
crawlingContext.session.markGood();
|
|
1294
1351
|
}
|
|
1295
1352
|
catch (rawError) {
|
|
1353
|
+
// Roll back *before* any error handler runs - error handlers write to real storage precisely
|
|
1354
|
+
// because the transaction is already closed. A no-op when the commit above succeeded.
|
|
1355
|
+
transaction?.rollback();
|
|
1296
1356
|
const err = this.unwrapError(rawError);
|
|
1297
1357
|
try {
|
|
1298
1358
|
request.state = RequestState.ERROR_HANDLER;
|
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.105",
|
|
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.105",
|
|
46
|
+
"@crawlee/http-client": "4.0.0-beta.105",
|
|
47
|
+
"@crawlee/types": "4.0.0-beta.105",
|
|
48
|
+
"@crawlee/utils": "4.0.0-beta.105",
|
|
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.105"
|
|
57
57
|
},
|
|
58
58
|
"lerna": {
|
|
59
59
|
"command": {
|
|
@@ -62,5 +62,5 @@
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
},
|
|
65
|
-
"gitHead": "
|
|
65
|
+
"gitHead": "26073a822c7699ac487931383a39192bc3daae7a"
|
|
66
66
|
}
|