@crawlee/basic 4.0.0-beta.20 → 4.0.0-beta.201
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/README.md +14 -14
- package/index.d.ts +11 -2
- package/index.js +11 -1
- package/internals/autoscaling/autoscaled_pool.d.ts +198 -0
- package/internals/autoscaling/autoscaled_pool.js +388 -0
- package/internals/autoscaling/concurrency_system.d.ts +273 -0
- package/internals/autoscaling/concurrency_system.js +359 -0
- package/internals/autoscaling/cpu_load_signal.d.ts +43 -0
- package/internals/autoscaling/cpu_load_signal.js +46 -0
- package/internals/autoscaling/event_loop_load_signal.d.ts +51 -0
- package/internals/autoscaling/event_loop_load_signal.js +60 -0
- package/internals/autoscaling/index.d.ts +9 -0
- package/internals/autoscaling/index.js +9 -0
- package/internals/autoscaling/load_signal.d.ts +98 -0
- package/internals/autoscaling/load_signal.js +103 -0
- package/internals/autoscaling/memory_load_signal.d.ts +47 -0
- package/internals/autoscaling/memory_load_signal.js +104 -0
- package/internals/autoscaling/snapshotter.d.ts +84 -0
- package/internals/autoscaling/snapshotter.js +67 -0
- package/internals/autoscaling/storage_backend_load_signal.d.ts +56 -0
- package/internals/autoscaling/storage_backend_load_signal.js +73 -0
- package/internals/autoscaling/system_status.d.ts +122 -0
- package/internals/autoscaling/system_status.js +139 -0
- package/internals/autoscaling/weighted_avg.d.ts +5 -0
- package/internals/autoscaling/weighted_avg.js +14 -0
- package/internals/basic-crawler.d.ts +448 -340
- package/internals/basic-crawler.js +1398 -823
- package/internals/cookie_utils.d.ts +40 -0
- package/internals/cookie_utils.js +122 -0
- package/internals/crawler-run.d.ts +76 -0
- package/internals/crawler-run.js +142 -0
- package/internals/crawlers/context_pipeline.d.ts +71 -0
- package/internals/crawlers/context_pipeline.js +117 -0
- package/internals/crawlers/crawler_commons.d.ts +194 -0
- package/internals/crawlers/crawler_commons.js +1 -0
- package/internals/crawlers/error_snapshotter.d.ts +29 -0
- package/internals/crawlers/error_snapshotter.js +116 -0
- package/internals/crawlers/error_tracker.d.ts +52 -0
- package/internals/crawlers/error_tracker.js +316 -0
- package/internals/crawlers/index.d.ts +4 -0
- package/internals/crawlers/index.js +3 -0
- package/internals/crawlers/internals/types.d.ts +7 -0
- package/internals/crawlers/internals/types.js +1 -0
- package/internals/crawlers/statistics.d.ts +318 -0
- package/internals/crawlers/statistics.js +525 -0
- package/internals/crawling_request.d.ts +86 -0
- package/internals/crawling_request.js +127 -0
- package/internals/enqueue_links/enqueue_links.d.ts +152 -0
- package/internals/enqueue_links/enqueue_links.js +76 -0
- package/internals/enqueue_links/index.d.ts +3 -0
- package/internals/enqueue_links/index.js +3 -0
- package/internals/enqueue_links/shared.d.ts +104 -0
- package/internals/enqueue_links/shared.js +258 -0
- package/internals/errors.d.ts +55 -0
- package/internals/errors.js +67 -0
- package/internals/http.d.ts +9 -0
- package/internals/http.js +28 -0
- package/internals/request-timeout.d.ts +48 -0
- package/internals/request-timeout.js +73 -0
- package/internals/router.d.ts +308 -0
- package/internals/router.js +312 -0
- package/internals/send-request.d.ts +4 -4
- package/internals/send-request.js +22 -11
- package/internals/session_pool/consts.d.ts +1 -0
- package/internals/session_pool/consts.js +1 -0
- package/internals/session_pool/errors.d.ts +7 -0
- package/internals/session_pool/errors.js +11 -0
- package/internals/session_pool/fingerprint.d.ts +9 -0
- package/internals/session_pool/fingerprint.js +30 -0
- package/internals/session_pool/index.d.ts +4 -0
- package/internals/session_pool/index.js +4 -0
- package/internals/session_pool/session.d.ts +155 -0
- package/internals/session_pool/session.js +222 -0
- package/internals/session_pool/session_pool.d.ts +241 -0
- package/internals/session_pool/session_pool.js +439 -0
- package/internals/sitemap_request_loader.d.ts +197 -0
- package/internals/sitemap_request_loader.js +432 -0
- package/internals/throttling_request_manager.d.ts +230 -0
- package/internals/throttling_request_manager.js +843 -0
- package/package.json +19 -13
- package/index.d.ts.map +0 -1
- package/index.js.map +0 -1
- package/internals/basic-crawler.d.ts.map +0 -1
- package/internals/basic-crawler.js.map +0 -1
- package/internals/send-request.d.ts.map +0 -1
- package/internals/send-request.js.map +0 -1
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { URL } from 'node:url';
|
|
2
|
+
import { Minimatch } from 'minimatch';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { Request } from '@crawlee/core';
|
|
5
|
+
import { schemas } from '@crawlee/utils/internal';
|
|
6
|
+
const MAX_ENQUEUE_LINKS_CACHE_SIZE = 1000;
|
|
7
|
+
/**
|
|
8
|
+
* To keep high performance when the same patterns are passed on every `enqueueLinks()` call,
|
|
9
|
+
* each glob/regexp is only compiled once and then kept in a cache.
|
|
10
|
+
* @ignore
|
|
11
|
+
*/
|
|
12
|
+
const enqueueLinksPatternCache = new Map();
|
|
13
|
+
/**
|
|
14
|
+
* Accepts one {@link UrlPatternInput} — a glob string, a RegExp instance, or a `{ glob }` / `{ regexp }` object.
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
export const urlPatternSchema = z.union([
|
|
18
|
+
z.string(),
|
|
19
|
+
z.instanceof(RegExp),
|
|
20
|
+
schemas.objectWithKeys(['glob']),
|
|
21
|
+
schemas.objectWithKeys(['regexp']),
|
|
22
|
+
]);
|
|
23
|
+
/**
|
|
24
|
+
* Builds the `{ request, reason }` argument passed to a {@link SkippedRequestCallback}, constructing the
|
|
25
|
+
* `Request` lazily on first access to `request` (and caching it) since most skips are never observed by a
|
|
26
|
+
* real callback and building a `Request` isn't free.
|
|
27
|
+
* @ignore
|
|
28
|
+
*/
|
|
29
|
+
export function createSkippedRequestArgs(source, reason) {
|
|
30
|
+
let request;
|
|
31
|
+
return {
|
|
32
|
+
reason,
|
|
33
|
+
get request() {
|
|
34
|
+
request ??=
|
|
35
|
+
source instanceof Request
|
|
36
|
+
? source
|
|
37
|
+
: new Request(typeof source === 'string' ? { url: source } : source);
|
|
38
|
+
return request;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* @ignore
|
|
44
|
+
*/
|
|
45
|
+
export function updateEnqueueLinksPatternCache(item, pattern) {
|
|
46
|
+
enqueueLinksPatternCache.set(item, pattern);
|
|
47
|
+
if (enqueueLinksPatternCache.size > MAX_ENQUEUE_LINKS_CACHE_SIZE) {
|
|
48
|
+
const key = enqueueLinksPatternCache.keys().next().value;
|
|
49
|
+
enqueueLinksPatternCache.delete(key);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Helper factory used in the `enqueueLinks()` and enqueueLinksByClickingElements() function
|
|
54
|
+
* to construct Glob objects from Glob pattern strings.
|
|
55
|
+
* @ignore
|
|
56
|
+
*/
|
|
57
|
+
export function constructGlobObjectsFromGlobs(globs) {
|
|
58
|
+
return globs
|
|
59
|
+
.filter((glob) => {
|
|
60
|
+
// Skip possibly nullish, empty strings
|
|
61
|
+
if (!glob) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
if (typeof glob === 'string') {
|
|
65
|
+
return glob.trim().length > 0;
|
|
66
|
+
}
|
|
67
|
+
if (glob.glob) {
|
|
68
|
+
return glob.glob.trim().length > 0;
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
})
|
|
72
|
+
.map((item) => {
|
|
73
|
+
// Get glob object from cache.
|
|
74
|
+
let globObject = enqueueLinksPatternCache.get(item);
|
|
75
|
+
if (globObject)
|
|
76
|
+
return globObject;
|
|
77
|
+
if (typeof item === 'string') {
|
|
78
|
+
globObject = { glob: validateGlobPattern(item) };
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
globObject = { glob: validateGlobPattern(item.glob) };
|
|
82
|
+
}
|
|
83
|
+
updateEnqueueLinksPatternCache(item, globObject);
|
|
84
|
+
return globObject;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* @internal
|
|
89
|
+
*/
|
|
90
|
+
export function validateGlobPattern(glob) {
|
|
91
|
+
const globTrimmed = glob.trim();
|
|
92
|
+
if (globTrimmed.length === 0)
|
|
93
|
+
throw new Error(`Cannot parse Glob pattern '${globTrimmed}': it must be an non-empty string`);
|
|
94
|
+
return globTrimmed;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Helper factory used in the `enqueueLinks()` and enqueueLinksByClickingElements() function
|
|
98
|
+
* to check RegExps input and return valid RegExps.
|
|
99
|
+
* @ignore
|
|
100
|
+
*/
|
|
101
|
+
export function constructRegExpObjectsFromRegExps(regexps) {
|
|
102
|
+
return regexps.map((item) => {
|
|
103
|
+
// Get regexp object from cache.
|
|
104
|
+
let regexpObject = enqueueLinksPatternCache.get(item);
|
|
105
|
+
if (regexpObject)
|
|
106
|
+
return regexpObject;
|
|
107
|
+
if (item instanceof RegExp) {
|
|
108
|
+
regexpObject = { regexp: item };
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
regexpObject = { regexp: item.regexp };
|
|
112
|
+
}
|
|
113
|
+
updateEnqueueLinksPatternCache(item, regexpObject);
|
|
114
|
+
return regexpObject;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Helper factory used in the `enqueueLinks()` function to construct UrlPatternObjects
|
|
119
|
+
* from a mixed array of glob strings, glob objects, RegExp instances, and regexp objects.
|
|
120
|
+
* @ignore
|
|
121
|
+
*/
|
|
122
|
+
export function constructUrlPatternObjects(patterns) {
|
|
123
|
+
const result = [];
|
|
124
|
+
for (const item of patterns) {
|
|
125
|
+
if (typeof item === 'string' || 'glob' in item) {
|
|
126
|
+
result.push(...constructGlobObjectsFromGlobs([item]));
|
|
127
|
+
}
|
|
128
|
+
else if (item instanceof RegExp || 'regexp' in item) {
|
|
129
|
+
result.push(...constructRegExpObjectsFromRegExps([item]));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Filters request options by URL patterns.
|
|
136
|
+
*
|
|
137
|
+
* When `includePatterns` is empty/undefined, all options pass through (only exclude filtering applies).
|
|
138
|
+
* @ignore
|
|
139
|
+
*/
|
|
140
|
+
export function filterRequestOptionsByPatterns(requestOptions, includePatterns, excludePatterns = [], strategy, onSkippedRequestOptions) {
|
|
141
|
+
const excludeMatchers = excludePatterns.map(createPatternObjectMatcher);
|
|
142
|
+
const includeMatchers = includePatterns?.length ? includePatterns.map(createPatternObjectMatcher) : undefined;
|
|
143
|
+
return requestOptions
|
|
144
|
+
.filter((opts) => {
|
|
145
|
+
const matchesExclude = excludeMatchers.some(({ match }) => match(opts.url));
|
|
146
|
+
if (matchesExclude) {
|
|
147
|
+
onSkippedRequestOptions?.(opts);
|
|
148
|
+
}
|
|
149
|
+
return !matchesExclude;
|
|
150
|
+
})
|
|
151
|
+
.map((opts) => {
|
|
152
|
+
if (!includeMatchers) {
|
|
153
|
+
return { ...opts, enqueueStrategy: strategy };
|
|
154
|
+
}
|
|
155
|
+
for (const { match } of includeMatchers) {
|
|
156
|
+
if (match(opts.url)) {
|
|
157
|
+
return { ...opts, enqueueStrategy: strategy };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// didn't match any positive pattern
|
|
161
|
+
onSkippedRequestOptions?.(opts);
|
|
162
|
+
return null;
|
|
163
|
+
})
|
|
164
|
+
.filter((opts) => opts !== null);
|
|
165
|
+
}
|
|
166
|
+
function isAbsoluteUrl(url) {
|
|
167
|
+
try {
|
|
168
|
+
// eslint-disable-next-line no-new
|
|
169
|
+
new URL(url);
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* @ignore
|
|
178
|
+
*/
|
|
179
|
+
export function createRequestOptions(sources, options = {}) {
|
|
180
|
+
return sources
|
|
181
|
+
.map((src) => typeof src === 'string'
|
|
182
|
+
? { url: src, enqueueStrategy: options.strategy }
|
|
183
|
+
: { ...src, enqueueStrategy: options.strategy })
|
|
184
|
+
.filter(({ url }) => {
|
|
185
|
+
try {
|
|
186
|
+
return new URL(url, options.baseUrl).href;
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
.map((requestOptions) => {
|
|
193
|
+
// Leave already-absolute URLs untouched - re-deriving them via `new URL()` would normalize them
|
|
194
|
+
// (e.g. adding a trailing slash to a bare domain), which is surprising for URLs that didn't need
|
|
195
|
+
// resolving against `baseUrl` in the first place.
|
|
196
|
+
if (!isAbsoluteUrl(requestOptions.url)) {
|
|
197
|
+
requestOptions.url = new URL(requestOptions.url, options.baseUrl).href;
|
|
198
|
+
}
|
|
199
|
+
requestOptions.userData ??= options.userData ?? {};
|
|
200
|
+
if (typeof options.label === 'string') {
|
|
201
|
+
requestOptions.userData = {
|
|
202
|
+
...requestOptions.userData,
|
|
203
|
+
label: options.label,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
if (options.skipNavigation) {
|
|
207
|
+
requestOptions.skipNavigation = true;
|
|
208
|
+
}
|
|
209
|
+
if (options.sessionId) {
|
|
210
|
+
requestOptions.sessionId = options.sessionId;
|
|
211
|
+
}
|
|
212
|
+
return requestOptions;
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* @ignore
|
|
217
|
+
*/
|
|
218
|
+
function createPatternObjectMatcher(urlPatternObject) {
|
|
219
|
+
const { regexp, glob } = urlPatternObject;
|
|
220
|
+
let match;
|
|
221
|
+
if (regexp) {
|
|
222
|
+
match = (url) => regexp.test(url);
|
|
223
|
+
}
|
|
224
|
+
else if (glob) {
|
|
225
|
+
const m = new Minimatch(glob, { nocase: true });
|
|
226
|
+
match = (url) => m.match(url);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
match = () => false;
|
|
230
|
+
}
|
|
231
|
+
return { match };
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Applies a {@link RequestTransform} function to a list of request options.
|
|
235
|
+
* Options for which the transform returns a falsy value are removed from the list.
|
|
236
|
+
* @param onSkipped Called with the original request options when the transform returns a falsy value (i.e. the request is skipped).
|
|
237
|
+
* @ignore
|
|
238
|
+
* @internal
|
|
239
|
+
*/
|
|
240
|
+
export function applyRequestTransform(requestOptions, transformFn, onSkipped) {
|
|
241
|
+
return requestOptions
|
|
242
|
+
.map((opts) => {
|
|
243
|
+
const transformed = transformFn(opts);
|
|
244
|
+
if (transformed === 'skip') {
|
|
245
|
+
onSkipped?.(opts);
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
if (transformed === 'unchanged') {
|
|
249
|
+
return opts;
|
|
250
|
+
}
|
|
251
|
+
if (!transformed) {
|
|
252
|
+
onSkipped?.(opts);
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
return transformed;
|
|
256
|
+
})
|
|
257
|
+
.filter((r) => r !== null);
|
|
258
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { CriticalError, NonRetryableError } from '@crawlee/core';
|
|
2
|
+
/**
|
|
3
|
+
* @ignore
|
|
4
|
+
*/
|
|
5
|
+
export declare class MissingRouteError extends CriticalError {
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Errors of `RetryRequestError` type will always be retried by the crawler.
|
|
9
|
+
*
|
|
10
|
+
* *This error overrides the `maxRequestRetries` option, i.e. the request can be retried indefinitely until it succeeds.*
|
|
11
|
+
*/
|
|
12
|
+
export declare class RetryRequestError extends Error {
|
|
13
|
+
constructor(message?: string);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Thrown when a domain has rate-limited us and the request should simply be attempted again later.
|
|
17
|
+
*
|
|
18
|
+
* The request is reclaimed without recording a failure: it costs neither a retry nor session reputation, because
|
|
19
|
+
* nothing about the request or the session was at fault. A {@link ThrottlingRequestManager} holds it back until
|
|
20
|
+
* the domain's backoff expires, so retries are paced rather than immediate.
|
|
21
|
+
*/
|
|
22
|
+
export declare class RequestThrottledError extends RetryRequestError {
|
|
23
|
+
constructor(message?: string);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Thrown when a domain has rate-limited us for so long that no request has got through, and the crawl is
|
|
27
|
+
* abandoned rather than kept waiting.
|
|
28
|
+
*
|
|
29
|
+
* Waiting longer will not help: at this point the concurrency is too high for the domain, or it has blocked us.
|
|
30
|
+
* The affected requests are deliberately left in their queue, so re-running the crawl without purging storages
|
|
31
|
+
* resumes them once the domain recovers.
|
|
32
|
+
*/
|
|
33
|
+
export declare class PersistentRateLimitError extends CriticalError {
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Thrown when a requested session is not found in the referenced SessionPool.
|
|
37
|
+
*/
|
|
38
|
+
export declare class MissingSessionError extends Error {
|
|
39
|
+
constructor(sessionId?: string);
|
|
40
|
+
}
|
|
41
|
+
export declare class ContextPipelineInterruptedError extends Error {
|
|
42
|
+
constructor(message?: string);
|
|
43
|
+
}
|
|
44
|
+
export declare class ContextPipelineCleanupError extends CriticalError {
|
|
45
|
+
constructor(error: unknown, options?: ErrorOptions);
|
|
46
|
+
}
|
|
47
|
+
export declare class RequestHandlerError extends Error {
|
|
48
|
+
constructor(error: unknown, options?: ErrorOptions);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Thrown by crawlers when `skipNavigation` is used on a request.
|
|
52
|
+
* Subclasses can catch this error to skip their own navigation-dependent logic.
|
|
53
|
+
*/
|
|
54
|
+
export declare class NavigationSkippedError extends NonRetryableError {
|
|
55
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { CriticalError, NonRetryableError } from '@crawlee/core';
|
|
2
|
+
/**
|
|
3
|
+
* @ignore
|
|
4
|
+
*/
|
|
5
|
+
export class MissingRouteError extends CriticalError {
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Errors of `RetryRequestError` type will always be retried by the crawler.
|
|
9
|
+
*
|
|
10
|
+
* *This error overrides the `maxRequestRetries` option, i.e. the request can be retried indefinitely until it succeeds.*
|
|
11
|
+
*/
|
|
12
|
+
export class RetryRequestError extends Error {
|
|
13
|
+
constructor(message) {
|
|
14
|
+
super(message ?? "Request is being retried at the user's request");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Thrown when a domain has rate-limited us and the request should simply be attempted again later.
|
|
19
|
+
*
|
|
20
|
+
* The request is reclaimed without recording a failure: it costs neither a retry nor session reputation, because
|
|
21
|
+
* nothing about the request or the session was at fault. A {@link ThrottlingRequestManager} holds it back until
|
|
22
|
+
* the domain's backoff expires, so retries are paced rather than immediate.
|
|
23
|
+
*/
|
|
24
|
+
export class RequestThrottledError extends RetryRequestError {
|
|
25
|
+
constructor(message) {
|
|
26
|
+
super(message ?? 'Request is being retried later because its domain is rate-limiting us');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Thrown when a domain has rate-limited us for so long that no request has got through, and the crawl is
|
|
31
|
+
* abandoned rather than kept waiting.
|
|
32
|
+
*
|
|
33
|
+
* Waiting longer will not help: at this point the concurrency is too high for the domain, or it has blocked us.
|
|
34
|
+
* The affected requests are deliberately left in their queue, so re-running the crawl without purging storages
|
|
35
|
+
* resumes them once the domain recovers.
|
|
36
|
+
*/
|
|
37
|
+
export class PersistentRateLimitError extends CriticalError {
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Thrown when a requested session is not found in the referenced SessionPool.
|
|
41
|
+
*/
|
|
42
|
+
export class MissingSessionError extends Error {
|
|
43
|
+
constructor(sessionId) {
|
|
44
|
+
super(`The current SessionPool instance couldn't find a valid session${sessionId ? ` for the following id: ${sessionId}.` : '.'}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export class ContextPipelineInterruptedError extends Error {
|
|
48
|
+
constructor(message) {
|
|
49
|
+
super(`Request handling was interrupted during context initialization ${message ? ` - ${message}` : ''}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export class ContextPipelineCleanupError extends CriticalError {
|
|
53
|
+
constructor(error, options) {
|
|
54
|
+
super(undefined, { cause: error, ...options });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export class RequestHandlerError extends Error {
|
|
58
|
+
constructor(error, options) {
|
|
59
|
+
super(undefined, { cause: error, ...options });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Thrown by crawlers when `skipNavigation` is used on a request.
|
|
64
|
+
* Subclasses can catch this error to skip their own navigation-dependent logic.
|
|
65
|
+
*/
|
|
66
|
+
export class NavigationSkippedError extends NonRetryableError {
|
|
67
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses a `Retry-After` response header into a delay in milliseconds.
|
|
3
|
+
*
|
|
4
|
+
* The header holds either a non-negative number of seconds or an HTTP-date.
|
|
5
|
+
* See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Retry-After).
|
|
6
|
+
*
|
|
7
|
+
* @returns The delay in milliseconds, or `null` if the header is absent, unparseable, or already elapsed.
|
|
8
|
+
*/
|
|
9
|
+
export declare function parseRetryAfterHeader(value?: string | null): number | null;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses a `Retry-After` response header into a delay in milliseconds.
|
|
3
|
+
*
|
|
4
|
+
* The header holds either a non-negative number of seconds or an HTTP-date.
|
|
5
|
+
* See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Retry-After).
|
|
6
|
+
*
|
|
7
|
+
* @returns The delay in milliseconds, or `null` if the header is absent, unparseable, or already elapsed.
|
|
8
|
+
*/
|
|
9
|
+
export function parseRetryAfterHeader(value) {
|
|
10
|
+
if (!value) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
const trimmed = value.trim();
|
|
14
|
+
// Per the spec this is a `delay-seconds`: digits only, so a negative or fractional value is not one.
|
|
15
|
+
if (/^\d+$/.test(trimmed)) {
|
|
16
|
+
// `Retry-After: 0` names no future deadline, same as an HTTP-date that has already passed. Reporting it
|
|
17
|
+
// as a zero delay would leave the domain unthrottled while still counting as a rate-limit event, so the
|
|
18
|
+
// caller would defer the request for free and re-send it immediately.
|
|
19
|
+
const delayMs = Number(trimmed) * 1000;
|
|
20
|
+
return delayMs > 0 ? delayMs : null;
|
|
21
|
+
}
|
|
22
|
+
const date = Date.parse(trimmed);
|
|
23
|
+
if (!Number.isNaN(date)) {
|
|
24
|
+
const delayMs = date - Date.now();
|
|
25
|
+
return delayMs > 0 ? delayMs : null;
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
@@ -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
|
+
}
|