@crawlee/core 4.0.0-rc.0 → 4.0.0-rc.1
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 +1 -1
- package/configuration.d.ts +15 -46
- package/configuration.js +8 -20
- package/errors.d.ts +12 -53
- package/errors.js +13 -66
- package/events/index.d.ts +1 -0
- package/events/local_event_manager.d.ts +0 -7
- package/events/local_event_manager.js +8 -8
- package/events/system_info.d.ts +38 -0
- package/index.d.ts +2 -8
- package/index.js +4 -8
- package/internal.d.ts +8 -0
- package/internal.js +9 -0
- package/log.d.ts +10 -11
- package/log.js +53 -25
- package/memory-storage/memory-storage.d.ts +12 -7
- package/memory-storage/memory-storage.js +45 -17
- package/memory-storage/resource-clients/dataset.d.ts +0 -5
- package/memory-storage/resource-clients/dataset.js +17 -20
- package/memory-storage/resource-clients/key-value-store.d.ts +0 -9
- package/memory-storage/resource-clients/key-value-store.js +11 -33
- package/memory-storage/resource-clients/request-queue.d.ts +0 -22
- package/memory-storage/resource-clients/request-queue.js +57 -53
- package/package.json +16 -18
- package/proxy_configuration.d.ts +20 -23
- package/proxy_configuration.js +18 -12
- package/recoverable_state.d.ts +28 -6
- package/recoverable_state.js +51 -14
- package/request.d.ts +18 -104
- package/request.js +41 -220
- package/serialization.js +3 -3
- package/service_locator.d.ts +3 -0
- package/service_locator.js +2 -0
- package/storages/dataset.d.ts +9 -15
- package/storages/dataset.js +22 -14
- package/storages/index.d.ts +3 -4
- package/storages/index.js +1 -4
- package/storages/key_value_store.d.ts +12 -46
- package/storages/key_value_store.js +31 -47
- package/storages/key_value_store_codec.js +6 -11
- package/storages/request_dedup_cache.d.ts +0 -2
- package/storages/request_dedup_cache.js +8 -8
- package/storages/request_list.d.ts +6 -82
- package/storages/request_list.js +175 -179
- package/storages/request_loader.d.ts +48 -22
- package/storages/request_loader.js +36 -1
- package/storages/request_manager.d.ts +86 -0
- package/storages/request_manager_tandem.d.ts +13 -28
- package/storages/request_manager_tandem.js +46 -43
- package/storages/request_queue.d.ts +19 -49
- package/storages/request_queue.js +92 -88
- package/storages/storage_instance_manager.d.ts +1 -2
- package/storages/storage_instance_manager.js +4 -4
- package/storages/transaction.d.ts +27 -9
- package/storages/transaction.js +56 -11
- package/storages/utils.d.ts +2 -2
- package/validators.d.ts +3 -2
- package/validators.js +3 -2
- package/autoscaling/autoscaled_pool.d.ts +0 -195
- package/autoscaling/autoscaled_pool.js +0 -386
- package/autoscaling/concurrency_system.d.ts +0 -268
- package/autoscaling/concurrency_system.js +0 -362
- package/autoscaling/cpu_load_signal.d.ts +0 -43
- package/autoscaling/cpu_load_signal.js +0 -47
- package/autoscaling/event_loop_load_signal.d.ts +0 -51
- package/autoscaling/event_loop_load_signal.js +0 -60
- package/autoscaling/index.d.ts +0 -9
- package/autoscaling/index.js +0 -9
- package/autoscaling/load_signal.d.ts +0 -100
- package/autoscaling/load_signal.js +0 -105
- package/autoscaling/memory_load_signal.d.ts +0 -47
- package/autoscaling/memory_load_signal.js +0 -106
- package/autoscaling/snapshotter.d.ts +0 -84
- package/autoscaling/snapshotter.js +0 -67
- package/autoscaling/storage_backend_load_signal.d.ts +0 -56
- package/autoscaling/storage_backend_load_signal.js +0 -73
- package/autoscaling/system_status.d.ts +0 -159
- package/autoscaling/system_status.js +0 -139
- package/autoscaling/weighted_avg.d.ts +0 -5
- package/autoscaling/weighted_avg.js +0 -14
- package/cookie_utils.d.ts +0 -44
- package/cookie_utils.js +0 -122
- package/crawlers/context_pipeline.d.ts +0 -70
- package/crawlers/context_pipeline.js +0 -122
- package/crawlers/crawler_commons.d.ts +0 -159
- package/crawlers/error_snapshotter.d.ts +0 -57
- package/crawlers/error_snapshotter.js +0 -117
- package/crawlers/error_tracker.d.ts +0 -54
- package/crawlers/error_tracker.js +0 -308
- package/crawlers/index.d.ts +0 -5
- package/crawlers/index.js +0 -4
- package/crawlers/internals/types.d.ts +0 -7
- package/crawlers/internals/types.js +0 -1
- package/crawlers/statistics.d.ts +0 -328
- package/crawlers/statistics.js +0 -536
- package/enqueue_links/enqueue_links.d.ts +0 -156
- package/enqueue_links/enqueue_links.js +0 -78
- package/enqueue_links/index.d.ts +0 -2
- package/enqueue_links/index.js +0 -2
- package/enqueue_links/shared.d.ts +0 -93
- package/enqueue_links/shared.js +0 -239
- package/http.d.ts +0 -9
- package/http.js +0 -28
- package/router.d.ts +0 -306
- package/router.js +0 -309
- package/session_pool/consts.d.ts +0 -3
- package/session_pool/consts.js +0 -3
- package/session_pool/errors.d.ts +0 -7
- package/session_pool/errors.js +0 -11
- package/session_pool/fingerprint.d.ts +0 -9
- package/session_pool/fingerprint.js +0 -30
- package/session_pool/index.d.ts +0 -4
- package/session_pool/index.js +0 -4
- package/session_pool/session.d.ts +0 -150
- package/session_pool/session.js +0 -220
- package/session_pool/session_pool.d.ts +0 -240
- package/session_pool/session_pool.js +0 -394
- package/storages/sitemap_request_loader.d.ts +0 -201
- package/storages/sitemap_request_loader.js +0 -438
- package/storages/throttling_request_manager.d.ts +0 -239
- package/storages/throttling_request_manager.js +0 -646
- /package/{crawlers/crawler_commons.js → events/system_info.js} +0 -0
package/recoverable_state.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
1
2
|
import { addTimeoutToPromise, storage as timeoutStorage } from '@apify/timeout';
|
|
2
|
-
import {
|
|
3
|
+
import { StateValidationError } from './errors.js';
|
|
4
|
+
import { EventType } from './events/event_manager.js';
|
|
5
|
+
import { serviceLocator } from './service_locator.js';
|
|
6
|
+
import { KeyValueStore } from './storages/key_value_store.js';
|
|
3
7
|
const DEFAULT_PERSISTENCE_TIMEOUT_MILLIS = 60_000;
|
|
4
8
|
/**
|
|
5
9
|
* Applies a {@link SyncStateConversion}, throwing a {@link StateValidationError} for a schema that rejects
|
|
@@ -34,6 +38,7 @@ export class RecoverableState {
|
|
|
34
38
|
#defaultState;
|
|
35
39
|
#state = null;
|
|
36
40
|
#initialized = false;
|
|
41
|
+
#recordLoaded = false;
|
|
37
42
|
#listening = false;
|
|
38
43
|
#persistenceEnabled;
|
|
39
44
|
#persistStateKey;
|
|
@@ -43,6 +48,7 @@ export class RecoverableState {
|
|
|
43
48
|
#log;
|
|
44
49
|
#serialize;
|
|
45
50
|
#deserialize;
|
|
51
|
+
#contentType;
|
|
46
52
|
#persistStateQuietly;
|
|
47
53
|
/**
|
|
48
54
|
* Initialize a new recoverable state object.
|
|
@@ -61,8 +67,18 @@ export class RecoverableState {
|
|
|
61
67
|
this.#configuration = options.configuration;
|
|
62
68
|
this.#keyValueStore = options.keyValueStore ?? null;
|
|
63
69
|
this.#log = options.logger ?? serviceLocator.getLogger().child({ prefix: 'RecoverableState' });
|
|
70
|
+
this.#contentType = options.contentType;
|
|
71
|
+
if (this.#contentType !== undefined && (options.serialize === undefined || options.deserialize === undefined)) {
|
|
72
|
+
throw new Error(`A 'contentType' for the state persisted under key '${this.#persistStateKey}' requires both 'serialize' and 'deserialize' - the record is no longer JSON the default codec can handle.`);
|
|
73
|
+
}
|
|
64
74
|
this.#serialize = this.#toConversion(options.serialize);
|
|
65
75
|
this.#deserialize = this.#toConversion(options.deserialize);
|
|
76
|
+
// A pending open() may reject before anything here awaits it. Observe the rejection so it surfaces on
|
|
77
|
+
// first use rather than as an unhandled rejection - most immediately when persistence is disabled and
|
|
78
|
+
// nothing ever awaits the store.
|
|
79
|
+
if (this.#keyValueStore !== null) {
|
|
80
|
+
Promise.resolve(this.#keyValueStore).then(undefined, () => { });
|
|
81
|
+
}
|
|
66
82
|
// The automatic persists, where a rejection has nowhere useful to go - the event manager does not catch
|
|
67
83
|
// listener errors, and throwing from teardown would bury the outcome of the work it cleans up after.
|
|
68
84
|
this.#persistStateQuietly = async (eventData) => this.persistState(eventData).catch((error) => this.#log.warning(`Failed to persist the state under key '${this.#persistStateKey}'.`, { error }));
|
|
@@ -91,7 +107,8 @@ export class RecoverableState {
|
|
|
91
107
|
* is no record to restore.
|
|
92
108
|
*
|
|
93
109
|
* Calling this again after a {@link RecoverableState.teardown} starts a new persistence window - the
|
|
94
|
-
* listener is registered again
|
|
110
|
+
* listener is registered again. The record is not reloaded: the in-memory state is what the teardown wrote, and
|
|
111
|
+
* reloading it would only drop whatever the deserialization leaves out.
|
|
95
112
|
*
|
|
96
113
|
* @returns The loaded state object
|
|
97
114
|
*/
|
|
@@ -100,10 +117,7 @@ export class RecoverableState {
|
|
|
100
117
|
return this.currentValue;
|
|
101
118
|
}
|
|
102
119
|
if (this.#persistenceEnabled) {
|
|
103
|
-
this.#
|
|
104
|
-
configuration: this.#configuration ?? serviceLocator.getConfiguration(),
|
|
105
|
-
});
|
|
106
|
-
await this.#resolveKeyValueStore();
|
|
120
|
+
await this.#openKeyValueStore();
|
|
107
121
|
serviceLocator.getEventManager().on(EventType.PERSIST_STATE, this.#persistStateQuietly);
|
|
108
122
|
this.#listening = true;
|
|
109
123
|
}
|
|
@@ -111,7 +125,10 @@ export class RecoverableState {
|
|
|
111
125
|
// fully wired object running on the default state rather than a half-initialized one.
|
|
112
126
|
this.#initialized = true;
|
|
113
127
|
this.#state ??= this.#defaultState();
|
|
114
|
-
|
|
128
|
+
if (!this.#recordLoaded) {
|
|
129
|
+
this.#recordLoaded = true;
|
|
130
|
+
await this.#loadSavedState();
|
|
131
|
+
}
|
|
115
132
|
return this.currentValue;
|
|
116
133
|
}
|
|
117
134
|
/**
|
|
@@ -161,7 +178,7 @@ export class RecoverableState {
|
|
|
161
178
|
* one would write the record straight back. Use {@link RecoverableState.reset} to reset the state itself,
|
|
162
179
|
* or {@link RecoverableState.teardown} before clearing the record.
|
|
163
180
|
*
|
|
164
|
-
* A no-op if persistence is disabled
|
|
181
|
+
* A no-op if persistence is disabled.
|
|
165
182
|
*/
|
|
166
183
|
async resetStore() {
|
|
167
184
|
if (this.#listening) {
|
|
@@ -170,10 +187,7 @@ export class RecoverableState {
|
|
|
170
187
|
if (!this.#persistenceEnabled) {
|
|
171
188
|
return;
|
|
172
189
|
}
|
|
173
|
-
const keyValueStore = await this.#
|
|
174
|
-
if (keyValueStore === null) {
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
190
|
+
const keyValueStore = await this.#openKeyValueStore();
|
|
177
191
|
await this.#withTimeout(async () => keyValueStore.setValue(this.#persistStateKey, null), 'Clearing the persisted state');
|
|
178
192
|
}
|
|
179
193
|
/**
|
|
@@ -195,7 +209,18 @@ export class RecoverableState {
|
|
|
195
209
|
}
|
|
196
210
|
this.#log.debug(`Persisting state of the RecoverableState (eventData=${JSON.stringify(eventData)}).`);
|
|
197
211
|
const serializedState = await this.#serialize(this.currentValue);
|
|
198
|
-
await this.#withTimeout(async () =>
|
|
212
|
+
await this.#withTimeout(async () => this.#contentType === undefined
|
|
213
|
+
? keyValueStore.setValue(this.#persistStateKey, serializedState)
|
|
214
|
+
: keyValueStore.setValue(this.#persistStateKey, serializedState, {
|
|
215
|
+
contentType: this.#contentType,
|
|
216
|
+
}), 'Persisting the state');
|
|
217
|
+
}
|
|
218
|
+
/** Falls back to the default store when none was supplied - persistence has to write somewhere. */
|
|
219
|
+
async #openKeyValueStore() {
|
|
220
|
+
this.#keyValueStore ??= KeyValueStore.open(null, {
|
|
221
|
+
configuration: this.#configuration ?? serviceLocator.getConfiguration(),
|
|
222
|
+
});
|
|
223
|
+
return (await this.#resolveKeyValueStore());
|
|
199
224
|
}
|
|
200
225
|
/** Awaits a store handed over as a pending `open()`, keeping the resolved instance for later calls. */
|
|
201
226
|
async #resolveKeyValueStore() {
|
|
@@ -216,7 +241,19 @@ export class RecoverableState {
|
|
|
216
241
|
if (keyValueStore === null) {
|
|
217
242
|
return;
|
|
218
243
|
}
|
|
219
|
-
|
|
244
|
+
// With a content type, the record is whatever `serialize` produced, so the codec's parse is skipped and
|
|
245
|
+
// `deserialize` gets the bytes as a stream - the shape a streaming parser wants.
|
|
246
|
+
// TODO: read the record as a stream instead of buffering it and wrapping (https://github.com/apify/crawlee/issues/2929).
|
|
247
|
+
const storedState = await this.#withTimeout(async () => {
|
|
248
|
+
if (this.#contentType === undefined) {
|
|
249
|
+
return keyValueStore.getValue(this.#persistStateKey);
|
|
250
|
+
}
|
|
251
|
+
const record = await keyValueStore.getRecord(this.#persistStateKey);
|
|
252
|
+
if (record === null) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
return Readable.from(Buffer.isBuffer(record.value) ? record.value : Buffer.from(record.value));
|
|
256
|
+
}, 'Loading the persisted state');
|
|
220
257
|
if (storedState === null || storedState === undefined) {
|
|
221
258
|
return;
|
|
222
259
|
}
|
package/request.d.ts
CHANGED
|
@@ -1,20 +1,12 @@
|
|
|
1
1
|
import type { BinaryLike } from 'node:crypto';
|
|
2
|
-
import type { AllowedHttpMethods, Dictionary } from '@crawlee/types';
|
|
3
|
-
import type {
|
|
4
|
-
|
|
5
|
-
export
|
|
6
|
-
UNPROCESSED = 0,
|
|
7
|
-
BEFORE_NAV = 1,
|
|
8
|
-
AFTER_NAV = 2,
|
|
9
|
-
REQUEST_HANDLER = 3,
|
|
10
|
-
DONE = 4,
|
|
11
|
-
ERROR_HANDLER = 5,
|
|
12
|
-
ERROR = 6,
|
|
13
|
-
SKIPPED = 7
|
|
14
|
-
}
|
|
2
|
+
import type { AllowedHttpMethods, Dictionary, RequestSchema } from '@crawlee/types';
|
|
3
|
+
import type { EnqueueStrategy } from '@crawlee/utils';
|
|
4
|
+
/** The `strategy` option accepted by {@link ExtractLinksOptions} and {@link EnqueueUrlsOptions}. */
|
|
5
|
+
export type EnqueueStrategyOption = EnqueueStrategy | 'all' | 'same-domain' | 'same-hostname' | 'same-origin';
|
|
15
6
|
/**
|
|
16
|
-
* Represents a URL to be crawled, optionally including HTTP method, headers, payload and other metadata
|
|
17
|
-
*
|
|
7
|
+
* Represents a URL to be crawled, optionally including HTTP method, headers, payload and other metadata, together
|
|
8
|
+
* with the processing state a request queue keeps about it (`retryCount`, `errorMessages`, `handledAt`, ...).
|
|
9
|
+
* Inside a crawler, requests are {@link CrawlingRequest}s, which add the crawler's own per-request settings.
|
|
18
10
|
*
|
|
19
11
|
* Each `Request` instance has the `uniqueKey` property, which can be either specified
|
|
20
12
|
* manually in the constructor or generated automatically from the URL. Two requests with the same `uniqueKey`
|
|
@@ -30,22 +22,16 @@ export declare enum RequestState {
|
|
|
30
22
|
* const request = new Request({
|
|
31
23
|
* url: 'http://www.example.com',
|
|
32
24
|
* headers: { Accept: 'application/json' },
|
|
25
|
+
* userData: { foo: 'bar' },
|
|
33
26
|
* });
|
|
34
27
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* request.userData.foo = 'bar';
|
|
38
|
-
* request.pushErrorMessage(new Error('Request failed!'));
|
|
39
|
-
*
|
|
40
|
-
* ...
|
|
41
|
-
*
|
|
42
|
-
* const foo = request.userData.foo;
|
|
28
|
+
* await requestQueue.addRequest(request);
|
|
43
29
|
* ```
|
|
44
30
|
* @category Sources
|
|
45
31
|
*/
|
|
46
32
|
declare class CrawleeRequest<UserData extends Dictionary = Dictionary> {
|
|
47
33
|
#private;
|
|
48
|
-
/**
|
|
34
|
+
/** Storage-assigned request ID. Only present on requests that went through a {@link RequestQueue}. */
|
|
49
35
|
id?: string;
|
|
50
36
|
/** URL of the web page to crawl. */
|
|
51
37
|
url: string;
|
|
@@ -89,78 +75,25 @@ declare class CrawleeRequest<UserData extends Dictionary = Dictionary> {
|
|
|
89
75
|
handledAt?: string;
|
|
90
76
|
/**
|
|
91
77
|
* `Request` parameters including the URL, HTTP method and headers, and others.
|
|
78
|
+
*
|
|
79
|
+
* Processing state (`retryCount`, `errorMessages`, `handledAt`, `loadedUrl`, `id`) is not an option;
|
|
80
|
+
* requests coming back from a storage are rebuilt with {@link Request.fromSchema}.
|
|
92
81
|
*/
|
|
93
82
|
constructor(options: RequestOptions<UserData>);
|
|
83
|
+
/**
|
|
84
|
+
* Rebuilds a request from its stored form, including the processing state a
|
|
85
|
+
* {@link RequestOptions} object cannot carry. Subclasses get instances of themselves.
|
|
86
|
+
*/
|
|
87
|
+
static fromSchema<UserData extends Dictionary = Dictionary>(schema: RequestSchema): CrawleeRequest<UserData>;
|
|
94
88
|
/**
|
|
95
89
|
* Converts the Crawlee Request object to a `fetch` API Request object.
|
|
96
90
|
* @returns The native `fetch` API Request object.
|
|
97
91
|
*/
|
|
98
92
|
intoFetchAPIRequest(): Request;
|
|
99
|
-
/**
|
|
100
|
-
* Tells the crawler processing this request to skip the navigation and process the request directly.
|
|
101
|
-
*
|
|
102
|
-
* When this is set to `true`, the crawling context will not contain the results of the navigation
|
|
103
|
-
* (e.g. `response`, `body`, `contentType`, `$` or `request.loadedUrl`).
|
|
104
|
-
* Accessing these properties will throw a {@link NavigationSkippedError} at runtime.
|
|
105
|
-
*/
|
|
106
|
-
get skipNavigation(): boolean;
|
|
107
|
-
/**
|
|
108
|
-
* Tells the crawler processing this request to skip the navigation and process the request directly.
|
|
109
|
-
*
|
|
110
|
-
* When this is set to `true`, the crawling context will not contain the results of the navigation
|
|
111
|
-
* (e.g. `response`, `body`, `contentType`, `$` or `request.loadedUrl`).
|
|
112
|
-
* Accessing these properties will throw a {@link NavigationSkippedError} at runtime.
|
|
113
|
-
*/
|
|
114
|
-
set skipNavigation(value: boolean);
|
|
115
|
-
/**
|
|
116
|
-
* Depth of the request in the current crawl tree.
|
|
117
|
-
* Note that this is dependent on the crawler setup and might produce unexpected results when used with multiple crawlers.
|
|
118
|
-
*/
|
|
119
|
-
get crawlDepth(): number;
|
|
120
|
-
/**
|
|
121
|
-
* Depth of the request in the current crawl tree.
|
|
122
|
-
* Note that this is dependent on the crawler setup and might produce unexpected results when used with multiple crawlers.
|
|
123
|
-
*/
|
|
124
|
-
set crawlDepth(value: number);
|
|
125
|
-
/** ID of a session to use for this request. When set, the crawler will fetch this session from the session pool instead of creating a new one. */
|
|
126
|
-
get sessionId(): string | undefined;
|
|
127
|
-
set sessionId(value: string | undefined);
|
|
128
93
|
/** shortcut for getting `request.userData.label` */
|
|
129
94
|
get label(): string | undefined;
|
|
130
95
|
/** shortcut for setting `request.userData.label` */
|
|
131
96
|
set label(value: string | undefined);
|
|
132
|
-
/** Maximum number of retries for this request. Allows to override the global `maxRequestRetries` option of `BasicCrawler`. */
|
|
133
|
-
get maxRetries(): number | undefined;
|
|
134
|
-
/** Maximum number of retries for this request. Allows to override the global `maxRequestRetries` option of `BasicCrawler`. */
|
|
135
|
-
set maxRetries(value: number | undefined);
|
|
136
|
-
/** Describes the request's current lifecycle state. */
|
|
137
|
-
get state(): RequestState;
|
|
138
|
-
/** Describes the request's current lifecycle state. */
|
|
139
|
-
set state(value: RequestState);
|
|
140
|
-
/**
|
|
141
|
-
* Reason for skipping this request.
|
|
142
|
-
*/
|
|
143
|
-
get skippedReason(): SkippedRequestReason | undefined;
|
|
144
|
-
/**
|
|
145
|
-
* Reason for skipping this request.
|
|
146
|
-
*/
|
|
147
|
-
set skippedReason(value: SkippedRequestReason | undefined);
|
|
148
|
-
private get enqueueStrategy();
|
|
149
|
-
private set enqueueStrategy(value);
|
|
150
|
-
/**
|
|
151
|
-
* Stores information about an error that occurred during processing of this request.
|
|
152
|
-
*
|
|
153
|
-
* You should always use Error instances when throwing errors in JavaScript.
|
|
154
|
-
*
|
|
155
|
-
* Nevertheless, to improve the debugging experience when using third party libraries
|
|
156
|
-
* that may not always throw an Error instance, the function performs a type
|
|
157
|
-
* inspection of the passed argument and attempts to extract as much information
|
|
158
|
-
* as possible, since just throwing a bad type error makes any debugging rather difficult.
|
|
159
|
-
*
|
|
160
|
-
* @param errorOrMessage Error object or error message to be stored in the request.
|
|
161
|
-
* @param [options]
|
|
162
|
-
*/
|
|
163
|
-
pushErrorMessage(errorOrMessage: unknown, options?: PushErrorMessageOptions): void;
|
|
164
97
|
/** @internal */
|
|
165
98
|
static computeUniqueKey({ url, method, payload, keepUrlFragment, useExtendedUniqueKey, alwaysEnqueue, }: ComputeUniqueKeyOptions): string;
|
|
166
99
|
/** @internal */
|
|
@@ -264,32 +197,13 @@ export interface RequestOptions<UserData extends Dictionary = Dictionary> {
|
|
|
264
197
|
* @default 0
|
|
265
198
|
*/
|
|
266
199
|
crawlDepth?: number;
|
|
267
|
-
/**
|
|
268
|
-
* Reason for skipping this request.
|
|
269
|
-
* This is used to provide more information about why the request was skipped.
|
|
270
|
-
* @internal
|
|
271
|
-
*/
|
|
272
|
-
skippedReason?: SkippedRequestReason;
|
|
273
200
|
/**
|
|
274
201
|
* Maximum number of retries for this request. Allows to override the global `maxRequestRetries` option of `BasicCrawler`.
|
|
275
202
|
*/
|
|
276
203
|
maxRetries?: number;
|
|
277
204
|
/** @internal */
|
|
278
|
-
id?: string;
|
|
279
|
-
/** @internal */
|
|
280
|
-
handledAt?: string;
|
|
281
|
-
/** @internal */
|
|
282
|
-
lockExpiresAt?: Date;
|
|
283
|
-
/** @internal */
|
|
284
205
|
enqueueStrategy?: EnqueueStrategyOption;
|
|
285
206
|
}
|
|
286
|
-
export interface PushErrorMessageOptions {
|
|
287
|
-
/**
|
|
288
|
-
* Only push the error message without stack trace when true.
|
|
289
|
-
* @default false
|
|
290
|
-
*/
|
|
291
|
-
omitStack?: boolean;
|
|
292
|
-
}
|
|
293
207
|
interface ComputeUniqueKeyOptions {
|
|
294
208
|
url: string;
|
|
295
209
|
method: AllowedHttpMethods;
|
package/request.js
CHANGED
|
@@ -1,41 +1,21 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
|
-
import util from 'node:util';
|
|
3
2
|
import { z } from 'zod';
|
|
4
3
|
import { cryptoRandomObjectId, normalizeUrl } from '@apify/utilities';
|
|
5
4
|
import { serviceLocator } from './service_locator.js';
|
|
6
|
-
import { keys } from './typedefs.js';
|
|
7
5
|
import { parseArgument, schemas } from './validators.js';
|
|
8
|
-
const dateString = z.string().refine((value) => !Number.isNaN(Date.parse(value)), {
|
|
9
|
-
message: 'Invalid input: expected a date string',
|
|
10
|
-
});
|
|
11
|
-
export var RequestState;
|
|
12
|
-
(function (RequestState) {
|
|
13
|
-
RequestState[RequestState["UNPROCESSED"] = 0] = "UNPROCESSED";
|
|
14
|
-
RequestState[RequestState["BEFORE_NAV"] = 1] = "BEFORE_NAV";
|
|
15
|
-
RequestState[RequestState["AFTER_NAV"] = 2] = "AFTER_NAV";
|
|
16
|
-
RequestState[RequestState["REQUEST_HANDLER"] = 3] = "REQUEST_HANDLER";
|
|
17
|
-
RequestState[RequestState["DONE"] = 4] = "DONE";
|
|
18
|
-
RequestState[RequestState["ERROR_HANDLER"] = 5] = "ERROR_HANDLER";
|
|
19
|
-
RequestState[RequestState["ERROR"] = 6] = "ERROR";
|
|
20
|
-
RequestState[RequestState["SKIPPED"] = 7] = "SKIPPED";
|
|
21
|
-
})(RequestState || (RequestState = {}));
|
|
22
|
-
const requestUrlSchema = z.object({ url: z.string() });
|
|
23
6
|
// new properties on the Request object breaks serialization
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
7
|
+
// Compiled once: every `Request` runs it, and the generated fast path skips zod's interpreter.
|
|
8
|
+
const requestOptionsSchema = z.compile(z.looseObject({
|
|
9
|
+
url: z.string(),
|
|
27
10
|
uniqueKey: z.string().optional(),
|
|
28
11
|
method: z.string().optional(),
|
|
29
12
|
payload: z.union([z.string(), z.instanceof(Uint8Array)]).optional(),
|
|
30
13
|
noRetry: z.boolean().optional(),
|
|
31
|
-
retryCount: schemas.anyNumber.optional(),
|
|
32
14
|
sessionId: z.string().optional(),
|
|
33
15
|
maxRetries: schemas.anyNumber.optional(),
|
|
34
|
-
errorMessages: schemas.arrayOf(z.string(), 'strings').optional(),
|
|
35
16
|
headers: z.looseObject({}).optional(),
|
|
36
17
|
userData: z.looseObject({}).optional(),
|
|
37
18
|
label: z.string().optional(),
|
|
38
|
-
handledAt: z.union([dateString, z.date()]).optional(),
|
|
39
19
|
keepUrlFragment: z.boolean().optional(),
|
|
40
20
|
useExtendedUniqueKey: z.boolean().optional(),
|
|
41
21
|
alwaysEnqueue: z.boolean().optional(),
|
|
@@ -43,13 +23,11 @@ const requestOptionalSchemaShapes = {
|
|
|
43
23
|
crawlDepth: schemas.anyNumber
|
|
44
24
|
.refine((value) => value >= 0, 'Expected a number greater than or equal to 0')
|
|
45
25
|
.optional(),
|
|
46
|
-
|
|
47
|
-
};
|
|
48
|
-
// Each schema is wrapped in a single-key object so validation errors carry the property name.
|
|
49
|
-
const requestOptionalSchemas = Object.fromEntries(Object.entries(requestOptionalSchemaShapes).map(([key, schema]) => [key, z.object({ [key]: schema })]));
|
|
26
|
+
}));
|
|
50
27
|
/**
|
|
51
|
-
* Represents a URL to be crawled, optionally including HTTP method, headers, payload and other metadata
|
|
52
|
-
*
|
|
28
|
+
* Represents a URL to be crawled, optionally including HTTP method, headers, payload and other metadata, together
|
|
29
|
+
* with the processing state a request queue keeps about it (`retryCount`, `errorMessages`, `handledAt`, ...).
|
|
30
|
+
* Inside a crawler, requests are {@link CrawlingRequest}s, which add the crawler's own per-request settings.
|
|
53
31
|
*
|
|
54
32
|
* Each `Request` instance has the `uniqueKey` property, which can be either specified
|
|
55
33
|
* manually in the constructor or generated automatically from the URL. Two requests with the same `uniqueKey`
|
|
@@ -65,21 +43,15 @@ const requestOptionalSchemas = Object.fromEntries(Object.entries(requestOptional
|
|
|
65
43
|
* const request = new Request({
|
|
66
44
|
* url: 'http://www.example.com',
|
|
67
45
|
* headers: { Accept: 'application/json' },
|
|
46
|
+
* userData: { foo: 'bar' },
|
|
68
47
|
* });
|
|
69
48
|
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* request.userData.foo = 'bar';
|
|
73
|
-
* request.pushErrorMessage(new Error('Request failed!'));
|
|
74
|
-
*
|
|
75
|
-
* ...
|
|
76
|
-
*
|
|
77
|
-
* const foo = request.userData.foo;
|
|
49
|
+
* await requestQueue.addRequest(request);
|
|
78
50
|
* ```
|
|
79
51
|
* @category Sources
|
|
80
52
|
*/
|
|
81
53
|
class CrawleeRequest {
|
|
82
|
-
/**
|
|
54
|
+
/** Storage-assigned request ID. Only present on requests that went through a {@link RequestQueue}. */
|
|
83
55
|
id;
|
|
84
56
|
/** URL of the web page to crawl. */
|
|
85
57
|
url;
|
|
@@ -125,6 +97,9 @@ class CrawleeRequest {
|
|
|
125
97
|
handledAt;
|
|
126
98
|
/**
|
|
127
99
|
* `Request` parameters including the URL, HTTP method and headers, and others.
|
|
100
|
+
*
|
|
101
|
+
* Processing state (`retryCount`, `errorMessages`, `handledAt`, `loadedUrl`, `id`) is not an option;
|
|
102
|
+
* requests coming back from a storage are rebuilt with {@link Request.fromSchema}.
|
|
128
103
|
*/
|
|
129
104
|
constructor(options) {
|
|
130
105
|
// A bare URL is a common slip — point at the object form instead of a generic type error.
|
|
@@ -133,23 +108,8 @@ class CrawleeRequest {
|
|
|
133
108
|
'Did you mean `new Request({ url })`?');
|
|
134
109
|
}
|
|
135
110
|
parseArgument(options, schemas.anyObject, 'RequestOptions');
|
|
136
|
-
parseArgument(options,
|
|
137
|
-
|
|
138
|
-
// even if the validated object has only 1 property.
|
|
139
|
-
// This custom validation loop iterates only over existing
|
|
140
|
-
// properties and speeds up the validation cca 3-fold.
|
|
141
|
-
keys(options).forEach((prop) => {
|
|
142
|
-
// skip url, because it is validated above
|
|
143
|
-
if (prop === 'url') {
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
const schema = requestOptionalSchemas[prop];
|
|
147
|
-
const value = options[prop];
|
|
148
|
-
if (schema) {
|
|
149
|
-
parseArgument({ [prop]: value }, schema, 'RequestOptions');
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
|
-
const { id, url, loadedUrl, uniqueKey, payload, noRetry = false, retryCount = 0, sessionId, maxRetries, errorMessages = [], headers = {}, userData = {}, label, handledAt, keepUrlFragment = false, useExtendedUniqueKey = false, alwaysEnqueue = false, skipNavigation, enqueueStrategy, crawlDepth, } = options;
|
|
111
|
+
parseArgument(options, requestOptionsSchema, 'RequestOptions');
|
|
112
|
+
const { url, uniqueKey, payload, noRetry = false, sessionId, maxRetries, headers = {}, userData = {}, label, keepUrlFragment = false, useExtendedUniqueKey = false, alwaysEnqueue = false, skipNavigation, enqueueStrategy, crawlDepth, } = options;
|
|
153
113
|
let { method = 'GET' } = options;
|
|
154
114
|
method = method.toUpperCase();
|
|
155
115
|
if (method === 'GET' && payload)
|
|
@@ -157,9 +117,7 @@ class CrawleeRequest {
|
|
|
157
117
|
if (uniqueKey && alwaysEnqueue) {
|
|
158
118
|
throw new Error('`alwaysEnqueue` cannot be used together with a custom `uniqueKey`.');
|
|
159
119
|
}
|
|
160
|
-
this.id = id;
|
|
161
120
|
this.url = url;
|
|
162
|
-
this.loadedUrl = loadedUrl;
|
|
163
121
|
this.uniqueKey =
|
|
164
122
|
uniqueKey ||
|
|
165
123
|
CrawleeRequest.computeUniqueKey({
|
|
@@ -173,10 +131,9 @@ class CrawleeRequest {
|
|
|
173
131
|
this.method = method;
|
|
174
132
|
this.payload = payload;
|
|
175
133
|
this.noRetry = noRetry;
|
|
176
|
-
this.retryCount =
|
|
177
|
-
this.errorMessages = [
|
|
134
|
+
this.retryCount = 0;
|
|
135
|
+
this.errorMessages = [];
|
|
178
136
|
this.headers = { ...headers };
|
|
179
|
-
this.handledAt = handledAt instanceof Date ? handledAt.toISOString() : handledAt;
|
|
180
137
|
if (label) {
|
|
181
138
|
userData.label = label;
|
|
182
139
|
}
|
|
@@ -216,18 +173,34 @@ class CrawleeRequest {
|
|
|
216
173
|
});
|
|
217
174
|
// reassign userData to ensure internal `__crawlee` object is non-enumerable
|
|
218
175
|
this.userData = userData;
|
|
176
|
+
const crawlee = (this.userData.__crawlee ??= {});
|
|
219
177
|
if (skipNavigation != null)
|
|
220
|
-
|
|
178
|
+
crawlee.skipNavigation = skipNavigation;
|
|
221
179
|
if (maxRetries != null)
|
|
222
|
-
|
|
180
|
+
crawlee.maxRetries = maxRetries;
|
|
223
181
|
if (crawlDepth != null)
|
|
224
|
-
|
|
182
|
+
crawlee.crawlDepth ??= crawlDepth;
|
|
225
183
|
if (sessionId)
|
|
226
|
-
|
|
227
|
-
//
|
|
228
|
-
if (enqueueStrategy)
|
|
229
|
-
|
|
230
|
-
|
|
184
|
+
crawlee.sessionId = sessionId;
|
|
185
|
+
// A stored request keeps the strategy it was enqueued under.
|
|
186
|
+
if (enqueueStrategy)
|
|
187
|
+
crawlee.enqueueStrategy ??= enqueueStrategy;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Rebuilds a request from its stored form, including the processing state a
|
|
191
|
+
* {@link RequestOptions} object cannot carry. Subclasses get instances of themselves.
|
|
192
|
+
*/
|
|
193
|
+
static fromSchema(schema) {
|
|
194
|
+
const { id, retryCount = 0, errorMessages = [], handledAt, loadedUrl, ...options } = schema;
|
|
195
|
+
const request = new this(options);
|
|
196
|
+
request.id = id;
|
|
197
|
+
request.retryCount = retryCount;
|
|
198
|
+
request.errorMessages = [...errorMessages];
|
|
199
|
+
request.loadedUrl = loadedUrl;
|
|
200
|
+
// `apify-client` parses `*At` fields into `Date`s, whatever the schema says.
|
|
201
|
+
request.handledAt =
|
|
202
|
+
handledAt instanceof Date ? handledAt.toISOString() : handledAt;
|
|
203
|
+
return request;
|
|
231
204
|
}
|
|
232
205
|
/**
|
|
233
206
|
* Converts the Crawlee Request object to a `fetch` API Request object.
|
|
@@ -240,54 +213,6 @@ class CrawleeRequest {
|
|
|
240
213
|
body: this.payload,
|
|
241
214
|
});
|
|
242
215
|
}
|
|
243
|
-
/**
|
|
244
|
-
* Tells the crawler processing this request to skip the navigation and process the request directly.
|
|
245
|
-
*
|
|
246
|
-
* When this is set to `true`, the crawling context will not contain the results of the navigation
|
|
247
|
-
* (e.g. `response`, `body`, `contentType`, `$` or `request.loadedUrl`).
|
|
248
|
-
* Accessing these properties will throw a {@link NavigationSkippedError} at runtime.
|
|
249
|
-
*/
|
|
250
|
-
get skipNavigation() {
|
|
251
|
-
return this.userData.__crawlee?.skipNavigation ?? false;
|
|
252
|
-
}
|
|
253
|
-
/**
|
|
254
|
-
* Tells the crawler processing this request to skip the navigation and process the request directly.
|
|
255
|
-
*
|
|
256
|
-
* When this is set to `true`, the crawling context will not contain the results of the navigation
|
|
257
|
-
* (e.g. `response`, `body`, `contentType`, `$` or `request.loadedUrl`).
|
|
258
|
-
* Accessing these properties will throw a {@link NavigationSkippedError} at runtime.
|
|
259
|
-
*/
|
|
260
|
-
set skipNavigation(value) {
|
|
261
|
-
if (!this.userData.__crawlee) {
|
|
262
|
-
this.userData.__crawlee = { skipNavigation: value };
|
|
263
|
-
}
|
|
264
|
-
else {
|
|
265
|
-
this.userData.__crawlee.skipNavigation = value;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
/**
|
|
269
|
-
* Depth of the request in the current crawl tree.
|
|
270
|
-
* Note that this is dependent on the crawler setup and might produce unexpected results when used with multiple crawlers.
|
|
271
|
-
*/
|
|
272
|
-
get crawlDepth() {
|
|
273
|
-
return this.userData.__crawlee?.crawlDepth ?? 0;
|
|
274
|
-
}
|
|
275
|
-
/**
|
|
276
|
-
* Depth of the request in the current crawl tree.
|
|
277
|
-
* Note that this is dependent on the crawler setup and might produce unexpected results when used with multiple crawlers.
|
|
278
|
-
*/
|
|
279
|
-
set crawlDepth(value) {
|
|
280
|
-
this.userData.__crawlee ??= {};
|
|
281
|
-
this.userData.__crawlee.crawlDepth = value;
|
|
282
|
-
}
|
|
283
|
-
/** ID of a session to use for this request. When set, the crawler will fetch this session from the session pool instead of creating a new one. */
|
|
284
|
-
get sessionId() {
|
|
285
|
-
return this.userData.__crawlee?.sessionId;
|
|
286
|
-
}
|
|
287
|
-
set sessionId(value) {
|
|
288
|
-
this.userData.__crawlee ??= {};
|
|
289
|
-
this.userData.__crawlee.sessionId = value;
|
|
290
|
-
}
|
|
291
216
|
/** shortcut for getting `request.userData.label` */
|
|
292
217
|
get label() {
|
|
293
218
|
return this.userData.label;
|
|
@@ -296,110 +221,6 @@ class CrawleeRequest {
|
|
|
296
221
|
set label(value) {
|
|
297
222
|
this.userData.label = value;
|
|
298
223
|
}
|
|
299
|
-
/** Maximum number of retries for this request. Allows to override the global `maxRequestRetries` option of `BasicCrawler`. */
|
|
300
|
-
get maxRetries() {
|
|
301
|
-
return this.userData.__crawlee?.maxRetries;
|
|
302
|
-
}
|
|
303
|
-
/** Maximum number of retries for this request. Allows to override the global `maxRequestRetries` option of `BasicCrawler`. */
|
|
304
|
-
set maxRetries(value) {
|
|
305
|
-
if (!this.userData.__crawlee) {
|
|
306
|
-
this.userData.__crawlee = { maxRetries: value };
|
|
307
|
-
}
|
|
308
|
-
else {
|
|
309
|
-
this.userData.__crawlee.maxRetries = value;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
/** Describes the request's current lifecycle state. */
|
|
313
|
-
get state() {
|
|
314
|
-
return this.userData.__crawlee?.state ?? RequestState.UNPROCESSED;
|
|
315
|
-
}
|
|
316
|
-
/** Describes the request's current lifecycle state. */
|
|
317
|
-
set state(value) {
|
|
318
|
-
if (!this.userData.__crawlee) {
|
|
319
|
-
this.userData.__crawlee = { state: value };
|
|
320
|
-
}
|
|
321
|
-
else {
|
|
322
|
-
this.userData.__crawlee.state = value;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
/**
|
|
326
|
-
* Reason for skipping this request.
|
|
327
|
-
*/
|
|
328
|
-
get skippedReason() {
|
|
329
|
-
return this.userData.__crawlee?.skippedReason;
|
|
330
|
-
}
|
|
331
|
-
/**
|
|
332
|
-
* Reason for skipping this request.
|
|
333
|
-
*/
|
|
334
|
-
set skippedReason(value) {
|
|
335
|
-
if (!this.userData.__crawlee) {
|
|
336
|
-
this.userData.__crawlee = { skippedReason: value };
|
|
337
|
-
}
|
|
338
|
-
else {
|
|
339
|
-
this.userData.__crawlee.skippedReason = value;
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
get enqueueStrategy() {
|
|
343
|
-
return this.userData.__crawlee?.enqueueStrategy;
|
|
344
|
-
}
|
|
345
|
-
set enqueueStrategy(value) {
|
|
346
|
-
if (!this.userData.__crawlee) {
|
|
347
|
-
this.userData.__crawlee = { enqueueStrategy: value };
|
|
348
|
-
}
|
|
349
|
-
else {
|
|
350
|
-
this.userData.__crawlee.enqueueStrategy = value;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
/**
|
|
354
|
-
* Stores information about an error that occurred during processing of this request.
|
|
355
|
-
*
|
|
356
|
-
* You should always use Error instances when throwing errors in JavaScript.
|
|
357
|
-
*
|
|
358
|
-
* Nevertheless, to improve the debugging experience when using third party libraries
|
|
359
|
-
* that may not always throw an Error instance, the function performs a type
|
|
360
|
-
* inspection of the passed argument and attempts to extract as much information
|
|
361
|
-
* as possible, since just throwing a bad type error makes any debugging rather difficult.
|
|
362
|
-
*
|
|
363
|
-
* @param errorOrMessage Error object or error message to be stored in the request.
|
|
364
|
-
* @param [options]
|
|
365
|
-
*/
|
|
366
|
-
pushErrorMessage(errorOrMessage, options = {}) {
|
|
367
|
-
const { omitStack } = options;
|
|
368
|
-
let message;
|
|
369
|
-
const type = typeof errorOrMessage;
|
|
370
|
-
if (type === 'object') {
|
|
371
|
-
if (!errorOrMessage) {
|
|
372
|
-
message = 'null';
|
|
373
|
-
}
|
|
374
|
-
else if (errorOrMessage instanceof Error) {
|
|
375
|
-
message = omitStack
|
|
376
|
-
? errorOrMessage.message
|
|
377
|
-
: // .stack includes the message
|
|
378
|
-
errorOrMessage.stack;
|
|
379
|
-
}
|
|
380
|
-
else if (Reflect.has(Object(errorOrMessage), 'message')) {
|
|
381
|
-
message = Reflect.get(Object(errorOrMessage), 'message');
|
|
382
|
-
}
|
|
383
|
-
else if (errorOrMessage.toString() !== '[object Object]') {
|
|
384
|
-
message = errorOrMessage.toString();
|
|
385
|
-
}
|
|
386
|
-
else {
|
|
387
|
-
try {
|
|
388
|
-
message = util.inspect(errorOrMessage);
|
|
389
|
-
}
|
|
390
|
-
catch (err) {
|
|
391
|
-
message = 'Unable to extract any message from the received object.';
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
else if (type === 'undefined') {
|
|
396
|
-
message = 'undefined';
|
|
397
|
-
}
|
|
398
|
-
else {
|
|
399
|
-
message = errorOrMessage.toString();
|
|
400
|
-
}
|
|
401
|
-
this.errorMessages.push(message);
|
|
402
|
-
}
|
|
403
224
|
/** @internal */
|
|
404
225
|
static computeUniqueKey({ url, method = 'GET', payload, keepUrlFragment = false, useExtendedUniqueKey = false, alwaysEnqueue = false, }) {
|
|
405
226
|
const normalizedMethod = method.toUpperCase();
|