@crawlee/core 3.17.1-beta.7 → 3.17.1-beta.70
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/autoscaled_pool.js +4 -1
- package/autoscaling/snapshotter.d.ts +2 -2
- package/cookie_utils.js +2 -0
- package/crawlers/statistics.d.ts +11 -2
- package/crawlers/statistics.js +7 -4
- package/enqueue_links/enqueue_links.d.ts +3 -53
- package/enqueue_links/enqueue_links.js +13 -63
- package/errors.d.ts +20 -0
- package/errors.js +32 -1
- package/http_clients/got-scraping-http-client.js +8 -0
- package/index.mjs +3 -0
- package/package.json +7 -6
- package/router.d.ts +133 -14
- package/router.js +136 -34
- package/session_pool/session_pool.d.ts +4 -1
- package/session_pool/session_pool.js +5 -2
- package/storages/request_dedup_cache.d.ts +23 -0
- package/storages/request_dedup_cache.js +66 -0
- package/storages/request_list.d.ts +6 -0
- package/storages/request_list.js +16 -2
- package/storages/request_provider.d.ts +7 -0
- package/storages/request_provider.js +39 -9
- package/storages/sitemap_request_list.d.ts +12 -0
- package/storages/sitemap_request_list.js +22 -3
package/router.d.ts
CHANGED
|
@@ -1,14 +1,71 @@
|
|
|
1
1
|
import type { Dictionary } from '@crawlee/types';
|
|
2
|
+
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
3
|
import type { CrawlingContext, LoadedRequest, RestrictedCrawlingContext } from './crawlers/crawler_commons';
|
|
3
4
|
import type { Request } from './request';
|
|
4
5
|
import type { Awaitable } from './typedefs';
|
|
5
|
-
|
|
6
|
+
/**
|
|
7
|
+
* The key of the default route — the fallback handler registered via {@link Router.addDefaultHandler}.
|
|
8
|
+
* Use it in a {@link RouteSchemas} map to register a schema that validates the `userData` of every request
|
|
9
|
+
* that falls through to the default handler (i.e. whose label has no route of its own).
|
|
10
|
+
*/
|
|
11
|
+
export declare const defaultRoute: unique symbol;
|
|
12
|
+
/**
|
|
13
|
+
* The crawling context received by a route handler, with `request.userData` narrowed to `UserData`.
|
|
14
|
+
*/
|
|
15
|
+
export type RouterHandlerContext<Context, UserData extends Dictionary> = Omit<Context, 'request'> & {
|
|
16
|
+
request: LoadedRequest<Request<UserData>>;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* A map of request labels to a [Standard Schema](https://standardschema.dev) (Zod, Valibot, ArkType, …)
|
|
20
|
+
* validating that label's `request.userData`. Pass it to {@link Router.create} or a `createXRouter`
|
|
21
|
+
* factory to derive the per-label `request.userData` types *and* validate them at runtime. The optional
|
|
22
|
+
* {@link defaultRoute} key registers a schema for requests handled by the default route.
|
|
23
|
+
*/
|
|
24
|
+
export type RouteSchemas = Record<string, StandardSchemaV1> & {
|
|
25
|
+
[defaultRoute]?: StandardSchemaV1;
|
|
26
|
+
};
|
|
27
|
+
/** Infers a label's `userData` type from its schema, falling back to a plain {@link Dictionary}. */
|
|
28
|
+
type SchemaUserData<Schema extends StandardSchemaV1> = StandardSchemaV1.InferOutput<Schema> extends Dictionary ? StandardSchemaV1.InferOutput<Schema> : Dictionary;
|
|
29
|
+
/**
|
|
30
|
+
* Derives a route map (label → `userData` type) from a {@link RouteSchemas} map by inferring each schema's
|
|
31
|
+
* output type. Outputs that are not object-shaped fall back to a plain {@link Dictionary}. The
|
|
32
|
+
* {@link defaultRoute} schema is kept under its symbol key so {@link Router.addDefaultHandler} can pick it
|
|
33
|
+
* up; string labels (the ones {@link Router.addHandler} accepts) ignore it.
|
|
34
|
+
*/
|
|
35
|
+
export type RoutesFromSchemas<Schemas extends RouteSchemas> = {
|
|
36
|
+
[Label in Extract<keyof Schemas, string>]: SchemaUserData<Schemas[Label]>;
|
|
37
|
+
} & (Schemas extends {
|
|
38
|
+
[defaultRoute]: StandardSchemaV1;
|
|
39
|
+
} ? {
|
|
40
|
+
[defaultRoute]: SchemaUserData<Schemas[typeof defaultRoute]>;
|
|
41
|
+
} : {});
|
|
42
|
+
/**
|
|
43
|
+
* The `userData` type of the default route: inferred from the {@link defaultRoute} schema when the route map
|
|
44
|
+
* carries one, otherwise the provided `Fallback`.
|
|
45
|
+
*/
|
|
46
|
+
export type DefaultRouteUserData<Routes, Fallback extends Dictionary> = Routes extends {
|
|
47
|
+
[defaultRoute]: infer DefaultUserData extends Dictionary;
|
|
48
|
+
} ? DefaultUserData : Fallback;
|
|
49
|
+
/**
|
|
50
|
+
* Validates `userData` against a {@link RouteSchemas|Standard Schema}, returning the parsed (and coerced)
|
|
51
|
+
* value. Throws a {@link RequestValidationError} when validation fails.
|
|
52
|
+
* @internal
|
|
53
|
+
*/
|
|
54
|
+
export declare function validateUserData(label: string | symbol, schema: StandardSchemaV1, userData: unknown): Promise<Dictionary>;
|
|
55
|
+
/**
|
|
56
|
+
* The set of labels accepted by {@link Router.addHandler}. When the router declares a concrete
|
|
57
|
+
* route map (e.g. `{ PRODUCT: ...; CATEGORY: ... }`), only those labels (plus symbols) are
|
|
58
|
+
* allowed — unknown labels become a compile-time error. When the map is left open (the default
|
|
59
|
+
* `Record<string, ...>`), any string or symbol label is accepted, preserving the original behaviour.
|
|
60
|
+
*/
|
|
61
|
+
export type RouterLabel<Routes extends Record<keyof Routes, Dictionary>> = string extends keyof Routes ? string | symbol : (keyof Routes & string) | symbol;
|
|
62
|
+
export interface RouterHandler<Context extends Omit<RestrictedCrawlingContext, 'enqueueLinks'> = CrawlingContext, Routes extends Record<keyof Routes, Dictionary> = Record<string, GetUserDataFromRequest<Context['request']>>> extends Router<Context, Routes> {
|
|
6
63
|
(ctx: Context): Awaitable<void>;
|
|
7
64
|
}
|
|
8
65
|
export type GetUserDataFromRequest<T> = T extends Request<infer Y> ? Y : never;
|
|
9
|
-
export type RouterRoutes<Context,
|
|
10
|
-
[
|
|
11
|
-
request: Request<
|
|
66
|
+
export type RouterRoutes<Context, Routes extends Record<keyof Routes, Dictionary>> = {
|
|
67
|
+
[Label in keyof Routes]: (ctx: Omit<Context, 'request'> & {
|
|
68
|
+
request: Request<Routes[Label]>;
|
|
12
69
|
}) => Awaitable<void>;
|
|
13
70
|
};
|
|
14
71
|
/**
|
|
@@ -75,9 +132,51 @@ export type RouterRoutes<Context, UserData extends Dictionary> = {
|
|
|
75
132
|
* ctx.log.info('...');
|
|
76
133
|
* });
|
|
77
134
|
* ```
|
|
135
|
+
*
|
|
136
|
+
* To get `request.userData` typed per label, declare a route map and pass it as the second
|
|
137
|
+
* type argument. The label passed to {@link Router.addHandler} then drives the type of
|
|
138
|
+
* `request.userData`, and unknown labels are rejected at compile time:
|
|
139
|
+
*
|
|
140
|
+
* ```ts
|
|
141
|
+
* import { createCheerioRouter, CheerioCrawlingContext } from 'crawlee';
|
|
142
|
+
*
|
|
143
|
+
* interface Routes {
|
|
144
|
+
* PRODUCT: { sku: string; price: number };
|
|
145
|
+
* CATEGORY: { categoryId: string };
|
|
146
|
+
* }
|
|
147
|
+
*
|
|
148
|
+
* const router = createCheerioRouter<CheerioCrawlingContext, Routes>();
|
|
149
|
+
*
|
|
150
|
+
* router.addHandler('PRODUCT', async ({ request }) => {
|
|
151
|
+
* request.userData.sku; // string
|
|
152
|
+
* request.userData.price; // number
|
|
153
|
+
* });
|
|
154
|
+
*
|
|
155
|
+
* router.addHandler('TYPO', async () => {}); // compile error: not a known label
|
|
156
|
+
* ```
|
|
157
|
+
*
|
|
158
|
+
* Passing a [Standard Schema](https://standardschema.dev) per label instead of a plain type both infers the
|
|
159
|
+
* `request.userData` types *and* validates them at runtime — when the request is handled, and when it is
|
|
160
|
+
* added to the crawler (`crawler.addRequests`, `context.addRequests`, `enqueueLinks`). A failing request
|
|
161
|
+
* throws a {@link RequestValidationError}.
|
|
162
|
+
*
|
|
163
|
+
* ```ts
|
|
164
|
+
* import { z } from 'zod';
|
|
165
|
+
* import { createCheerioRouter } from 'crawlee';
|
|
166
|
+
*
|
|
167
|
+
* const router = createCheerioRouter({
|
|
168
|
+
* PRODUCT: z.object({ sku: z.string(), price: z.number() }),
|
|
169
|
+
* CATEGORY: z.object({ categoryId: z.string() }),
|
|
170
|
+
* });
|
|
171
|
+
*
|
|
172
|
+
* router.addHandler('PRODUCT', async ({ request }) => {
|
|
173
|
+
* request.userData.price; // number, inferred from the schema and validated at runtime
|
|
174
|
+
* });
|
|
175
|
+
* ```
|
|
78
176
|
*/
|
|
79
|
-
export declare class Router<Context extends Omit<RestrictedCrawlingContext, 'enqueueLinks'
|
|
177
|
+
export declare class Router<Context extends Omit<RestrictedCrawlingContext, 'enqueueLinks'>, Routes extends Record<keyof Routes, Dictionary> = Record<string, GetUserDataFromRequest<Context['request']>>> {
|
|
80
178
|
private readonly routes;
|
|
179
|
+
private readonly schemas;
|
|
81
180
|
private readonly middlewares;
|
|
82
181
|
/**
|
|
83
182
|
* use Router.create() instead!
|
|
@@ -85,17 +184,29 @@ export declare class Router<Context extends Omit<RestrictedCrawlingContext, 'enq
|
|
|
85
184
|
*/
|
|
86
185
|
protected constructor();
|
|
87
186
|
/**
|
|
88
|
-
* Registers new route handler for given label.
|
|
187
|
+
* Registers new route handler for given label. When the router declares a route map, the
|
|
188
|
+
* `label` is restricted to the declared labels and `request.userData` is typed accordingly.
|
|
189
|
+
*/
|
|
190
|
+
addHandler<Label extends keyof Routes & string>(label: Label, handler: (ctx: RouterHandlerContext<Context, Routes[Label]>) => Awaitable<void>): void;
|
|
191
|
+
/**
|
|
192
|
+
* Registers new route handler for given label, explicitly typing `request.userData` via the
|
|
193
|
+
* `UserData` type argument. Useful when the router has no declared route map (the open default)
|
|
194
|
+
* and you want to type a single handler, or to register a handler under a `symbol` label.
|
|
89
195
|
*/
|
|
90
|
-
addHandler<UserData extends Dictionary = GetUserDataFromRequest<Context['request']>>(label:
|
|
91
|
-
request: LoadedRequest<Request<UserData>>;
|
|
92
|
-
}) => Awaitable<void>): void;
|
|
196
|
+
addHandler<UserData extends Dictionary = GetUserDataFromRequest<Context['request']>>(label: RouterLabel<Routes>, handler: (ctx: RouterHandlerContext<Context, UserData>) => Awaitable<void>): void;
|
|
93
197
|
/**
|
|
94
|
-
* Registers default route handler.
|
|
198
|
+
* Registers default route handler. As a fallback it can receive any request (including labels not
|
|
199
|
+
* declared in the route map). When the router was created with a {@link defaultRoute} schema,
|
|
200
|
+
* `request.userData` is typed from it; otherwise it defaults to the context's (loosely typed) `userData`.
|
|
201
|
+
* Pass an explicit `UserData` type argument to narrow it.
|
|
95
202
|
*/
|
|
96
|
-
addDefaultHandler<UserData extends Dictionary = GetUserDataFromRequest<Context['request']
|
|
97
|
-
|
|
98
|
-
|
|
203
|
+
addDefaultHandler<UserData extends Dictionary = DefaultRouteUserData<Routes, GetUserDataFromRequest<Context['request']>>>(handler: (ctx: RouterHandlerContext<Context, UserData>) => Awaitable<void>): void;
|
|
204
|
+
/**
|
|
205
|
+
* Returns the {@link RouteSchemas|Standard Schema} registered for a label, if any. Used by the crawler
|
|
206
|
+
* to validate `request.userData` when requests are added.
|
|
207
|
+
* @internal
|
|
208
|
+
*/
|
|
209
|
+
getSchema(label?: string | symbol): StandardSchemaV1 | undefined;
|
|
99
210
|
/**
|
|
100
211
|
* Registers a middleware that will be fired before the matching route handler.
|
|
101
212
|
* Multiple middlewares can be registered, they will be fired in the same order.
|
|
@@ -105,6 +216,11 @@ export declare class Router<Context extends Omit<RestrictedCrawlingContext, 'enq
|
|
|
105
216
|
* Returns route handler for given label. If no label is provided, the default request handler will be returned.
|
|
106
217
|
*/
|
|
107
218
|
getHandler(label?: string | symbol): (ctx: Context) => Awaitable<void>;
|
|
219
|
+
/**
|
|
220
|
+
* Validates `request.userData` against the schema registered for its label (if any), replacing it with
|
|
221
|
+
* the parsed value. Throws a {@link RequestValidationError} when validation fails.
|
|
222
|
+
*/
|
|
223
|
+
private validateRequest;
|
|
108
224
|
/**
|
|
109
225
|
* Throws when the label already exists in our registry.
|
|
110
226
|
*/
|
|
@@ -129,5 +245,8 @@ export declare class Router<Context extends Omit<RestrictedCrawlingContext, 'enq
|
|
|
129
245
|
* await crawler.run();
|
|
130
246
|
* ```
|
|
131
247
|
*/
|
|
132
|
-
static create<Context extends Omit<RestrictedCrawlingContext, 'enqueueLinks'> = CrawlingContext,
|
|
248
|
+
static create<Context extends Omit<RestrictedCrawlingContext, 'enqueueLinks'> = CrawlingContext, Routes extends Record<keyof Routes, Dictionary> = Record<string, GetUserDataFromRequest<Context['request']>>>(routes?: RouterRoutes<Context, Routes>): RouterHandler<Context, Routes>;
|
|
249
|
+
static create<Context extends Omit<RestrictedCrawlingContext, 'enqueueLinks'> = CrawlingContext, UserData extends Dictionary = GetUserDataFromRequest<Context['request']>>(routes?: RouterRoutes<Context, Record<string, UserData>>): RouterHandler<Context, Record<string, UserData>>;
|
|
250
|
+
static create<Context extends Omit<RestrictedCrawlingContext, 'enqueueLinks'> = CrawlingContext, const Schemas extends RouteSchemas = RouteSchemas>(schemas: Schemas): RouterHandler<Context, RoutesFromSchemas<Schemas>>;
|
|
133
251
|
}
|
|
252
|
+
export {};
|
package/router.js
CHANGED
|
@@ -1,8 +1,43 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Router = void 0;
|
|
3
|
+
exports.Router = exports.defaultRoute = void 0;
|
|
4
|
+
exports.validateUserData = validateUserData;
|
|
4
5
|
const errors_1 = require("./errors");
|
|
5
|
-
|
|
6
|
+
/**
|
|
7
|
+
* The key of the default route — the fallback handler registered via {@link Router.addDefaultHandler}.
|
|
8
|
+
* Use it in a {@link RouteSchemas} map to register a schema that validates the `userData` of every request
|
|
9
|
+
* that falls through to the default handler (i.e. whose label has no route of its own).
|
|
10
|
+
*/
|
|
11
|
+
exports.defaultRoute = Symbol('default-route');
|
|
12
|
+
/** Whether a validation issue points at the top-level `label` key. */
|
|
13
|
+
function isLabelIssue(issue) {
|
|
14
|
+
if (issue.path?.length !== 1) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
const [segment] = issue.path;
|
|
18
|
+
return (typeof segment === 'object' ? segment.key : segment) === 'label';
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Validates `userData` against a {@link RouteSchemas|Standard Schema}, returning the parsed (and coerced)
|
|
22
|
+
* value. Throws a {@link RequestValidationError} when validation fails.
|
|
23
|
+
* @internal
|
|
24
|
+
*/
|
|
25
|
+
async function validateUserData(label, schema, userData) {
|
|
26
|
+
const { label: _label, ...rest } = (userData ?? {});
|
|
27
|
+
// `label` is a Crawlee-managed key that lives inside `userData`, so validating it is opt-in: we validate
|
|
28
|
+
// without it first, letting schemas that don't describe it pass (including `.strict()` ones). A schema that
|
|
29
|
+
// *does* declare `label` reports an issue for the now-missing key — so we re-validate with it included,
|
|
30
|
+
// honouring the declaration. Unlike `userData.__crawlee`, `label` is enumerable, so schemas do see it.
|
|
31
|
+
let result = await schema['~standard'].validate(rest);
|
|
32
|
+
if (result.issues?.some(isLabelIssue)) {
|
|
33
|
+
result = await schema['~standard'].validate({ ...rest, label });
|
|
34
|
+
}
|
|
35
|
+
if (result.issues) {
|
|
36
|
+
throw new errors_1.RequestValidationError(label, result.issues);
|
|
37
|
+
}
|
|
38
|
+
// Restore the label so it survives schemas that strip undeclared keys.
|
|
39
|
+
return { ...result.value, label };
|
|
40
|
+
}
|
|
6
41
|
/**
|
|
7
42
|
* Simple router that works based on request labels. This instance can then serve as a `requestHandler` of your crawler.
|
|
8
43
|
*
|
|
@@ -67,6 +102,47 @@ const defaultRoute = Symbol('default-route');
|
|
|
67
102
|
* ctx.log.info('...');
|
|
68
103
|
* });
|
|
69
104
|
* ```
|
|
105
|
+
*
|
|
106
|
+
* To get `request.userData` typed per label, declare a route map and pass it as the second
|
|
107
|
+
* type argument. The label passed to {@link Router.addHandler} then drives the type of
|
|
108
|
+
* `request.userData`, and unknown labels are rejected at compile time:
|
|
109
|
+
*
|
|
110
|
+
* ```ts
|
|
111
|
+
* import { createCheerioRouter, CheerioCrawlingContext } from 'crawlee';
|
|
112
|
+
*
|
|
113
|
+
* interface Routes {
|
|
114
|
+
* PRODUCT: { sku: string; price: number };
|
|
115
|
+
* CATEGORY: { categoryId: string };
|
|
116
|
+
* }
|
|
117
|
+
*
|
|
118
|
+
* const router = createCheerioRouter<CheerioCrawlingContext, Routes>();
|
|
119
|
+
*
|
|
120
|
+
* router.addHandler('PRODUCT', async ({ request }) => {
|
|
121
|
+
* request.userData.sku; // string
|
|
122
|
+
* request.userData.price; // number
|
|
123
|
+
* });
|
|
124
|
+
*
|
|
125
|
+
* router.addHandler('TYPO', async () => {}); // compile error: not a known label
|
|
126
|
+
* ```
|
|
127
|
+
*
|
|
128
|
+
* Passing a [Standard Schema](https://standardschema.dev) per label instead of a plain type both infers the
|
|
129
|
+
* `request.userData` types *and* validates them at runtime — when the request is handled, and when it is
|
|
130
|
+
* added to the crawler (`crawler.addRequests`, `context.addRequests`, `enqueueLinks`). A failing request
|
|
131
|
+
* throws a {@link RequestValidationError}.
|
|
132
|
+
*
|
|
133
|
+
* ```ts
|
|
134
|
+
* import { z } from 'zod';
|
|
135
|
+
* import { createCheerioRouter } from 'crawlee';
|
|
136
|
+
*
|
|
137
|
+
* const router = createCheerioRouter({
|
|
138
|
+
* PRODUCT: z.object({ sku: z.string(), price: z.number() }),
|
|
139
|
+
* CATEGORY: z.object({ categoryId: z.string() }),
|
|
140
|
+
* });
|
|
141
|
+
*
|
|
142
|
+
* router.addHandler('PRODUCT', async ({ request }) => {
|
|
143
|
+
* request.userData.price; // number, inferred from the schema and validated at runtime
|
|
144
|
+
* });
|
|
145
|
+
* ```
|
|
70
146
|
*/
|
|
71
147
|
class Router {
|
|
72
148
|
/**
|
|
@@ -80,6 +156,12 @@ class Router {
|
|
|
80
156
|
writable: true,
|
|
81
157
|
value: new Map()
|
|
82
158
|
});
|
|
159
|
+
Object.defineProperty(this, "schemas", {
|
|
160
|
+
enumerable: true,
|
|
161
|
+
configurable: true,
|
|
162
|
+
writable: true,
|
|
163
|
+
value: new Map()
|
|
164
|
+
});
|
|
83
165
|
Object.defineProperty(this, "middlewares", {
|
|
84
166
|
enumerable: true,
|
|
85
167
|
configurable: true,
|
|
@@ -87,19 +169,39 @@ class Router {
|
|
|
87
169
|
value: []
|
|
88
170
|
});
|
|
89
171
|
}
|
|
90
|
-
/**
|
|
91
|
-
* Registers new route handler for given label.
|
|
92
|
-
*/
|
|
93
172
|
addHandler(label, handler) {
|
|
94
173
|
this.validate(label);
|
|
95
174
|
this.routes.set(label, handler);
|
|
96
175
|
}
|
|
97
176
|
/**
|
|
98
|
-
* Registers default route handler.
|
|
177
|
+
* Registers default route handler. As a fallback it can receive any request (including labels not
|
|
178
|
+
* declared in the route map). When the router was created with a {@link defaultRoute} schema,
|
|
179
|
+
* `request.userData` is typed from it; otherwise it defaults to the context's (loosely typed) `userData`.
|
|
180
|
+
* Pass an explicit `UserData` type argument to narrow it.
|
|
99
181
|
*/
|
|
100
182
|
addDefaultHandler(handler) {
|
|
101
|
-
this.validate(defaultRoute);
|
|
102
|
-
this.routes.set(defaultRoute, handler);
|
|
183
|
+
this.validate(exports.defaultRoute);
|
|
184
|
+
this.routes.set(exports.defaultRoute, handler);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Returns the {@link RouteSchemas|Standard Schema} registered for a label, if any. Used by the crawler
|
|
188
|
+
* to validate `request.userData` when requests are added.
|
|
189
|
+
* @internal
|
|
190
|
+
*/
|
|
191
|
+
getSchema(label) {
|
|
192
|
+
if (label != null) {
|
|
193
|
+
const schema = this.schemas.get(label);
|
|
194
|
+
if (schema) {
|
|
195
|
+
return schema;
|
|
196
|
+
}
|
|
197
|
+
// A label with its own route is fully specified; don't fall back to the default-route schema.
|
|
198
|
+
if (this.routes.has(label)) {
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// Requests with no route of their own fall through to the default handler, so validate their
|
|
203
|
+
// `userData` against the default-route schema, if one was registered.
|
|
204
|
+
return this.schemas.get(exports.defaultRoute);
|
|
103
205
|
}
|
|
104
206
|
/**
|
|
105
207
|
* Registers a middleware that will be fired before the matching route handler.
|
|
@@ -115,57 +217,57 @@ class Router {
|
|
|
115
217
|
if (label && this.routes.has(label)) {
|
|
116
218
|
return this.routes.get(label);
|
|
117
219
|
}
|
|
118
|
-
if (this.routes.has(defaultRoute)) {
|
|
119
|
-
return this.routes.get(defaultRoute);
|
|
220
|
+
if (this.routes.has(exports.defaultRoute)) {
|
|
221
|
+
return this.routes.get(exports.defaultRoute);
|
|
120
222
|
}
|
|
121
223
|
throw new errors_1.MissingRouteError(`Route not found for label '${String(label)}'.` +
|
|
122
224
|
' You must set up a route for this label or a default route.' +
|
|
123
225
|
' Use `requestHandler`, `router.addHandler` or `router.addDefaultHandler`.');
|
|
124
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* Validates `request.userData` against the schema registered for its label (if any), replacing it with
|
|
229
|
+
* the parsed value. Throws a {@link RequestValidationError} when validation fails.
|
|
230
|
+
*/
|
|
231
|
+
async validateRequest(context) {
|
|
232
|
+
const label = context.request.label;
|
|
233
|
+
const schema = this.getSchema(label);
|
|
234
|
+
if (schema) {
|
|
235
|
+
context.request.userData = (await validateUserData(label, schema, context.request.userData));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
125
238
|
/**
|
|
126
239
|
* Throws when the label already exists in our registry.
|
|
127
240
|
*/
|
|
128
241
|
validate(label) {
|
|
129
242
|
if (this.routes.has(label)) {
|
|
130
|
-
const message = label === defaultRoute
|
|
243
|
+
const message = label === exports.defaultRoute
|
|
131
244
|
? `Default route is already defined!`
|
|
132
245
|
: `Route for label '${String(label)}' is already defined!`;
|
|
133
246
|
throw new Error(message);
|
|
134
247
|
}
|
|
135
248
|
}
|
|
136
|
-
|
|
137
|
-
* Creates new router instance. This instance can then serve as a `requestHandler` of your crawler.
|
|
138
|
-
*
|
|
139
|
-
* ```ts
|
|
140
|
-
* import { Router, CheerioCrawler, CheerioCrawlingContext } from 'crawlee';
|
|
141
|
-
*
|
|
142
|
-
* const router = Router.create<CheerioCrawlingContext>();
|
|
143
|
-
* router.addHandler('label-a', async (ctx) => {
|
|
144
|
-
* ctx.log.info('...');
|
|
145
|
-
* });
|
|
146
|
-
* router.addDefaultHandler(async (ctx) => {
|
|
147
|
-
* ctx.log.info('...');
|
|
148
|
-
* });
|
|
149
|
-
*
|
|
150
|
-
* const crawler = new CheerioCrawler({
|
|
151
|
-
* requestHandler: router,
|
|
152
|
-
* });
|
|
153
|
-
* await crawler.run();
|
|
154
|
-
* ```
|
|
155
|
-
*/
|
|
156
|
-
static create(routes) {
|
|
249
|
+
static create(routesOrSchemas) {
|
|
157
250
|
const router = new Router();
|
|
158
251
|
const obj = Object.create(Function.prototype);
|
|
159
252
|
obj.addHandler = router.addHandler.bind(router);
|
|
160
253
|
obj.addDefaultHandler = router.addDefaultHandler.bind(router);
|
|
254
|
+
obj.getSchema = router.getSchema.bind(router);
|
|
161
255
|
obj.getHandler = router.getHandler.bind(router);
|
|
162
256
|
obj.use = router.use.bind(router);
|
|
163
|
-
|
|
164
|
-
|
|
257
|
+
// `Reflect.ownKeys` (unlike `Object.entries`) also yields the `defaultRoute` symbol key.
|
|
258
|
+
for (const label of Reflect.ownKeys(routesOrSchemas ?? {})) {
|
|
259
|
+
const value = routesOrSchemas[label];
|
|
260
|
+
if (typeof value === 'function') {
|
|
261
|
+
router.addHandler(label, value);
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
router.schemas.set(label, value);
|
|
265
|
+
}
|
|
165
266
|
}
|
|
166
267
|
const func = async function (context) {
|
|
167
268
|
const { url, loadedUrl, label } = context.request;
|
|
168
269
|
context.log.debug('Page opened.', { label, url: loadedUrl ?? url });
|
|
270
|
+
await router.validateRequest(context);
|
|
169
271
|
for (const middleware of router.middlewares) {
|
|
170
272
|
await middleware(context);
|
|
171
273
|
}
|
|
@@ -192,8 +192,11 @@ export declare class SessionPool extends EventEmitter {
|
|
|
192
192
|
/**
|
|
193
193
|
* Removes listener from `persistState` event.
|
|
194
194
|
* This function should be called after you are done with using the `SessionPool` instance.
|
|
195
|
+
* @param options - Set `persistState` to false when the final state was already persisted by the event manager.
|
|
195
196
|
*/
|
|
196
|
-
teardown(
|
|
197
|
+
teardown({ persistState }?: {
|
|
198
|
+
persistState?: boolean;
|
|
199
|
+
}): Promise<void>;
|
|
197
200
|
/**
|
|
198
201
|
* SessionPool should not work before initialization.
|
|
199
202
|
*/
|
|
@@ -342,10 +342,13 @@ class SessionPool extends node_events_1.EventEmitter {
|
|
|
342
342
|
/**
|
|
343
343
|
* Removes listener from `persistState` event.
|
|
344
344
|
* This function should be called after you are done with using the `SessionPool` instance.
|
|
345
|
+
* @param options - Set `persistState` to false when the final state was already persisted by the event manager.
|
|
345
346
|
*/
|
|
346
|
-
async teardown() {
|
|
347
|
+
async teardown({ persistState = true } = {}) {
|
|
347
348
|
this.events.off("persistState" /* EventType.PERSIST_STATE */, this._listener);
|
|
348
|
-
|
|
349
|
+
if (persistState) {
|
|
350
|
+
await this.persistState();
|
|
351
|
+
}
|
|
349
352
|
}
|
|
350
353
|
/**
|
|
351
354
|
* SessionPool should not work before initialization.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A fixed-size, direct-mapped cache for `uniqueKey`-based request deduplication.
|
|
3
|
+
*
|
|
4
|
+
* `RequestProvider.requestCache` only remembers the first batch of requests, so repeated
|
|
5
|
+
* `addRequestsBatched()` calls with overlapping URLs re-submit already-enqueued requests
|
|
6
|
+
* (https://github.com/apify/crawlee/issues/3120). This is a separate, cheaper cache we can populate on
|
|
7
|
+
* every batch: a fixed number of slots indexed by a hash of the request's cache key, storing the
|
|
8
|
+
* server-assigned `requestId`. Memory is capped by the slot count regardless of the working set size;
|
|
9
|
+
* a hash collision just overwrites a slot, causing an occasional cache miss (a harmless re-submission)
|
|
10
|
+
* but never a false hit — so a genuinely new request is never dropped.
|
|
11
|
+
*
|
|
12
|
+
* @internal
|
|
13
|
+
*/
|
|
14
|
+
export declare class RequestDeduplicationCache {
|
|
15
|
+
private readonly size;
|
|
16
|
+
private keys;
|
|
17
|
+
private ids;
|
|
18
|
+
constructor(size?: number);
|
|
19
|
+
get(cacheKey: string): string | null;
|
|
20
|
+
add(cacheKey: string, requestId: string): void;
|
|
21
|
+
clear(): void;
|
|
22
|
+
private indexOf;
|
|
23
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RequestDeduplicationCache = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* A fixed-size, direct-mapped cache for `uniqueKey`-based request deduplication.
|
|
6
|
+
*
|
|
7
|
+
* `RequestProvider.requestCache` only remembers the first batch of requests, so repeated
|
|
8
|
+
* `addRequestsBatched()` calls with overlapping URLs re-submit already-enqueued requests
|
|
9
|
+
* (https://github.com/apify/crawlee/issues/3120). This is a separate, cheaper cache we can populate on
|
|
10
|
+
* every batch: a fixed number of slots indexed by a hash of the request's cache key, storing the
|
|
11
|
+
* server-assigned `requestId`. Memory is capped by the slot count regardless of the working set size;
|
|
12
|
+
* a hash collision just overwrites a slot, causing an occasional cache miss (a harmless re-submission)
|
|
13
|
+
* but never a false hit — so a genuinely new request is never dropped.
|
|
14
|
+
*
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
class RequestDeduplicationCache {
|
|
18
|
+
// The slot count is the same for every queue, so it's a fixed default rather than a per-consumer option.
|
|
19
|
+
constructor(size = 1000000) {
|
|
20
|
+
Object.defineProperty(this, "size", {
|
|
21
|
+
enumerable: true,
|
|
22
|
+
configurable: true,
|
|
23
|
+
writable: true,
|
|
24
|
+
value: size
|
|
25
|
+
});
|
|
26
|
+
Object.defineProperty(this, "keys", {
|
|
27
|
+
enumerable: true,
|
|
28
|
+
configurable: true,
|
|
29
|
+
writable: true,
|
|
30
|
+
value: void 0
|
|
31
|
+
});
|
|
32
|
+
Object.defineProperty(this, "ids", {
|
|
33
|
+
enumerable: true,
|
|
34
|
+
configurable: true,
|
|
35
|
+
writable: true,
|
|
36
|
+
value: void 0
|
|
37
|
+
});
|
|
38
|
+
this.keys = new Array(size);
|
|
39
|
+
this.ids = new Array(size);
|
|
40
|
+
}
|
|
41
|
+
get(cacheKey) {
|
|
42
|
+
const index = this.indexOf(cacheKey);
|
|
43
|
+
return this.keys[index] === cacheKey ? this.ids[index] : null;
|
|
44
|
+
}
|
|
45
|
+
add(cacheKey, requestId) {
|
|
46
|
+
const index = this.indexOf(cacheKey);
|
|
47
|
+
this.keys[index] = cacheKey;
|
|
48
|
+
this.ids[index] = requestId;
|
|
49
|
+
}
|
|
50
|
+
clear() {
|
|
51
|
+
this.keys = new Array(this.size);
|
|
52
|
+
this.ids = new Array(this.size);
|
|
53
|
+
}
|
|
54
|
+
// A cheap FNV-1a hash of the cache key — avoids pulling in a dedicated hashing dependency.
|
|
55
|
+
indexOf(cacheKey) {
|
|
56
|
+
/* eslint-disable no-bitwise */
|
|
57
|
+
let hash = 0x811c9dc5;
|
|
58
|
+
for (let i = 0; i < cacheKey.length; i++) {
|
|
59
|
+
hash ^= cacheKey.charCodeAt(i);
|
|
60
|
+
hash = Math.imul(hash, 0x01000193);
|
|
61
|
+
}
|
|
62
|
+
return (hash >>> 0) % this.size;
|
|
63
|
+
/* eslint-enable no-bitwise */
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
exports.RequestDeduplicationCache = RequestDeduplicationCache;
|
|
@@ -336,6 +336,12 @@ export declare class RequestList implements IRequestList {
|
|
|
336
336
|
* @inheritDoc
|
|
337
337
|
*/
|
|
338
338
|
persistState(): Promise<void>;
|
|
339
|
+
/**
|
|
340
|
+
* Removes the `PERSIST_STATE` event listener registered during initialization and persists
|
|
341
|
+
* the current state one last time. Call this when you are done with the `RequestList` to avoid
|
|
342
|
+
* leaking the listener (and the requests it retains) on the shared event manager.
|
|
343
|
+
*/
|
|
344
|
+
teardown(): Promise<void>;
|
|
339
345
|
/**
|
|
340
346
|
* Unlike persistState(), this is used only internally, since the sources
|
|
341
347
|
* are automatically persisted at RequestList initialization (if the persistRequestsKey is set),
|
package/storages/request_list.js
CHANGED
|
@@ -248,6 +248,7 @@ class RequestList {
|
|
|
248
248
|
this.sourcesFunction = sourcesFunction;
|
|
249
249
|
// The proxy configuration used for `requestsFromUrl` requests.
|
|
250
250
|
this.proxyConfiguration = proxyConfiguration;
|
|
251
|
+
this.persistState = this.persistState.bind(this);
|
|
251
252
|
}
|
|
252
253
|
/**
|
|
253
254
|
* Loads all remote sources of URLs and potentially starts periodic state persistence.
|
|
@@ -273,7 +274,7 @@ class RequestList {
|
|
|
273
274
|
if (this.persistRequestsKey && !this.areRequestsPersisted)
|
|
274
275
|
await this._persistRequests();
|
|
275
276
|
if (this.persistStateKey) {
|
|
276
|
-
this.events.on("persistState" /* EventType.PERSIST_STATE */, this.persistState
|
|
277
|
+
this.events.on("persistState" /* EventType.PERSIST_STATE */, this.persistState);
|
|
277
278
|
}
|
|
278
279
|
return this;
|
|
279
280
|
}
|
|
@@ -324,9 +325,11 @@ class RequestList {
|
|
|
324
325
|
const sourcesFromFunction = await this.sourcesFunction();
|
|
325
326
|
const sourcesFromFunctionCount = sourcesFromFunction.length;
|
|
326
327
|
for (let i = 0; i < sourcesFromFunctionCount; i++) {
|
|
327
|
-
const source = sourcesFromFunction
|
|
328
|
+
const source = sourcesFromFunction[i];
|
|
329
|
+
delete sourcesFromFunction[i];
|
|
328
330
|
this._addRequest(source);
|
|
329
331
|
}
|
|
332
|
+
sourcesFromFunction.length = 0;
|
|
330
333
|
}
|
|
331
334
|
catch (e) {
|
|
332
335
|
const err = e;
|
|
@@ -353,6 +356,17 @@ class RequestList {
|
|
|
353
356
|
this.log.exception(err, 'Attempted to persist state, but failed.');
|
|
354
357
|
}
|
|
355
358
|
}
|
|
359
|
+
/**
|
|
360
|
+
* Removes the `PERSIST_STATE` event listener registered during initialization and persists
|
|
361
|
+
* the current state one last time. Call this when you are done with the `RequestList` to avoid
|
|
362
|
+
* leaking the listener (and the requests it retains) on the shared event manager.
|
|
363
|
+
*/
|
|
364
|
+
async teardown() {
|
|
365
|
+
this.events.off("persistState" /* EventType.PERSIST_STATE */, this.persistState);
|
|
366
|
+
if (this.persistStateKey) {
|
|
367
|
+
await this.persistState();
|
|
368
|
+
}
|
|
369
|
+
}
|
|
356
370
|
/**
|
|
357
371
|
* Unlike persistState(), this is used only internally, since the sources
|
|
358
372
|
* are automatically persisted at RequestList initialization (if the persistRequestsKey is set),
|
|
@@ -5,6 +5,7 @@ import { Configuration } from '../configuration';
|
|
|
5
5
|
import type { ProxyConfiguration } from '../proxy_configuration';
|
|
6
6
|
import type { InternalSource, RequestOptions, Source } from '../request';
|
|
7
7
|
import { Request } from '../request';
|
|
8
|
+
import { RequestDeduplicationCache } from './request_dedup_cache';
|
|
8
9
|
import type { IStorage, StorageManagerOptions } from './storage_manager';
|
|
9
10
|
export type RequestsLike = AsyncIterable<Source | string> | Iterable<Source | string> | (Source | string)[];
|
|
10
11
|
/**
|
|
@@ -74,6 +75,12 @@ export declare abstract class RequestProvider implements IStorage, IRequestManag
|
|
|
74
75
|
private initialHandledCount;
|
|
75
76
|
protected queueHeadIds: ListDictionary<string>;
|
|
76
77
|
protected requestCache: LruCache<RequestLruItem>;
|
|
78
|
+
/**
|
|
79
|
+
* Remembers the `requestId` of every request already submitted to the client — including background
|
|
80
|
+
* batches that `requestCache` skips — so overlapping URL sets aren't re-submitted.
|
|
81
|
+
* See {@link RequestDeduplicationCache} for why this is a separate, cheaper cache.
|
|
82
|
+
*/
|
|
83
|
+
protected requestSeenCache: RequestDeduplicationCache;
|
|
77
84
|
protected recentlyHandledRequestsCache: LruCache<boolean>;
|
|
78
85
|
protected queuePausedForMigration: boolean;
|
|
79
86
|
protected lastActivity: Date;
|