@crawlee/basic 4.0.0-beta.96 → 4.0.0-beta.97
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.
|
@@ -6,6 +6,7 @@ import type { ReadonlyDeep, SetRequired } from 'type-fest';
|
|
|
6
6
|
import { TimeoutError } from '@apify/timeout';
|
|
7
7
|
export interface BasicCrawlingContext<UserData extends Dictionary = Dictionary> extends CrawlingContext<UserData> {
|
|
8
8
|
}
|
|
9
|
+
export { navigationDeadlineKey, remainingNavigationWindowMillis } from './request-timeout.js';
|
|
9
10
|
export type RequestHandler<Context extends CrawlingContext = CrawlingContext> = (inputs: Context) => Awaitable<void>;
|
|
10
11
|
/**
|
|
11
12
|
* An error handler receives the crawling context and the error that was thrown while processing the request.
|
|
@@ -769,6 +770,32 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
769
770
|
* Initializes the crawler.
|
|
770
771
|
*/
|
|
771
772
|
protected _init(): Promise<void>;
|
|
773
|
+
/**
|
|
774
|
+
* The navigation timeout (pre-navigation hooks, navigation, and post-navigation hooks) in milliseconds, used
|
|
775
|
+
* to size the internal request timeout. `BasicCrawler` has no navigation phase, so this is 0; the HTTP and
|
|
776
|
+
* browser crawlers override it with their `navigationTimeoutSecs`.
|
|
777
|
+
*/
|
|
778
|
+
protected getNavigationTimeoutMillis(): number;
|
|
779
|
+
/**
|
|
780
|
+
* Races the request against the internal timeout (see {@link raceWithTimeout}), sized to outlast the phases
|
|
781
|
+
* that have their own timeout - the navigation, its hooks, and the request handler - so a legitimately slow
|
|
782
|
+
* request, a per-route override, or a low `CRAWLEE_INTERNAL_TIMEOUT` is not cut short mid-phase. It takes
|
|
783
|
+
* whichever is larger: the configured internal timeout, or this request's combined phase budget.
|
|
784
|
+
*/
|
|
785
|
+
private withRequestTimeout;
|
|
786
|
+
/**
|
|
787
|
+
* The request handler timeout for a request with the given route label. A router route may override the
|
|
788
|
+
* crawler's own `requestHandlerTimeoutSecs`; anything else falls back to `fallbackMillis`.
|
|
789
|
+
*
|
|
790
|
+
* @param label The request's route label, or `undefined` for the default route / no specific request.
|
|
791
|
+
* @param fallbackMillis Timeout to use when no route overrides it.
|
|
792
|
+
*/
|
|
793
|
+
private resolveRequestHandlerTimeoutMillis;
|
|
794
|
+
/**
|
|
795
|
+
* The timeout the router route with the given label asked for, or `undefined` when it did not override one
|
|
796
|
+
* (or the request handler is not a router at all).
|
|
797
|
+
*/
|
|
798
|
+
private getRouteTimeoutMillis;
|
|
772
799
|
protected runRequestHandler(crawlingContext: ExtendedContext): Promise<void>;
|
|
773
800
|
/**
|
|
774
801
|
* Handles blocked request
|
|
@@ -893,4 +920,3 @@ export interface CrawlerRunOptions extends CrawlerAddRequestsOptions {
|
|
|
893
920
|
*/
|
|
894
921
|
export declare function createBasicRouter<Context extends BasicCrawlingContext = BasicCrawlingContext, Routes extends Record<keyof Routes, Dictionary> = Record<string, GetUserDataFromRequest<Context['request']>>>(routes?: RouterRoutes<Context, Routes>): RouterHandler<Context, Routes>;
|
|
895
922
|
export declare function createBasicRouter<Context extends BasicCrawlingContext = BasicCrawlingContext, UserData extends Dictionary = GetUserDataFromRequest<Context['request']>>(routes?: RouterRoutes<Context, Record<string, UserData>>): RouterHandler<Context, Record<string, UserData>>;
|
|
896
|
-
export {};
|
|
@@ -8,8 +8,9 @@ import { ensureDir, writeJSON } from 'fs-extra/esm';
|
|
|
8
8
|
import ow, { ArgumentError } from 'ow';
|
|
9
9
|
import { getDomain } from 'tldts';
|
|
10
10
|
import { LruCache } from '@apify/datastructures';
|
|
11
|
-
import { addTimeoutToPromise, TimeoutError } from '@apify/timeout';
|
|
11
|
+
import { addTimeoutToPromise, extendTimeout, TimeoutError } from '@apify/timeout';
|
|
12
12
|
import { cryptoRandomObjectId } from '@apify/utilities';
|
|
13
|
+
import { extendTimeoutKey, navigationDeadlineKey, raceWithTimeout, timeoutExpiredKey, } from './request-timeout.js';
|
|
13
14
|
import { createSendRequest } from './send-request.js';
|
|
14
15
|
class LazyDefaultHttpClient {
|
|
15
16
|
_delegatePromise;
|
|
@@ -37,6 +38,8 @@ class LazyDefaultHttpClient {
|
|
|
37
38
|
*/
|
|
38
39
|
const SAFE_MIGRATION_WAIT_MILLIS = 20000;
|
|
39
40
|
const deferredCleanupKey = Symbol('deferredCleanup');
|
|
41
|
+
// The request timeout plumbing (the window helper, the context symbols, and the race) lives in its own module.
|
|
42
|
+
export { navigationDeadlineKey, remainingNavigationWindowMillis } from './request-timeout.js';
|
|
40
43
|
export class BasicCrawler {
|
|
41
44
|
static CRAWLEE_STATE_KEY = 'CRAWLEE_STATE';
|
|
42
45
|
/**
|
|
@@ -314,10 +317,10 @@ export class BasicCrawler {
|
|
|
314
317
|
this.retryOnBlocked = retryOnBlocked;
|
|
315
318
|
this.respectRobotsTxtFile = respectRobotsTxtFile;
|
|
316
319
|
this.onSkippedRequest = onSkippedRequest;
|
|
317
|
-
const tryEnv = (val) => (val == null ? null : +val);
|
|
318
320
|
// allow at least 5min for internal timeouts
|
|
319
321
|
this.internalTimeoutMillis =
|
|
320
|
-
|
|
322
|
+
serviceLocator.getConfiguration().internalTimeoutMillis ??
|
|
323
|
+
Math.max(this.requestHandlerTimeoutMillis * 2, 300e3);
|
|
321
324
|
this.maxRequestRetries = maxRequestRetries;
|
|
322
325
|
this.maxCrawlDepth = maxCrawlDepth;
|
|
323
326
|
this.sameDomainDelayMillis = sameDomainDelaySecs * 1000;
|
|
@@ -370,9 +373,12 @@ export class BasicCrawler {
|
|
|
370
373
|
this.stats.startJob(request.id || request.uniqueKey);
|
|
371
374
|
const crawlingContext = { request };
|
|
372
375
|
try {
|
|
373
|
-
|
|
376
|
+
// Navigation, the navigation hooks and the request handler are timed individually, but the
|
|
377
|
+
// phases between them are not, so a request could still get stuck indefinitely. This is the
|
|
378
|
+
// catch-all for that - see `raceWithTimeout` for why it is a bare timer, not a timeout frame.
|
|
379
|
+
await this.withRequestTimeout(crawlingContext, this.basicContextPipeline
|
|
374
380
|
.chain(this.contextPipeline)
|
|
375
|
-
.call(crawlingContext, (ctx) => this.handleRequest(ctx, source, request));
|
|
381
|
+
.call(crawlingContext, (ctx) => this.handleRequest(ctx, source, request)));
|
|
376
382
|
}
|
|
377
383
|
catch (error) {
|
|
378
384
|
// ContextPipelineInterruptedError means the request was intentionally skipped
|
|
@@ -383,8 +389,11 @@ export class BasicCrawler {
|
|
|
383
389
|
return;
|
|
384
390
|
}
|
|
385
391
|
// If the error happened during pipeline initialization (e.g., navigation timeout, session/proxy error,
|
|
386
|
-
// i.e. not in user's requestHandler), handle it through the normal error flow.
|
|
387
|
-
|
|
392
|
+
// i.e. not in user's requestHandler), handle it through the normal error flow. A bare `TimeoutError`
|
|
393
|
+
// here is the internal timeout above firing - anything else thrown inside the pipeline arrives wrapped.
|
|
394
|
+
const isPipelineError = error instanceof ContextPipelineInitializationError ||
|
|
395
|
+
error instanceof SessionError ||
|
|
396
|
+
error instanceof TimeoutError;
|
|
388
397
|
if (isPipelineError) {
|
|
389
398
|
const unwrappedError = this.unwrapError(error);
|
|
390
399
|
await this.requestFunctionErrorHandler(unwrappedError, crawlingContext, request, this.requestManager);
|
|
@@ -481,7 +490,7 @@ export class BasicCrawler {
|
|
|
481
490
|
buildBasicContextPipeline() {
|
|
482
491
|
return ContextPipeline.create()
|
|
483
492
|
.compose({ action: this.checkRobotsTxt.bind(this) })
|
|
484
|
-
.compose({ action: () => this.createBaseContext() })
|
|
493
|
+
.compose({ action: (context) => this.createBaseContext(context) })
|
|
485
494
|
.compose({ action: this.resolveSession.bind(this) })
|
|
486
495
|
.compose({ action: this.createContextHelpers.bind(this) });
|
|
487
496
|
}
|
|
@@ -505,7 +514,7 @@ export class BasicCrawler {
|
|
|
505
514
|
buildContextPipeline() {
|
|
506
515
|
return ContextPipeline.create();
|
|
507
516
|
}
|
|
508
|
-
createBaseContext() {
|
|
517
|
+
createBaseContext(context) {
|
|
509
518
|
const deferredCleanup = [];
|
|
510
519
|
return {
|
|
511
520
|
id: cryptoRandomObjectId(10),
|
|
@@ -516,6 +525,18 @@ export class BasicCrawler {
|
|
|
516
525
|
registerDeferredCleanup: (cleanup) => {
|
|
517
526
|
deferredCleanup.push(cleanup);
|
|
518
527
|
},
|
|
528
|
+
extendTimeout: (secs) => {
|
|
529
|
+
const extraMillis = secs * 1000;
|
|
530
|
+
// the current `addTimeoutToPromise` window (the request handler, or a navigation hook)...
|
|
531
|
+
extendTimeout(extraMillis);
|
|
532
|
+
// ...the internal timeout around the whole request, which is not an `addTimeoutToPromise` frame...
|
|
533
|
+
context[extendTimeoutKey]?.(extraMillis);
|
|
534
|
+
// ...and, when called from within the navigation phase, its shared window, so extending a hook
|
|
535
|
+
// extends the whole navigation budget rather than just that hook's step.
|
|
536
|
+
if (context[navigationDeadlineKey] !== undefined) {
|
|
537
|
+
context[navigationDeadlineKey] += extraMillis;
|
|
538
|
+
}
|
|
539
|
+
},
|
|
519
540
|
[deferredCleanupKey]: deferredCleanup,
|
|
520
541
|
};
|
|
521
542
|
}
|
|
@@ -856,7 +877,12 @@ export class BasicCrawler {
|
|
|
856
877
|
* regardless of whether the manager is a plain {@link RequestQueue} or a `RequestManagerTandem`.
|
|
857
878
|
*/
|
|
858
879
|
async applyRequestManagerTimeouts(requestManager) {
|
|
859
|
-
|
|
880
|
+
// A router route may hold a request for longer than the crawler's own timeout, and we cannot know
|
|
881
|
+
// which routes a run will hit, so reserve for the longest one any route asked for. The hint is
|
|
882
|
+
// raise-only, so erring high here is safe.
|
|
883
|
+
const maxRouteTimeoutSecs = this.requestHandler.getMaxTimeoutSecs?.() ?? 0;
|
|
884
|
+
const handlerTimeoutSecs = Math.max(this.requestHandlerTimeoutMillis / 1000, maxRouteTimeoutSecs);
|
|
885
|
+
await requestManager.setExpectedRequestProcessingTimeSecs?.(Math.max(handlerTimeoutSecs + 5, 60));
|
|
860
886
|
}
|
|
861
887
|
/**
|
|
862
888
|
* Validates a request source's `userData` against the {@link RouteSchemas|Standard Schema} registered
|
|
@@ -1068,6 +1094,16 @@ export class BasicCrawler {
|
|
|
1068
1094
|
await eventManager.init();
|
|
1069
1095
|
this._closeEvents = true;
|
|
1070
1096
|
}
|
|
1097
|
+
// Warn once at startup if the internal timeout is shorter than the phases it is meant to outlast. It is
|
|
1098
|
+
// floored per request so it will not actually cut them short, but the configured value is then effectively
|
|
1099
|
+
// ignored, which is worth flagging. Checked here (not in the constructor) because a subclass sets its
|
|
1100
|
+
// navigation timeout only after `super()`.
|
|
1101
|
+
const phasesMillis = this.getNavigationTimeoutMillis() + this.resolveRequestHandlerTimeoutMillis(undefined);
|
|
1102
|
+
if (this.internalTimeoutMillis < phasesMillis) {
|
|
1103
|
+
this.log.warning(`CRAWLEE_INTERNAL_TIMEOUT (${this.internalTimeoutMillis / 1000}s) is shorter than the navigation ` +
|
|
1104
|
+
`and request handler timeouts combined (${phasesMillis / 1000}s); it will be raised per request ` +
|
|
1105
|
+
`so it does not cut them short.`);
|
|
1106
|
+
}
|
|
1071
1107
|
// An owned governor is rebuilt (and started) for every run, so it always starts from a clean slate — stale
|
|
1072
1108
|
// resource snapshots or a previous run's scaled desired concurrency would otherwise distort this run's
|
|
1073
1109
|
// scaling. An injected one is long-lived and its lifecycle belongs to the caller.
|
|
@@ -1080,8 +1116,51 @@ export class BasicCrawler {
|
|
|
1080
1116
|
});
|
|
1081
1117
|
await this.getRequestManager();
|
|
1082
1118
|
}
|
|
1119
|
+
/**
|
|
1120
|
+
* The navigation timeout (pre-navigation hooks, navigation, and post-navigation hooks) in milliseconds, used
|
|
1121
|
+
* to size the internal request timeout. `BasicCrawler` has no navigation phase, so this is 0; the HTTP and
|
|
1122
|
+
* browser crawlers override it with their `navigationTimeoutSecs`.
|
|
1123
|
+
*/
|
|
1124
|
+
getNavigationTimeoutMillis() {
|
|
1125
|
+
return 0;
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Races the request against the internal timeout (see {@link raceWithTimeout}), sized to outlast the phases
|
|
1129
|
+
* that have their own timeout - the navigation, its hooks, and the request handler - so a legitimately slow
|
|
1130
|
+
* request, a per-route override, or a low `CRAWLEE_INTERNAL_TIMEOUT` is not cut short mid-phase. It takes
|
|
1131
|
+
* whichever is larger: the configured internal timeout, or this request's combined phase budget.
|
|
1132
|
+
*/
|
|
1133
|
+
async withRequestTimeout(crawlingContext, work) {
|
|
1134
|
+
const { request } = crawlingContext;
|
|
1135
|
+
const phasesMillis = this.getNavigationTimeoutMillis() + this.resolveRequestHandlerTimeoutMillis(request.label);
|
|
1136
|
+
const timeoutMillis = Math.max(this.internalTimeoutMillis, phasesMillis);
|
|
1137
|
+
await raceWithTimeout(crawlingContext, work, { timeoutMillis, requestId: request.id });
|
|
1138
|
+
}
|
|
1139
|
+
/**
|
|
1140
|
+
* The request handler timeout for a request with the given route label. A router route may override the
|
|
1141
|
+
* crawler's own `requestHandlerTimeoutSecs`; anything else falls back to `fallbackMillis`.
|
|
1142
|
+
*
|
|
1143
|
+
* @param label The request's route label, or `undefined` for the default route / no specific request.
|
|
1144
|
+
* @param fallbackMillis Timeout to use when no route overrides it.
|
|
1145
|
+
*/
|
|
1146
|
+
resolveRequestHandlerTimeoutMillis(label, fallbackMillis = this.requestHandlerTimeoutMillis) {
|
|
1147
|
+
return this.getRouteTimeoutMillis(label) ?? fallbackMillis;
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* The timeout the router route with the given label asked for, or `undefined` when it did not override one
|
|
1151
|
+
* (or the request handler is not a router at all).
|
|
1152
|
+
*/
|
|
1153
|
+
getRouteTimeoutMillis(label) {
|
|
1154
|
+
const getTimeoutSecs = this.requestHandler.getTimeoutSecs;
|
|
1155
|
+
if (typeof getTimeoutSecs !== 'function') {
|
|
1156
|
+
return undefined;
|
|
1157
|
+
}
|
|
1158
|
+
const timeoutSecs = getTimeoutSecs(label);
|
|
1159
|
+
return timeoutSecs === undefined ? undefined : timeoutSecs * 1000;
|
|
1160
|
+
}
|
|
1083
1161
|
async runRequestHandler(crawlingContext) {
|
|
1084
|
-
|
|
1162
|
+
const timeoutMillis = this.resolveRequestHandlerTimeoutMillis(crawlingContext.request.label);
|
|
1163
|
+
await addTimeoutToPromise(async () => this.requestHandler(crawlingContext), timeoutMillis, `requestHandler timed out after ${timeoutMillis / 1000} seconds (${crawlingContext.request.id}).`);
|
|
1085
1164
|
}
|
|
1086
1165
|
/**
|
|
1087
1166
|
* Handles blocked request
|
|
@@ -1189,6 +1268,12 @@ export class BasicCrawler {
|
|
|
1189
1268
|
}
|
|
1190
1269
|
/** Handles a single request - runs the request handler with retries, error handling, and lifecycle management. */
|
|
1191
1270
|
async handleRequest(crawlingContext, requestSource, request) {
|
|
1271
|
+
// An earlier phase we cannot cancel (e.g. a slow `extendContext`) may have run past the internal timeout,
|
|
1272
|
+
// which already failed the request in `runTaskFunction`. Bail before running the handler so it does not
|
|
1273
|
+
// execute (and re-report) on top of a request the crawler has already moved past.
|
|
1274
|
+
if (crawlingContext[timeoutExpiredKey]?.()) {
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1192
1277
|
const statisticsId = request.id || request.uniqueKey;
|
|
1193
1278
|
let isRequestLocked = true;
|
|
1194
1279
|
try {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared navigation-window deadline (epoch millis), stored on the in-flight context so the pre- and
|
|
3
|
+
* post-navigation hooks and the navigation itself all draw from one budget - and so `context.extendTimeout`
|
|
4
|
+
* can push the whole window, not just the current step.
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
export declare const navigationDeadlineKey: unique symbol;
|
|
8
|
+
/**
|
|
9
|
+
* Lets `context.extendTimeout` push back the internal request timeout as well. That timeout is a bare timer
|
|
10
|
+
* rather than an `addTimeoutToPromise` frame, so `extendTimeout` from `@apify/timeout` cannot reach it on its own.
|
|
11
|
+
*/
|
|
12
|
+
export declare const extendTimeoutKey: unique symbol;
|
|
13
|
+
/**
|
|
14
|
+
* Returns `true` once the internal request timeout has fired for this request. It cannot cancel work stuck
|
|
15
|
+
* somewhere we do not control, but the phases we do run (e.g. the request handler) check this at their start and
|
|
16
|
+
* bail, so a request whose timeout already elapsed does not carry on after the crawler moved past it.
|
|
17
|
+
*/
|
|
18
|
+
export declare const timeoutExpiredKey: unique symbol;
|
|
19
|
+
/** The slots the internal request timeout hangs on the in-flight crawling context. */
|
|
20
|
+
export interface RequestTimeoutContext {
|
|
21
|
+
[extendTimeoutKey]?: (extraMillis: number) => void;
|
|
22
|
+
[timeoutExpiredKey]?: () => boolean;
|
|
23
|
+
[navigationDeadlineKey]?: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Milliseconds left in the shared navigation window for `ctx`, lazily starting the window on first use.
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
29
|
+
export declare function remainingNavigationWindowMillis(ctx: object, windowMillis: number): number;
|
|
30
|
+
/**
|
|
31
|
+
* Races `work` against a `timeoutMillis` timeout, so a request stuck in a phase that has no timeout of its own
|
|
32
|
+
* (anything that is not the navigation, a navigation hook or the request handler) still fails instead of stalling
|
|
33
|
+
* the crawler.
|
|
34
|
+
*
|
|
35
|
+
* This is a bare timer, not an `addTimeoutToPromise` frame: nested `addTimeoutToPromise` calls share one
|
|
36
|
+
* `AbortController`, so wrapping the request in one would let the request handler timing out abort this outer
|
|
37
|
+
* context too, cancelling the error handling that follows it.
|
|
38
|
+
*
|
|
39
|
+
* `Promise.race` attaches a handler to both sides, so a late rejection from `work` cannot go unhandled. The losing
|
|
40
|
+
* side is not cancelled - by definition it is stuck somewhere we do not control - but the phases we do run check
|
|
41
|
+
* {@link timeoutExpiredKey} at their start and bail, so a request whose timeout already fired does not carry
|
|
42
|
+
* on (e.g. run the handler) after the crawler has moved past it. `context.extendTimeout` pushes the deadline back
|
|
43
|
+
* via {@link extendTimeoutKey}.
|
|
44
|
+
*/
|
|
45
|
+
export declare function raceWithTimeout(context: RequestTimeoutContext, work: Promise<void>, { timeoutMillis, requestId }: {
|
|
46
|
+
timeoutMillis: number;
|
|
47
|
+
requestId?: string;
|
|
48
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { TimeoutError } from '@apify/timeout';
|
|
2
|
+
/**
|
|
3
|
+
* The shared navigation-window deadline (epoch millis), stored on the in-flight context so the pre- and
|
|
4
|
+
* post-navigation hooks and the navigation itself all draw from one budget - and so `context.extendTimeout`
|
|
5
|
+
* can push the whole window, not just the current step.
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export const navigationDeadlineKey = Symbol('navigationDeadline');
|
|
9
|
+
/**
|
|
10
|
+
* Lets `context.extendTimeout` push back the internal request timeout as well. That timeout is a bare timer
|
|
11
|
+
* rather than an `addTimeoutToPromise` frame, so `extendTimeout` from `@apify/timeout` cannot reach it on its own.
|
|
12
|
+
*/
|
|
13
|
+
export const extendTimeoutKey = Symbol('extendTimeout');
|
|
14
|
+
/**
|
|
15
|
+
* Returns `true` once the internal request timeout has fired for this request. It cannot cancel work stuck
|
|
16
|
+
* somewhere we do not control, but the phases we do run (e.g. the request handler) check this at their start and
|
|
17
|
+
* bail, so a request whose timeout already elapsed does not carry on after the crawler moved past it.
|
|
18
|
+
*/
|
|
19
|
+
export const timeoutExpiredKey = Symbol('timeoutExpired');
|
|
20
|
+
/**
|
|
21
|
+
* Milliseconds left in the shared navigation window for `ctx`, lazily starting the window on first use.
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
export function remainingNavigationWindowMillis(ctx, windowMillis) {
|
|
25
|
+
const store = ctx;
|
|
26
|
+
store[navigationDeadlineKey] ??= Date.now() + windowMillis;
|
|
27
|
+
return store[navigationDeadlineKey] - Date.now();
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Races `work` against a `timeoutMillis` timeout, so a request stuck in a phase that has no timeout of its own
|
|
31
|
+
* (anything that is not the navigation, a navigation hook or the request handler) still fails instead of stalling
|
|
32
|
+
* the crawler.
|
|
33
|
+
*
|
|
34
|
+
* This is a bare timer, not an `addTimeoutToPromise` frame: nested `addTimeoutToPromise` calls share one
|
|
35
|
+
* `AbortController`, so wrapping the request in one would let the request handler timing out abort this outer
|
|
36
|
+
* context too, cancelling the error handling that follows it.
|
|
37
|
+
*
|
|
38
|
+
* `Promise.race` attaches a handler to both sides, so a late rejection from `work` cannot go unhandled. The losing
|
|
39
|
+
* side is not cancelled - by definition it is stuck somewhere we do not control - but the phases we do run check
|
|
40
|
+
* {@link timeoutExpiredKey} at their start and bail, so a request whose timeout already fired does not carry
|
|
41
|
+
* on (e.g. run the handler) after the crawler has moved past it. `context.extendTimeout` pushes the deadline back
|
|
42
|
+
* via {@link extendTimeoutKey}.
|
|
43
|
+
*/
|
|
44
|
+
export async function raceWithTimeout(context, work, { timeoutMillis, requestId }) {
|
|
45
|
+
let timer;
|
|
46
|
+
let deadline = Date.now() + timeoutMillis;
|
|
47
|
+
let settled = false;
|
|
48
|
+
let firedByTimeout = false;
|
|
49
|
+
const timeout = new Promise((_, reject) => {
|
|
50
|
+
const fire = () => {
|
|
51
|
+
settled = true;
|
|
52
|
+
firedByTimeout = true;
|
|
53
|
+
reject(new TimeoutError(`Request timed out after ${timeoutMillis / 1e3} seconds (${requestId}).`));
|
|
54
|
+
};
|
|
55
|
+
timer = setTimeout(fire, timeoutMillis);
|
|
56
|
+
context[timeoutExpiredKey] = () => firedByTimeout;
|
|
57
|
+
context[extendTimeoutKey] = (extraMillis) => {
|
|
58
|
+
if (settled) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
deadline += extraMillis;
|
|
63
|
+
timer = setTimeout(fire, Math.max(deadline - Date.now(), 0));
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
try {
|
|
67
|
+
await Promise.race([work, timeout]);
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
settled = true;
|
|
71
|
+
clearTimeout(timer);
|
|
72
|
+
}
|
|
73
|
+
}
|
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.97",
|
|
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"
|
|
@@ -40,12 +40,12 @@
|
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@apify/datastructures": "^2.0.0",
|
|
43
|
-
"@apify/timeout": "^0.
|
|
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.97",
|
|
46
|
+
"@crawlee/http-client": "4.0.0-beta.97",
|
|
47
|
+
"@crawlee/types": "4.0.0-beta.97",
|
|
48
|
+
"@crawlee/utils": "4.0.0-beta.97",
|
|
49
49
|
"csv-stringify": "^6.5.2",
|
|
50
50
|
"fs-extra": "^11.3.0",
|
|
51
51
|
"ow": "^2.0.0",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"type-fest": "^4.41.0"
|
|
55
55
|
},
|
|
56
56
|
"optionalDependencies": {
|
|
57
|
-
"@crawlee/impit-client": "^4.0.0-beta.
|
|
57
|
+
"@crawlee/impit-client": "^4.0.0-beta.97"
|
|
58
58
|
},
|
|
59
59
|
"lerna": {
|
|
60
60
|
"command": {
|
|
@@ -63,5 +63,5 @@
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
},
|
|
66
|
-
"gitHead": "
|
|
66
|
+
"gitHead": "43008dd43f4832d083353ba1b731c0b4607c9cfa"
|
|
67
67
|
}
|