@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.
Files changed (122) hide show
  1. package/README.md +1 -1
  2. package/configuration.d.ts +15 -46
  3. package/configuration.js +8 -20
  4. package/errors.d.ts +12 -53
  5. package/errors.js +13 -66
  6. package/events/index.d.ts +1 -0
  7. package/events/local_event_manager.d.ts +0 -7
  8. package/events/local_event_manager.js +8 -8
  9. package/events/system_info.d.ts +38 -0
  10. package/index.d.ts +2 -8
  11. package/index.js +4 -8
  12. package/internal.d.ts +8 -0
  13. package/internal.js +9 -0
  14. package/log.d.ts +10 -11
  15. package/log.js +53 -25
  16. package/memory-storage/memory-storage.d.ts +12 -7
  17. package/memory-storage/memory-storage.js +45 -17
  18. package/memory-storage/resource-clients/dataset.d.ts +0 -5
  19. package/memory-storage/resource-clients/dataset.js +17 -20
  20. package/memory-storage/resource-clients/key-value-store.d.ts +0 -9
  21. package/memory-storage/resource-clients/key-value-store.js +11 -33
  22. package/memory-storage/resource-clients/request-queue.d.ts +0 -22
  23. package/memory-storage/resource-clients/request-queue.js +57 -53
  24. package/package.json +16 -18
  25. package/proxy_configuration.d.ts +20 -23
  26. package/proxy_configuration.js +18 -12
  27. package/recoverable_state.d.ts +28 -6
  28. package/recoverable_state.js +51 -14
  29. package/request.d.ts +18 -104
  30. package/request.js +41 -220
  31. package/serialization.js +3 -3
  32. package/service_locator.d.ts +3 -0
  33. package/service_locator.js +2 -0
  34. package/storages/dataset.d.ts +9 -15
  35. package/storages/dataset.js +22 -14
  36. package/storages/index.d.ts +3 -4
  37. package/storages/index.js +1 -4
  38. package/storages/key_value_store.d.ts +12 -46
  39. package/storages/key_value_store.js +31 -47
  40. package/storages/key_value_store_codec.js +6 -11
  41. package/storages/request_dedup_cache.d.ts +0 -2
  42. package/storages/request_dedup_cache.js +8 -8
  43. package/storages/request_list.d.ts +6 -82
  44. package/storages/request_list.js +175 -179
  45. package/storages/request_loader.d.ts +48 -22
  46. package/storages/request_loader.js +36 -1
  47. package/storages/request_manager.d.ts +86 -0
  48. package/storages/request_manager_tandem.d.ts +13 -28
  49. package/storages/request_manager_tandem.js +46 -43
  50. package/storages/request_queue.d.ts +19 -49
  51. package/storages/request_queue.js +92 -88
  52. package/storages/storage_instance_manager.d.ts +1 -2
  53. package/storages/storage_instance_manager.js +4 -4
  54. package/storages/transaction.d.ts +27 -9
  55. package/storages/transaction.js +56 -11
  56. package/storages/utils.d.ts +2 -2
  57. package/validators.d.ts +3 -2
  58. package/validators.js +3 -2
  59. package/autoscaling/autoscaled_pool.d.ts +0 -195
  60. package/autoscaling/autoscaled_pool.js +0 -386
  61. package/autoscaling/concurrency_system.d.ts +0 -268
  62. package/autoscaling/concurrency_system.js +0 -362
  63. package/autoscaling/cpu_load_signal.d.ts +0 -43
  64. package/autoscaling/cpu_load_signal.js +0 -47
  65. package/autoscaling/event_loop_load_signal.d.ts +0 -51
  66. package/autoscaling/event_loop_load_signal.js +0 -60
  67. package/autoscaling/index.d.ts +0 -9
  68. package/autoscaling/index.js +0 -9
  69. package/autoscaling/load_signal.d.ts +0 -100
  70. package/autoscaling/load_signal.js +0 -105
  71. package/autoscaling/memory_load_signal.d.ts +0 -47
  72. package/autoscaling/memory_load_signal.js +0 -106
  73. package/autoscaling/snapshotter.d.ts +0 -84
  74. package/autoscaling/snapshotter.js +0 -67
  75. package/autoscaling/storage_backend_load_signal.d.ts +0 -56
  76. package/autoscaling/storage_backend_load_signal.js +0 -73
  77. package/autoscaling/system_status.d.ts +0 -159
  78. package/autoscaling/system_status.js +0 -139
  79. package/autoscaling/weighted_avg.d.ts +0 -5
  80. package/autoscaling/weighted_avg.js +0 -14
  81. package/cookie_utils.d.ts +0 -44
  82. package/cookie_utils.js +0 -122
  83. package/crawlers/context_pipeline.d.ts +0 -70
  84. package/crawlers/context_pipeline.js +0 -122
  85. package/crawlers/crawler_commons.d.ts +0 -159
  86. package/crawlers/error_snapshotter.d.ts +0 -57
  87. package/crawlers/error_snapshotter.js +0 -117
  88. package/crawlers/error_tracker.d.ts +0 -54
  89. package/crawlers/error_tracker.js +0 -308
  90. package/crawlers/index.d.ts +0 -5
  91. package/crawlers/index.js +0 -4
  92. package/crawlers/internals/types.d.ts +0 -7
  93. package/crawlers/internals/types.js +0 -1
  94. package/crawlers/statistics.d.ts +0 -328
  95. package/crawlers/statistics.js +0 -536
  96. package/enqueue_links/enqueue_links.d.ts +0 -156
  97. package/enqueue_links/enqueue_links.js +0 -78
  98. package/enqueue_links/index.d.ts +0 -2
  99. package/enqueue_links/index.js +0 -2
  100. package/enqueue_links/shared.d.ts +0 -93
  101. package/enqueue_links/shared.js +0 -239
  102. package/http.d.ts +0 -9
  103. package/http.js +0 -28
  104. package/router.d.ts +0 -306
  105. package/router.js +0 -309
  106. package/session_pool/consts.d.ts +0 -3
  107. package/session_pool/consts.js +0 -3
  108. package/session_pool/errors.d.ts +0 -7
  109. package/session_pool/errors.js +0 -11
  110. package/session_pool/fingerprint.d.ts +0 -9
  111. package/session_pool/fingerprint.js +0 -30
  112. package/session_pool/index.d.ts +0 -4
  113. package/session_pool/index.js +0 -4
  114. package/session_pool/session.d.ts +0 -150
  115. package/session_pool/session.js +0 -220
  116. package/session_pool/session_pool.d.ts +0 -240
  117. package/session_pool/session_pool.js +0 -394
  118. package/storages/sitemap_request_loader.d.ts +0 -201
  119. package/storages/sitemap_request_loader.js +0 -438
  120. package/storages/throttling_request_manager.d.ts +0 -239
  121. package/storages/throttling_request_manager.js +0 -646
  122. /package/{crawlers/crawler_commons.js → events/system_info.js} +0 -0
@@ -1,5 +1,9 @@
1
+ import { Readable } from 'node:stream';
1
2
  import { addTimeoutToPromise, storage as timeoutStorage } from '@apify/timeout';
2
- import { EventType, KeyValueStore, serviceLocator, StateValidationError } from '@crawlee/core';
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 and the record reloaded.
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.#keyValueStore ??= KeyValueStore.open(null, {
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
- await this.#loadSavedState();
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 or no KeyValueStore is available yet.
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.#resolveKeyValueStore();
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 () => keyValueStore.setValue(this.#persistStateKey, serializedState), 'Persisting the state');
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
- const storedState = await this.#withTimeout(async () => keyValueStore.getValue(this.#persistStateKey), 'Loading the persisted state');
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 { EnqueueStrategyOption } from './enqueue_links/enqueue_links.js';
4
- import type { SkippedRequestReason } from './enqueue_links/shared.js';
5
- export declare enum RequestState {
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
- * The `Request` object also stores information about errors that occurred during processing of the request.
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
- /** Request ID */
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
- const requestOptionalSchemaShapes = {
25
- id: z.string().optional(),
26
- loadedUrl: z.url().optional(),
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
- state: z.enum(RequestState).optional(),
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
- * The `Request` object also stores information about errors that occurred during processing of the request.
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
- /** Request ID */
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, requestUrlSchema, 'RequestOptions');
137
- // Full-shape validation is slow, because it checks all predicates
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 = retryCount;
177
- this.errorMessages = [...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
- this.skipNavigation = skipNavigation;
178
+ crawlee.skipNavigation = skipNavigation;
221
179
  if (maxRetries != null)
222
- this.maxRetries = maxRetries;
180
+ crawlee.maxRetries = maxRetries;
223
181
  if (crawlDepth != null)
224
- this.userData.__crawlee.crawlDepth ??= crawlDepth;
182
+ crawlee.crawlDepth ??= crawlDepth;
225
183
  if (sessionId)
226
- this.sessionId = sessionId;
227
- // If it's already set, don't override it (for instance when fetching from storage)
228
- if (enqueueStrategy) {
229
- this.enqueueStrategy ??= enqueueStrategy;
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();