@crawlee/core 4.0.0-beta.122 → 4.0.0-beta.124
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/autoscaling/concurrency_system.d.ts +2 -2
- package/autoscaling/index.d.ts +1 -1
- package/autoscaling/index.js +1 -1
- package/autoscaling/load_signal.d.ts +7 -6
- package/autoscaling/load_signal.js +2 -1
- package/autoscaling/snapshotter.d.ts +6 -6
- package/autoscaling/snapshotter.js +9 -9
- package/autoscaling/{client_load_signal.d.ts → storage_backend_load_signal.d.ts} +13 -12
- package/autoscaling/{client_load_signal.js → storage_backend_load_signal.js} +11 -11
- package/autoscaling/system_status.d.ts +8 -8
- package/autoscaling/system_status.js +2 -2
- package/crawlers/crawler_commons.d.ts +8 -54
- package/enqueue_links/enqueue_links.d.ts +33 -61
- package/enqueue_links/enqueue_links.js +35 -157
- package/enqueue_links/shared.d.ts +11 -4
- package/enqueue_links/shared.js +16 -1
- package/events/event_manager.js +2 -1
- package/package.json +6 -6
- package/request.d.ts +2 -2
- package/router.d.ts +5 -5
- package/storages/throttling_request_manager.d.ts +72 -49
- package/storages/throttling_request_manager.js +321 -130
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
import { getDomain } from 'tldts';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
import { Request } from '../request.js';
|
|
4
|
-
import { parseArgument, schemas } from '../validators.js';
|
|
5
|
-
import { applyRequestTransform, constructUrlPatternObjects, createRequestOptions, filterRequestOptionsByPatterns, urlPatternSchema, } from './shared.js';
|
|
6
2
|
/**
|
|
7
3
|
* The different enqueueing strategies available.
|
|
8
4
|
*
|
|
@@ -55,159 +51,6 @@ export var EnqueueStrategy;
|
|
|
55
51
|
*/
|
|
56
52
|
EnqueueStrategy["SameOrigin"] = "same-origin";
|
|
57
53
|
})(EnqueueStrategy || (EnqueueStrategy = {}));
|
|
58
|
-
// `schemas.anyObject` passes values through by reference (object schemas return a pruned plain
|
|
59
|
-
// copy), so `userData` keeps its identity for the enqueued requests.
|
|
60
|
-
const enqueueLinksOptionsSchema = z.strictObject({
|
|
61
|
-
urls: schemas.arrayOf(z.string(), 'strings'),
|
|
62
|
-
requestManager: schemas.objectWithKeys(['addRequestsBatched']),
|
|
63
|
-
robotsTxtFile: schemas.objectWithKeys(['isAllowed']).optional(),
|
|
64
|
-
respectRobotsTxtFile: z.union([z.boolean(), z.strictObject({ userAgent: z.string().optional() })]).optional(),
|
|
65
|
-
onSkippedRequest: schemas.anyFunction.optional(),
|
|
66
|
-
forefront: z.boolean().optional(),
|
|
67
|
-
skipNavigation: z.boolean().optional(),
|
|
68
|
-
sessionId: z.string().optional(),
|
|
69
|
-
limit: schemas.anyNumber.optional(),
|
|
70
|
-
selector: z.string().optional(),
|
|
71
|
-
baseUrl: z.string().optional(),
|
|
72
|
-
userData: schemas.anyObject.optional(),
|
|
73
|
-
label: z.string().optional(),
|
|
74
|
-
include: schemas.arrayOf(urlPatternSchema, 'URL patterns').min(1).optional(),
|
|
75
|
-
exclude: schemas.arrayOf(urlPatternSchema, 'URL patterns').optional(),
|
|
76
|
-
transformRequestFunction: schemas.anyFunction.optional(),
|
|
77
|
-
strategy: z.enum(EnqueueStrategy).optional(),
|
|
78
|
-
waitForAllRequestsToBeAdded: z.boolean().optional(),
|
|
79
|
-
});
|
|
80
|
-
/**
|
|
81
|
-
* This function enqueues the urls provided to the {@link RequestQueue} provided. If you want to automatically find and enqueue links,
|
|
82
|
-
* you should use the context-aware `enqueueLinks` function provided on the crawler contexts.
|
|
83
|
-
*
|
|
84
|
-
* Optionally, the function allows you to filter the target links' URLs using an array of glob or regexp patterns.
|
|
85
|
-
*
|
|
86
|
-
* **Example usage**
|
|
87
|
-
*
|
|
88
|
-
* ```javascript
|
|
89
|
-
* await enqueueLinks({
|
|
90
|
-
* urls: aListOfFoundUrls,
|
|
91
|
-
* requestManager,
|
|
92
|
-
* selector: 'a.product-detail',
|
|
93
|
-
* include: [
|
|
94
|
-
* 'https://www.example.com/handbags/*',
|
|
95
|
-
* 'https://www.example.com/purses/*'
|
|
96
|
-
* ],
|
|
97
|
-
* });
|
|
98
|
-
* ```
|
|
99
|
-
*
|
|
100
|
-
* @param options All `enqueueLinks()` parameters are passed via an options object.
|
|
101
|
-
* @returns Promise that resolves to {@link BatchAddRequestsResult} object.
|
|
102
|
-
*/
|
|
103
|
-
export async function enqueueLinks(options) {
|
|
104
|
-
if (!options || Object.keys(options).length === 0) {
|
|
105
|
-
throw new RangeError([
|
|
106
|
-
'enqueueLinks() was called without the required options. You can only do that when you use the `crawlingContext.enqueueLinks()` method in request handlers.',
|
|
107
|
-
'Check out our guide on how to use enqueueLinks() here: https://crawlee.dev/js/docs/examples/crawl-relative-links',
|
|
108
|
-
].join('\n'));
|
|
109
|
-
}
|
|
110
|
-
const parsedOptions = parseArgument(options, enqueueLinksOptionsSchema, 'EnqueueLinksOptions');
|
|
111
|
-
const { requestManager, limit, urls, include, exclude, transformRequestFunction, forefront, waitForAllRequestsToBeAdded, robotsTxtFile, onSkippedRequest, } = parsedOptions;
|
|
112
|
-
const urlExcludePatternObjects = exclude?.length ? constructUrlPatternObjects(exclude) : [];
|
|
113
|
-
const urlPatternObjects = include?.length ? constructUrlPatternObjects(include) : [];
|
|
114
|
-
// The strategy always applies, even when `include` patterns are provided - the two are AND-ed together
|
|
115
|
-
// (a URL must match an `include` pattern *and* satisfy the strategy). This mirrors crawlee-python.
|
|
116
|
-
parsedOptions.strategy ??= EnqueueStrategy.SameHostname;
|
|
117
|
-
const enqueueStrategyPatterns = [];
|
|
118
|
-
if (parsedOptions.baseUrl) {
|
|
119
|
-
const url = new URL(parsedOptions.baseUrl);
|
|
120
|
-
switch (parsedOptions.strategy) {
|
|
121
|
-
case EnqueueStrategy.SameHostname:
|
|
122
|
-
// We need to get the origin of the passed in domain in the event someone sets baseUrl
|
|
123
|
-
// to an url like https://example.com/deep/default/path and one of the found urls is an
|
|
124
|
-
// absolute relative path (/path/to/page)
|
|
125
|
-
enqueueStrategyPatterns.push({ glob: ignoreHttpSchema(`${url.origin}/**`) });
|
|
126
|
-
break;
|
|
127
|
-
case EnqueueStrategy.SameDomain: {
|
|
128
|
-
// Get the actual hostname from the base url
|
|
129
|
-
const baseUrlHostname = getDomain(url.hostname, { mixedInputs: false });
|
|
130
|
-
if (baseUrlHostname) {
|
|
131
|
-
// We have a hostname, so we can use it to match all links on the page that point to it and any subdomains of it
|
|
132
|
-
url.hostname = baseUrlHostname;
|
|
133
|
-
enqueueStrategyPatterns.push({ glob: ignoreHttpSchema(`${url.origin.replace(baseUrlHostname, `*.${baseUrlHostname}`)}/**`) }, { glob: ignoreHttpSchema(`${url.origin}/**`) });
|
|
134
|
-
}
|
|
135
|
-
else {
|
|
136
|
-
// We don't have a hostname (can happen for ips for instance), so reproduce the same behavior
|
|
137
|
-
// as SameDomainAndSubdomain
|
|
138
|
-
enqueueStrategyPatterns.push({ glob: ignoreHttpSchema(`${url.origin}/**`) });
|
|
139
|
-
}
|
|
140
|
-
break;
|
|
141
|
-
}
|
|
142
|
-
case EnqueueStrategy.SameOrigin: {
|
|
143
|
-
// The same behavior as SameHostname, but respecting the protocol of the URL
|
|
144
|
-
enqueueStrategyPatterns.push({ glob: `${url.origin}/**` });
|
|
145
|
-
break;
|
|
146
|
-
}
|
|
147
|
-
case EnqueueStrategy.All:
|
|
148
|
-
default:
|
|
149
|
-
enqueueStrategyPatterns.push({ glob: `http{s,}://**` });
|
|
150
|
-
break;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
async function reportSkippedRequests(skippedRequests, reason) {
|
|
154
|
-
if (onSkippedRequest && skippedRequests.length > 0) {
|
|
155
|
-
await Promise.all(skippedRequests.map((request) => {
|
|
156
|
-
return onSkippedRequest({
|
|
157
|
-
url: request.url,
|
|
158
|
-
reason: request.skippedReason ?? reason,
|
|
159
|
-
});
|
|
160
|
-
}));
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
let requestOptions = createRequestOptions(urls, parsedOptions);
|
|
164
|
-
if (robotsTxtFile && parsedOptions.respectRobotsTxtFile !== false) {
|
|
165
|
-
const robotsUserAgent = typeof parsedOptions.respectRobotsTxtFile === 'object'
|
|
166
|
-
? (parsedOptions.respectRobotsTxtFile.userAgent ?? '*')
|
|
167
|
-
: '*';
|
|
168
|
-
const skippedRequests = [];
|
|
169
|
-
requestOptions = requestOptions.filter((request) => {
|
|
170
|
-
if (robotsTxtFile.isAllowed(request.url, robotsUserAgent)) {
|
|
171
|
-
return true;
|
|
172
|
-
}
|
|
173
|
-
skippedRequests.push(request);
|
|
174
|
-
return false;
|
|
175
|
-
});
|
|
176
|
-
await reportSkippedRequests(skippedRequests, 'robotsTxt');
|
|
177
|
-
}
|
|
178
|
-
async function createFilteredRequests() {
|
|
179
|
-
const skippedRequests = [];
|
|
180
|
-
// Step 1: Filter request options by exclude patterns, user include patterns, and strategy patterns.
|
|
181
|
-
let filteredOptions;
|
|
182
|
-
if (urlPatternObjects.length === 0) {
|
|
183
|
-
filteredOptions = filterRequestOptionsByPatterns(requestOptions, enqueueStrategyPatterns.length > 0 ? enqueueStrategyPatterns : undefined, urlExcludePatternObjects, parsedOptions.strategy, (url) => skippedRequests.push(url));
|
|
184
|
-
}
|
|
185
|
-
else {
|
|
186
|
-
// Filter by user patterns first (with exclude)
|
|
187
|
-
const afterUserPatterns = filterRequestOptionsByPatterns(requestOptions, urlPatternObjects, urlExcludePatternObjects, parsedOptions.strategy, (url) => skippedRequests.push(url));
|
|
188
|
-
// ...then filter by the enqueue links strategy (making this an AND check)
|
|
189
|
-
filteredOptions = filterRequestOptionsByPatterns(afterUserPatterns, enqueueStrategyPatterns.length > 0 ? enqueueStrategyPatterns : undefined, [], parsedOptions.strategy, (url) => skippedRequests.push(url));
|
|
190
|
-
}
|
|
191
|
-
await reportSkippedRequests(skippedRequests.map((url) => ({ url })), 'filters');
|
|
192
|
-
// Step 2: Apply transformRequestFunction on request options - it has the highest priority
|
|
193
|
-
if (transformRequestFunction) {
|
|
194
|
-
const skippedByTransform = [];
|
|
195
|
-
filteredOptions = applyRequestTransform(filteredOptions, transformRequestFunction, (r) => skippedByTransform.push(r));
|
|
196
|
-
await reportSkippedRequests(skippedByTransform, 'transform');
|
|
197
|
-
}
|
|
198
|
-
// Step 3: Create Request instances from the final request options
|
|
199
|
-
return filteredOptions.map((opts) => new Request(opts));
|
|
200
|
-
}
|
|
201
|
-
const { addedRequests, requestsOverLimit } = await requestManager.addRequestsBatched(await createFilteredRequests(), {
|
|
202
|
-
forefront,
|
|
203
|
-
waitForAllRequestsToBeAdded,
|
|
204
|
-
maxNewRequests: limit,
|
|
205
|
-
});
|
|
206
|
-
if (requestsOverLimit?.length !== undefined && requestsOverLimit.length > 0) {
|
|
207
|
-
await reportSkippedRequests(requestsOverLimit.map((r) => ({ url: typeof r === 'string' ? r : r.url })), 'enqueueLimit');
|
|
208
|
-
}
|
|
209
|
-
return { processedRequests: addedRequests, unprocessedRequests: [] };
|
|
210
|
-
}
|
|
211
54
|
/**
|
|
212
55
|
* @internal
|
|
213
56
|
* This method helps resolve the baseUrl that will be used for filtering in {@link enqueueLinks}.
|
|
@@ -242,6 +85,41 @@ export function resolveBaseUrlForEnqueueLinksFiltering({ enqueueStrategy, finalR
|
|
|
242
85
|
// before actually finding the urls
|
|
243
86
|
return originalUrlOrigin;
|
|
244
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* @internal
|
|
90
|
+
* Builds the glob patterns a URL must match to satisfy the given enqueue `strategy`, anchored at `baseUrl`.
|
|
91
|
+
*/
|
|
92
|
+
export function buildEnqueueStrategyPatterns(baseUrl, strategy) {
|
|
93
|
+
const url = new URL(baseUrl);
|
|
94
|
+
switch (strategy) {
|
|
95
|
+
case EnqueueStrategy.SameHostname:
|
|
96
|
+
// We need to get the origin of the passed in domain in the event someone sets baseUrl
|
|
97
|
+
// to an url like https://example.com/deep/default/path and one of the found urls is an
|
|
98
|
+
// absolute relative path (/path/to/page)
|
|
99
|
+
return [{ glob: ignoreHttpSchema(`${url.origin}/**`) }];
|
|
100
|
+
case EnqueueStrategy.SameDomain: {
|
|
101
|
+
// Get the actual hostname from the base url
|
|
102
|
+
const baseUrlHostname = getDomain(url.hostname, { mixedInputs: false });
|
|
103
|
+
if (baseUrlHostname) {
|
|
104
|
+
// We have a hostname, so we can use it to match all links on the page that point to it and any subdomains of it
|
|
105
|
+
url.hostname = baseUrlHostname;
|
|
106
|
+
return [
|
|
107
|
+
{ glob: ignoreHttpSchema(`${url.origin.replace(baseUrlHostname, `*.${baseUrlHostname}`)}/**`) },
|
|
108
|
+
{ glob: ignoreHttpSchema(`${url.origin}/**`) },
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
// We don't have a hostname (can happen for ips for instance), so reproduce the same behavior
|
|
112
|
+
// as SameDomainAndSubdomain
|
|
113
|
+
return [{ glob: ignoreHttpSchema(`${url.origin}/**`) }];
|
|
114
|
+
}
|
|
115
|
+
case EnqueueStrategy.SameOrigin:
|
|
116
|
+
// The same behavior as SameHostname, but respecting the protocol of the URL
|
|
117
|
+
return [{ glob: `${url.origin}/**` }];
|
|
118
|
+
case EnqueueStrategy.All:
|
|
119
|
+
default:
|
|
120
|
+
return [{ glob: `http{s,}://**` }];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
245
123
|
/**
|
|
246
124
|
* Internal function that changes the enqueue glob patterns to match both http and https
|
|
247
125
|
*/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { Awaitable } from '@crawlee/types';
|
|
1
|
+
import type { Awaitable, Dictionary } from '@crawlee/types';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import type { RequestOptions } from '../request.js';
|
|
4
|
-
import type {
|
|
4
|
+
import type { EnqueueStrategyOption } from './enqueue_links.js';
|
|
5
5
|
export { tryAbsoluteURL } from '@crawlee/utils/internal';
|
|
6
6
|
export interface UrlPatternObject {
|
|
7
7
|
glob?: string;
|
|
@@ -59,11 +59,18 @@ export declare function constructUrlPatternObjects(patterns: readonly UrlPattern
|
|
|
59
59
|
* When `includePatterns` is empty/undefined, all options pass through (only exclude filtering applies).
|
|
60
60
|
* @ignore
|
|
61
61
|
*/
|
|
62
|
-
export declare function filterRequestOptionsByPatterns(requestOptions: RequestOptions[], includePatterns: UrlPatternObject[] | undefined, excludePatterns?: UrlPatternObject[], strategy?:
|
|
62
|
+
export declare function filterRequestOptionsByPatterns(requestOptions: RequestOptions[], includePatterns: UrlPatternObject[] | undefined, excludePatterns?: UrlPatternObject[], strategy?: EnqueueStrategyOption, onSkippedUrl?: (url: string) => void): RequestOptions[];
|
|
63
63
|
/**
|
|
64
64
|
* @ignore
|
|
65
65
|
*/
|
|
66
|
-
export declare function createRequestOptions(sources: readonly (string | Record<string, unknown>)[], options?:
|
|
66
|
+
export declare function createRequestOptions(sources: readonly (string | Record<string, unknown>)[], options?: {
|
|
67
|
+
label?: string;
|
|
68
|
+
userData?: Dictionary;
|
|
69
|
+
baseUrl?: string;
|
|
70
|
+
skipNavigation?: boolean;
|
|
71
|
+
sessionId?: string;
|
|
72
|
+
strategy?: EnqueueStrategyOption;
|
|
73
|
+
}): RequestOptions[];
|
|
67
74
|
/**
|
|
68
75
|
* Takes a {@link RequestOptions} object and changes its attributes in a desired way. This user-function is used
|
|
69
76
|
* by {@link enqueueLinks} to modify request options before they are converted to {@link Request} instances.
|
package/enqueue_links/shared.js
CHANGED
|
@@ -144,6 +144,16 @@ export function filterRequestOptionsByPatterns(requestOptions, includePatterns,
|
|
|
144
144
|
})
|
|
145
145
|
.filter((opts) => opts !== null);
|
|
146
146
|
}
|
|
147
|
+
function isAbsoluteUrl(url) {
|
|
148
|
+
try {
|
|
149
|
+
// eslint-disable-next-line no-new
|
|
150
|
+
new URL(url);
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
147
157
|
/**
|
|
148
158
|
* @ignore
|
|
149
159
|
*/
|
|
@@ -161,7 +171,12 @@ export function createRequestOptions(sources, options = {}) {
|
|
|
161
171
|
}
|
|
162
172
|
})
|
|
163
173
|
.map((requestOptions) => {
|
|
164
|
-
|
|
174
|
+
// Leave already-absolute URLs untouched - re-deriving them via `new URL()` would normalize them
|
|
175
|
+
// (e.g. adding a trailing slash to a bare domain), which is surprising for URLs that didn't need
|
|
176
|
+
// resolving against `baseUrl` in the first place.
|
|
177
|
+
if (!isAbsoluteUrl(requestOptions.url)) {
|
|
178
|
+
requestOptions.url = new URL(requestOptions.url, options.baseUrl).href;
|
|
179
|
+
}
|
|
165
180
|
requestOptions.userData ??= options.userData ?? {};
|
|
166
181
|
if (typeof options.label === 'string') {
|
|
167
182
|
requestOptions.userData = {
|
package/events/event_manager.js
CHANGED
|
@@ -18,7 +18,8 @@ export class EventManager {
|
|
|
18
18
|
#persistStateIntervalMillis;
|
|
19
19
|
constructor(options) {
|
|
20
20
|
this.#persistStateIntervalMillis = options.persistStateIntervalMillis;
|
|
21
|
-
|
|
21
|
+
// One MIGRATING listener per RequestQueue, and ThrottlingRequestManager opens one per domain.
|
|
22
|
+
this.events.setMaxListeners(150);
|
|
22
23
|
}
|
|
23
24
|
/**
|
|
24
25
|
* Initializes the event manager by starting the `persistState` event interval.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crawlee/core",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.124",
|
|
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"
|
|
@@ -52,10 +52,10 @@
|
|
|
52
52
|
"@apify/log": "^2.5.18",
|
|
53
53
|
"@apify/timeout": "^0.4.4",
|
|
54
54
|
"@apify/utilities": "^2.15.5",
|
|
55
|
-
"@crawlee/fs-storage": "4.0.0-beta.
|
|
56
|
-
"@crawlee/http-client": "4.0.0-beta.
|
|
57
|
-
"@crawlee/types": "4.0.0-beta.
|
|
58
|
-
"@crawlee/utils": "4.0.0-beta.
|
|
55
|
+
"@crawlee/fs-storage": "4.0.0-beta.124",
|
|
56
|
+
"@crawlee/http-client": "4.0.0-beta.124",
|
|
57
|
+
"@crawlee/types": "4.0.0-beta.124",
|
|
58
|
+
"@crawlee/utils": "4.0.0-beta.124",
|
|
59
59
|
"@sapphire/async-queue": "^1.5.5",
|
|
60
60
|
"@vladfrangu/async_event_emitter": "^2.4.6",
|
|
61
61
|
"content-type": "^1.0.5",
|
|
@@ -77,5 +77,5 @@
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
},
|
|
80
|
-
"gitHead": "
|
|
80
|
+
"gitHead": "0694ee1b94c755b98141671baa93cc363f2bf8e3"
|
|
81
81
|
}
|
package/request.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { BinaryLike } from 'node:crypto';
|
|
2
2
|
import type { AllowedHttpMethods, Dictionary } from '@crawlee/types';
|
|
3
|
-
import type {
|
|
3
|
+
import type { EnqueueStrategyOption } from './enqueue_links/enqueue_links.js';
|
|
4
4
|
import type { SkippedRequestReason } from './enqueue_links/shared.js';
|
|
5
5
|
export declare enum RequestState {
|
|
6
6
|
UNPROCESSED = 0,
|
|
@@ -281,7 +281,7 @@ export interface RequestOptions<UserData extends Dictionary = Dictionary> {
|
|
|
281
281
|
/** @internal */
|
|
282
282
|
lockExpiresAt?: Date;
|
|
283
283
|
/** @internal */
|
|
284
|
-
enqueueStrategy?:
|
|
284
|
+
enqueueStrategy?: EnqueueStrategyOption;
|
|
285
285
|
}
|
|
286
286
|
export interface PushErrorMessageOptions {
|
|
287
287
|
/**
|
package/router.d.ts
CHANGED
|
@@ -65,7 +65,7 @@ export declare function validateUserData(label: string | symbol, schema: Standar
|
|
|
65
65
|
* `Record<string, ...>`), any string or symbol label is accepted, preserving the original behaviour.
|
|
66
66
|
*/
|
|
67
67
|
export type RouterLabel<Routes extends Record<keyof Routes, Dictionary>> = string extends keyof Routes ? string | symbol : (keyof Routes & string) | symbol;
|
|
68
|
-
export interface RouterHandler<Context extends
|
|
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
71
|
export type GetUserDataFromRequest<T> = T extends Request<infer Y> ? Y : never;
|
|
@@ -214,7 +214,7 @@ export type RouterRoutes<Context, Routes extends Record<keyof Routes, Dictionary
|
|
|
214
214
|
* });
|
|
215
215
|
* ```
|
|
216
216
|
*/
|
|
217
|
-
export declare class Router<Context extends
|
|
217
|
+
export declare class Router<Context extends RestrictedCrawlingContext, Routes extends Record<keyof Routes, Dictionary> = Record<string, GetUserDataFromRequest<Context['request']>>> {
|
|
218
218
|
#private;
|
|
219
219
|
/**
|
|
220
220
|
* use Router.create() instead!
|
|
@@ -299,8 +299,8 @@ export declare class Router<Context extends Omit<RestrictedCrawlingContext, 'enq
|
|
|
299
299
|
* await crawler.run();
|
|
300
300
|
* ```
|
|
301
301
|
*/
|
|
302
|
-
static create<Context extends
|
|
303
|
-
static create<Context extends
|
|
304
|
-
static create<Context extends
|
|
302
|
+
static create<Context extends RestrictedCrawlingContext = CrawlingContext, Routes extends Record<keyof Routes, Dictionary> = Record<string, GetUserDataFromRequest<Context['request']>>>(routes?: RouterRoutes<Context, Routes>): RouterHandler<Context, Routes>;
|
|
303
|
+
static create<Context extends RestrictedCrawlingContext = CrawlingContext, UserData extends Dictionary = GetUserDataFromRequest<Context['request']>>(routes?: RouterRoutes<Context, Record<string, UserData>>): RouterHandler<Context, Record<string, UserData>>;
|
|
304
|
+
static create<Context extends RestrictedCrawlingContext = CrawlingContext, const Schemas extends RouteSchemas = RouteSchemas>(schemas: Schemas): RouterHandler<Context, RoutesFromSchemas<Schemas>>;
|
|
305
305
|
}
|
|
306
306
|
export {};
|
|
@@ -37,15 +37,58 @@ export interface ThrottlingRequestManagerOptions<T extends IRequestManager = IRe
|
|
|
37
37
|
*/
|
|
38
38
|
inner: T;
|
|
39
39
|
/**
|
|
40
|
-
*
|
|
41
|
-
* supported, so list each subdomain you care about. Requests for any other domain bypass throttling entirely.
|
|
40
|
+
* Which domains to throttle: a list of hostnames, or `'all'` for every domain the crawl encounters.
|
|
42
41
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
42
|
+
* Matching a listed hostname is case-insensitive and exact - wildcards such as `*.example.com` are not
|
|
43
|
+
* supported, so list each subdomain you care about (or set
|
|
44
|
+
* {@link ThrottlingRequestManagerOptions.throttleBy|`throttleBy: 'registrableDomain'`}). An
|
|
45
|
+
* internationalized domain may be given in either its unicode or its punycode form, and an IPv6 address has
|
|
46
|
+
* to be bracketed (`[::1]`). Requests for any other domain bypass throttling entirely.
|
|
47
|
+
*
|
|
48
|
+
* `'all'` gives each domain a queue of its own the first time it is seen, so that it can be held back
|
|
49
|
+
* without its requests being repeatedly popped and re-enqueued. One request queue per domain is not free,
|
|
50
|
+
* which is what {@link ThrottlingRequestManagerOptions.maxThrottledDomains|`maxThrottledDomains`} is
|
|
51
|
+
* there to bound.
|
|
52
|
+
*/
|
|
53
|
+
domains: string[] | 'all';
|
|
54
|
+
/**
|
|
55
|
+
* A floor under the crawl delay of every throttled domain, in seconds - the proactive clock described on
|
|
56
|
+
* {@link ThrottlingRequestManager}. A domain whose robots.txt asks for a longer `Crawl-delay` gets the
|
|
57
|
+
* longer one; this is a minimum, not an override.
|
|
58
|
+
* @default 0
|
|
59
|
+
*/
|
|
60
|
+
minCrawlDelaySecs?: number;
|
|
61
|
+
/**
|
|
62
|
+
* What counts as "the same domain": the exact hostname, or the registrable domain it belongs to
|
|
63
|
+
* (`example.com` for `www.example.com`, `a.example.co.uk` and so on). Hosts with no registrable domain -
|
|
64
|
+
* IP addresses, `localhost` - are always throttled per hostname.
|
|
65
|
+
*
|
|
66
|
+
* Grouping by registrable domain gives subdomains a single pair of clocks and a single queue, which is what
|
|
67
|
+
* you want when the pacing is there to be polite to one server rather than to satisfy a specific host's
|
|
68
|
+
* rate limit.
|
|
69
|
+
* @default 'hostname'
|
|
70
|
+
*/
|
|
71
|
+
throttleBy?: 'hostname' | 'registrableDomain';
|
|
72
|
+
/**
|
|
73
|
+
* The most domains a run may throttle at once. Exceeding it throws, rather than silently letting the
|
|
74
|
+
* throttling lapse - one request queue per domain is not free, and a crawl that discovers domains without
|
|
75
|
+
* bound would drown the storage backend in them.
|
|
76
|
+
*
|
|
77
|
+
* Only domains discovered under `domains: 'all'` count against this; an explicit list is taken at face value.
|
|
78
|
+
* @default 100
|
|
45
79
|
*/
|
|
46
|
-
|
|
80
|
+
maxThrottledDomains?: number;
|
|
47
81
|
/**
|
|
48
|
-
*
|
|
82
|
+
* The key under which the discovered domain list is kept in the default key-value store, so that a restart
|
|
83
|
+
* with `purgeOnStart` disabled reopens their queues instead of stranding whatever they still hold. Only
|
|
84
|
+
* written under `domains: 'all'`.
|
|
85
|
+
*
|
|
86
|
+
* Give each manager its own key when running several of them against the same storage.
|
|
87
|
+
* @default 'CRAWLEE_THROTTLED_DOMAINS'
|
|
88
|
+
*/
|
|
89
|
+
persistStateKey?: string;
|
|
90
|
+
/**
|
|
91
|
+
* Opens the per-domain queues, one per throttled domain, each under the alias `throttled-<domain>`.
|
|
49
92
|
* @default RequestQueue.open
|
|
50
93
|
*/
|
|
51
94
|
requestManagerOpener?: RequestManagerOpener<T>;
|
|
@@ -77,22 +120,26 @@ export interface ThrottlingRequestManagerOptions<T extends IRequestManager = IRe
|
|
|
77
120
|
/**
|
|
78
121
|
* A request manager that wraps another one and paces requests per domain.
|
|
79
122
|
*
|
|
80
|
-
* Requests for
|
|
81
|
-
*
|
|
82
|
-
* else goes to the wrapped manager untouched.
|
|
123
|
+
* Requests for a throttled domain are routed into their own queue when they are added, so each request lives in
|
|
124
|
+
* exactly one place and deduplication keeps working. Everything else goes to the wrapped manager untouched.
|
|
83
125
|
*
|
|
84
126
|
* {@link ThrottlingRequestManager.fetchNextRequest|`fetchNextRequest()`} serves the domain that has been waiting
|
|
85
127
|
* longest and skips any that are backing off, falling back to the wrapped manager. It never blocks: while every
|
|
86
128
|
* remaining request belongs to a throttled domain it returns `null` and {@link ThrottlingRequestManager.isEmpty}
|
|
87
129
|
* reports `true`, so the crawler idles instead of holding a concurrency slot open.
|
|
88
130
|
*
|
|
89
|
-
*
|
|
90
|
-
* - HTTP 429 responses
|
|
91
|
-
*
|
|
131
|
+
* Each throttled domain runs two independent clocks, and may be dispatched to once **both** have run out:
|
|
132
|
+
* - **Backoff**, set by HTTP 429 responses - honouring `Retry-After`, and otherwise doubling from `baseDelaySecs`.
|
|
133
|
+
* Reactive and temporary: it decays once the domain stops turning us away. The crawlers report the 429s
|
|
134
|
+
* themselves; a request held back this way is retried later without counting against `maxRequestRetries` and
|
|
92
135
|
* without penalising its session.
|
|
93
|
-
* -
|
|
136
|
+
* - **Crawl delay**, the minimum interval between two dispatches to the domain, armed after each one. Proactive
|
|
137
|
+
* and constant: whatever the domain's robots.txt asks for, floored by
|
|
138
|
+
* {@link ThrottlingRequestManagerOptions.minCrawlDelaySecs|`minCrawlDelaySecs`}. Either may be absent, in
|
|
139
|
+
* which case the other one is the delay.
|
|
94
140
|
*
|
|
95
|
-
*
|
|
141
|
+
* Which domains get those clocks is {@link ThrottlingRequestManagerOptions.domains|`domains`} - a list, or
|
|
142
|
+
* `'all'` for every domain the crawl encounters.
|
|
96
143
|
*
|
|
97
144
|
* **Example usage:**
|
|
98
145
|
*
|
|
@@ -109,40 +156,13 @@ export interface ThrottlingRequestManagerOptions<T extends IRequestManager = IRe
|
|
|
109
156
|
* @category Sources
|
|
110
157
|
*/
|
|
111
158
|
export declare class ThrottlingRequestManager<T extends IRequestManager = IRequestManager> implements IRequestManager, SupportsDomainThrottling {
|
|
159
|
+
#private;
|
|
112
160
|
private readonly config;
|
|
113
|
-
private readonly inner;
|
|
114
|
-
private readonly requestManagerOpener;
|
|
115
|
-
private readonly baseDelayMs;
|
|
116
|
-
private readonly maxDelayMs;
|
|
117
|
-
private readonly maxDomainStallMs;
|
|
118
161
|
private readonly domainStates;
|
|
119
|
-
private readonly subManagers;
|
|
120
162
|
private readonly log;
|
|
121
|
-
/**
|
|
122
|
-
* Sub-managers are keyed by a stable alias, so with `purgeOnStart` disabled they outlive the process. They
|
|
123
|
-
* must therefore be reopened for every configured domain rather than created on first insert - otherwise a
|
|
124
|
-
* restart sees an empty map, reports the crawl finished, and strands whatever the previous run left in them.
|
|
125
|
-
*/
|
|
126
|
-
private subManagersReady?;
|
|
127
|
-
/** Batches still being added in the background; keeps {@link ThrottlingRequestManager.isFinished} honest. */
|
|
128
|
-
private inProgressBatchCount;
|
|
129
|
-
private readonly warnedAbout;
|
|
130
|
-
private get hasThrottledDomains();
|
|
131
163
|
constructor(options: ThrottlingRequestManagerOptions<T>, config?: Configuration);
|
|
132
164
|
/** The wrapped manager, holding every request whose domain is not throttled. */
|
|
133
165
|
get innerManager(): T;
|
|
134
|
-
/** Warns once about sources that cannot be routed by domain, because their URLs are not known yet. */
|
|
135
|
-
private warnIfNotRoutable;
|
|
136
|
-
private warnOnce;
|
|
137
|
-
private extractDomain;
|
|
138
|
-
private getDomainState;
|
|
139
|
-
private selectManager;
|
|
140
|
-
/** Only valid once {@link ThrottlingRequestManager.ensureSubManagers} has resolved. */
|
|
141
|
-
private managerForUrl;
|
|
142
|
-
private ensureSubManagers;
|
|
143
|
-
private getSubManagers;
|
|
144
|
-
/** Configured domains that are not currently backing off, longest-overdue first. */
|
|
145
|
-
private fetchableDomains;
|
|
146
166
|
/**
|
|
147
167
|
* Records a 429 response and puts the URL's domain into backoff.
|
|
148
168
|
*
|
|
@@ -150,11 +170,12 @@ export declare class ThrottlingRequestManager<T extends IRequestManager = IReque
|
|
|
150
170
|
*/
|
|
151
171
|
recordDomainDelay(url: string, retryAfterMs?: number | null): boolean;
|
|
152
172
|
/**
|
|
153
|
-
*
|
|
173
|
+
* Records the `Crawl-delay` a domain's robots.txt asked for, which becomes its crawl delay unless
|
|
174
|
+
* {@link ThrottlingRequestManagerOptions.minCrawlDelaySecs|`minCrawlDelaySecs`} asks for longer.
|
|
154
175
|
*
|
|
155
176
|
* The first value wins, so a robots.txt re-fetch cannot change the cadence mid-crawl.
|
|
156
177
|
*
|
|
157
|
-
* @returns `false` if the domain is not
|
|
178
|
+
* @returns `false` if the domain is not throttled, in which case this is a no-op.
|
|
158
179
|
*/
|
|
159
180
|
setCrawlDelay(url: string, delaySeconds: number): boolean;
|
|
160
181
|
/**
|
|
@@ -167,8 +188,6 @@ export declare class ThrottlingRequestManager<T extends IRequestManager = IReque
|
|
|
167
188
|
* `Crawl-delay` is being obeyed, not stonewalled.
|
|
168
189
|
*/
|
|
169
190
|
assertNoStalledDomains(): Promise<void>;
|
|
170
|
-
/** Records that a domain let a request through, which ends any rate-limit run stall detection was timing. */
|
|
171
|
-
private recordProgress;
|
|
172
191
|
addRequest(requestLike: Source, options?: RequestQueueOperationOptions): Promise<RequestQueueOperationInfo>;
|
|
173
192
|
/**
|
|
174
193
|
* Adds requests in batches, routing each one to the manager that owns its domain.
|
|
@@ -197,10 +216,14 @@ export declare class ThrottlingRequestManager<T extends IRequestManager = IReque
|
|
|
197
216
|
* site rather than of the run, so it survives.
|
|
198
217
|
*/
|
|
199
218
|
purge(): Promise<void>;
|
|
219
|
+
/**
|
|
220
|
+
* Empties the per-domain queues, leaving the wrapped manager alone.
|
|
221
|
+
*
|
|
222
|
+
* Those queues are this manager's own no matter who owns the one it wraps, which is what makes this safe to
|
|
223
|
+
* call where a full {@link ThrottlingRequestManager.purge|`purge()`} would not be.
|
|
224
|
+
*/
|
|
225
|
+
purgeDomainQueues(): Promise<void>;
|
|
200
226
|
setExpectedRequestProcessingTimeSecs(secs: number): Promise<void>;
|
|
201
|
-
private forEachManager;
|
|
202
|
-
private sumOverManagers;
|
|
203
|
-
private everyManager;
|
|
204
227
|
/**
|
|
205
228
|
* Returns the next request from a domain that is not backing off, or from the inner manager.
|
|
206
229
|
*
|