@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
@@ -19,11 +19,11 @@ export class RequestQueueBackend extends BaseClient {
19
19
  pendingRequestCount = 0;
20
20
  /**
21
21
  * Serializes every operation that reads-then-writes this backend's shared queue state — the
22
- * `requests` map, the `forefrontRequestIds` array, the `inProgressRequestIds` set and the request
23
- * counts. Those mutations span `await` points, so without this mutex a concurrent operation could
24
- * interleave and corrupt them (e.g. a head scan pruning `forefrontRequestIds` while
25
- * `addBatchOfRequests` pushes to it). Held by every mutating method as well as by `isEmpty`/
26
- * `isFinished`, whose head scan also prunes `forefrontRequestIds`.
22
+ * `requests` map, the `pendingRequestIds` set, the `forefrontRequestIds` array, the
23
+ * `inProgressRequestIds` set and the request counts. Those mutations span `await` points, so
24
+ * without this mutex a concurrent operation could interleave and corrupt them (e.g. a head scan
25
+ * pruning `forefrontRequestIds` while `addBatchOfRequests` pushes to it). Held by every mutating
26
+ * method as well as by `isEmpty`/`isFinished`, whose head scan also prunes `forefrontRequestIds`.
27
27
  */
28
28
  #queueStateMutex = new AsyncQueue();
29
29
  #forefrontRequestIds = [];
@@ -37,16 +37,21 @@ export class RequestQueueBackend extends BaseClient {
37
37
  */
38
38
  #inProgressRequestIds = new Set();
39
39
  #requests = new Map();
40
- // kept as TS-private: storage-backend tests read this field at runtime
41
- storageBackend;
40
+ /**
41
+ * IDs of requests that are not yet handled (pending or in progress), in insertion order. Handled
42
+ * requests stay in `requests` for deduplication but are removed from here, so head scans only
43
+ * ever walk the unhandled tail instead of every request the queue has ever seen.
44
+ */
45
+ #pendingRequestIds = new Set();
46
+ #storageBackend;
42
47
  constructor(options) {
43
48
  super(options.id ?? randomUUID());
44
49
  this.name = options.name;
45
50
  this.cacheKey = options.cacheKey ?? this.name ?? this.id;
46
- this.storageBackend = options.storageBackend;
51
+ this.#storageBackend = options.storageBackend;
47
52
  }
48
53
  async getMetadata() {
49
- this.updateTimestamps(false);
54
+ this.#updateTimestamps(false);
50
55
  return this.toRequestQueueInfo();
51
56
  }
52
57
  async drop() {
@@ -55,16 +60,15 @@ export class RequestQueueBackend extends BaseClient {
55
60
  // removed, which `listPendingHead` would then dereference as `undefined`.
56
61
  await this.#queueStateMutex.wait();
57
62
  try {
58
- const storeIndex = this.storageBackend.requestQueueBackendCache.findIndex((queue) => queue.id === this.id);
59
- if (storeIndex !== -1) {
60
- const [oldBackend] = this.storageBackend.requestQueueBackendCache.splice(storeIndex, 1);
61
- oldBackend.pendingRequestCount = 0;
63
+ if (this.#storageBackend.evictBackend('RequestQueue', this.id)) {
64
+ this.pendingRequestCount = 0;
62
65
  // Clear all in-memory state, consistent with `purge`. Clearing `requests` alone would
63
66
  // leave dangling ids in `forefrontRequestIds`/`inProgressRequestIds`, which a later head
64
67
  // scan would resolve to a missing request and dereference.
65
- oldBackend.#requests.clear();
66
- oldBackend.#forefrontRequestIds = [];
67
- oldBackend.#inProgressRequestIds.clear();
68
+ this.#requests.clear();
69
+ this.#pendingRequestIds.clear();
70
+ this.#forefrontRequestIds = [];
71
+ this.#inProgressRequestIds.clear();
68
72
  }
69
73
  }
70
74
  finally {
@@ -78,23 +82,22 @@ export class RequestQueueBackend extends BaseClient {
78
82
  try {
79
83
  // Clear all in-memory state
80
84
  this.#requests.clear();
85
+ this.#pendingRequestIds.clear();
81
86
  this.#forefrontRequestIds = [];
82
87
  this.#inProgressRequestIds.clear();
83
88
  this.handledRequestCount = 0;
84
89
  this.pendingRequestCount = 0;
85
- this.updateTimestamps(true);
90
+ this.#updateTimestamps(true);
86
91
  }
87
92
  finally {
88
93
  this.#queueStateMutex.shift();
89
94
  }
90
95
  }
91
- *requestKeyIterator() {
96
+ *#requestKeyIterator() {
92
97
  for (let i = this.#forefrontRequestIds.length - 1; i >= 0; i--) {
93
98
  yield this.#forefrontRequestIds[i];
94
99
  }
95
- for (const key of this.#requests.keys()) {
96
- yield key;
97
- }
100
+ yield* this.#pendingRequestIds;
98
101
  }
99
102
  /**
100
103
  * Scans the queue and returns the pending head — requests that are neither handled nor currently
@@ -109,16 +112,16 @@ export class RequestQueueBackend extends BaseClient {
109
112
  * Computing the flag is expensive: because an in-progress request may sit anywhere in the queue, it
110
113
  * forces a scan of every pending entry even when only `limit` items are wanted. Callers that only
111
114
  * need the head (e.g. {@link fetchNextRequest}, {@link isEmpty}) leave it off so the scan can stop as
112
- * soon as the page is filled, keeping those calls O(head) instead of O(N).
115
+ * soon as the page is filled, keeping those calls O(head) instead of O(pending).
113
116
  */
114
- async listPendingHead(limit, detectInProgressRequests = false) {
117
+ async #listPendingHead(limit, detectInProgressRequests = false) {
115
118
  const items = [];
116
119
  let hasInProgressRequests = false;
117
120
  // Tracks processed request IDs to avoid duplicates (request in both `forefrontRequestIds` and `requests`).
118
121
  const seenRequestIds = new Set();
119
122
  // Tracks handled request IDs from `forefrontRequestIds` to be removed.
120
123
  const handledForefrontIds = new Set();
121
- for (const requestId of this.requestKeyIterator()) {
124
+ for (const requestId of this.#requestKeyIterator()) {
122
125
  // Once the requested page is filled we can stop — unless the caller asked us to detect
123
126
  // in-progress requests and we have not yet seen one, in which case we must keep scanning.
124
127
  if (items.length >= limit && (!detectInProgressRequests || hasInProgressRequests)) {
@@ -129,11 +132,10 @@ export class RequestQueueBackend extends BaseClient {
129
132
  }
130
133
  seenRequestIds.add(requestId);
131
134
  const request = this.#requests.get(requestId);
132
- // Permanently-handled requests (`orderNo === null`) are in a terminal state and can be skipped.
135
+ // Only `forefrontRequestIds` can still reference a handled request (`orderNo === null`);
136
+ // `pendingRequestIds` drops them on handling. Remember the id so the list gets pruned below.
133
137
  if (request.orderNo === null) {
134
- if (this.#forefrontRequestIds.includes(requestId)) {
135
- handledForefrontIds.add(requestId);
136
- }
138
+ handledForefrontIds.add(requestId);
137
139
  continue;
138
140
  }
139
141
  // In progress (fetched but not yet handled or reclaimed) — skip it, but remember that the
@@ -153,17 +155,17 @@ export class RequestQueueBackend extends BaseClient {
153
155
  };
154
156
  }
155
157
  async fetchNextRequest() {
156
- this.updateTimestamps(false);
158
+ this.#updateTimestamps(false);
157
159
  await this.#queueStateMutex.wait();
158
160
  try {
159
- const { items: [head], } = await this.listPendingHead(1);
161
+ const { items: [head], } = await this.#listPendingHead(1);
160
162
  if (!head) {
161
163
  return undefined;
162
164
  }
163
165
  // Mark the request as in progress so it is not handed out again until it is handled or
164
166
  // reclaimed. The request keeps its `orderNo` (and thus its forefront / normal ordering).
165
167
  this.#inProgressRequestIds.add(head.id);
166
- return this.jsonToRequest(head.json) ?? undefined;
168
+ return this.#jsonToRequest(head.json) ?? undefined;
167
169
  }
168
170
  finally {
169
171
  this.#queueStateMutex.shift();
@@ -182,7 +184,7 @@ export class RequestQueueBackend extends BaseClient {
182
184
  unprocessedRequests: [],
183
185
  };
184
186
  for (const model of requests) {
185
- const requestModel = this.createInternalRequest(model, options.forefront);
187
+ const requestModel = this.#createInternalRequest(model, options.forefront);
186
188
  const existingRequestWithId = this.#requests.get(requestModel.id);
187
189
  if (existingRequestWithId) {
188
190
  result.processedRequests.push({
@@ -195,6 +197,7 @@ export class RequestQueueBackend extends BaseClient {
195
197
  }
196
198
  this.#requests.set(requestModel.id, requestModel);
197
199
  if (requestModel.orderNo) {
200
+ this.#pendingRequestIds.add(requestModel.id);
198
201
  this.pendingRequestCount += 1;
199
202
  }
200
203
  else {
@@ -212,7 +215,7 @@ export class RequestQueueBackend extends BaseClient {
212
215
  wasAlreadyPresent: false,
213
216
  });
214
217
  }
215
- this.updateTimestamps(true);
218
+ this.#updateTimestamps(true);
216
219
  return result;
217
220
  }
218
221
  finally {
@@ -221,14 +224,14 @@ export class RequestQueueBackend extends BaseClient {
221
224
  }
222
225
  async getRequest(uniqueKey) {
223
226
  parseArgument(uniqueKey, uniqueKeySchema);
224
- this.updateTimestamps(false);
227
+ this.#updateTimestamps(false);
225
228
  const id = uniqueKeyToRequestId(uniqueKey);
226
229
  const json = this.#requests.get(id)?.json;
227
- return this.jsonToRequest(json);
230
+ return this.#jsonToRequest(json);
228
231
  }
229
232
  async markRequestAsHandled(request) {
230
233
  parseArgument(request, schemas.storageRequest);
231
- this.updateTimestamps(false);
234
+ this.#updateTimestamps(false);
232
235
  // Serialize against other mutators (and the head scans in `isEmpty`/`isFinished`) so the shared
233
236
  // `requests` map, `inProgressRequestIds` set and request counts stay consistent across the
234
237
  // `await` points below.
@@ -245,15 +248,16 @@ export class RequestQueueBackend extends BaseClient {
245
248
  // A handled request has `orderNo === null`. Marking it again is an idempotent no-op.
246
249
  const wasAlreadyHandled = existingRequest.orderNo === null;
247
250
  const handledAt = request.handledAt ?? new Date().toISOString();
248
- const requestModel = this.createInternalRequest({ ...request, handledAt }, false);
251
+ const requestModel = this.#createInternalRequest({ ...request, handledAt }, false);
249
252
  this.#requests.set(id, requestModel);
253
+ this.#pendingRequestIds.delete(id);
250
254
  // The request is no longer in progress for this client.
251
255
  this.#inProgressRequestIds.delete(id);
252
256
  if (!wasAlreadyHandled) {
253
257
  this.pendingRequestCount -= 1;
254
258
  this.handledRequestCount += 1;
255
259
  }
256
- this.updateTimestamps(true);
260
+ this.#updateTimestamps(true);
257
261
  return {
258
262
  requestId: id,
259
263
  wasAlreadyHandled,
@@ -267,7 +271,7 @@ export class RequestQueueBackend extends BaseClient {
267
271
  async reclaimRequest(request, options = {}) {
268
272
  parseArgument(request, schemas.storageRequest);
269
273
  parseArgument(options, schemas.requestQueueOperationOptions);
270
- this.updateTimestamps(false);
274
+ this.#updateTimestamps(false);
271
275
  // Serialize against other mutators (and the head scans in `isEmpty`/`isFinished`) so the shared
272
276
  // `requests` map, `forefrontRequestIds` array and `inProgressRequestIds` set stay consistent
273
277
  // across the `await` points below.
@@ -284,14 +288,14 @@ export class RequestQueueBackend extends BaseClient {
284
288
  }
285
289
  // Reclaiming resets the `orderNo` to a fresh timestamp, restoring the request to the queue
286
290
  // (at the front if `forefront`).
287
- const requestModel = this.createInternalRequest(request, options.forefront);
291
+ const requestModel = this.#createInternalRequest(request, options.forefront);
288
292
  this.#requests.set(id, requestModel);
289
293
  // The request is no longer in progress for this client.
290
294
  this.#inProgressRequestIds.delete(id);
291
295
  if (options.forefront) {
292
296
  this.#forefrontRequestIds.push(id);
293
297
  }
294
- this.updateTimestamps(true);
298
+ this.#updateTimestamps(true);
295
299
  return {
296
300
  requestId: id,
297
301
  wasAlreadyHandled: false,
@@ -303,7 +307,7 @@ export class RequestQueueBackend extends BaseClient {
303
307
  }
304
308
  }
305
309
  async isEmpty() {
306
- this.updateTimestamps(false);
310
+ this.#updateTimestamps(false);
307
311
  // "Empty" means there is nothing left to fetch right now — i.e. the next `fetchNextRequest`
308
312
  // would return `null`. Requests that are currently in progress are intentionally NOT counted
309
313
  // here: they are not fetchable, so the queue is empty from a consumer's point of view. Whether
@@ -314,7 +318,7 @@ export class RequestQueueBackend extends BaseClient {
314
318
  // racing a concurrent mutator (e.g. `addBatchOfRequests`) at its `await` points.
315
319
  await this.#queueStateMutex.wait();
316
320
  try {
317
- const { items } = await this.listPendingHead(1);
321
+ const { items } = await this.#listPendingHead(1);
318
322
  return items.length === 0;
319
323
  }
320
324
  finally {
@@ -322,7 +326,7 @@ export class RequestQueueBackend extends BaseClient {
322
326
  }
323
327
  }
324
328
  async isFinished() {
325
- this.updateTimestamps(false);
329
+ this.#updateTimestamps(false);
326
330
  // The queue is finished only when there is nothing left to fetch AND nothing currently in
327
331
  // progress. Counting in-progress requests is what allows a crawler with concurrency to keep
328
332
  // waiting while it still holds the last requests, instead of finishing prematurely.
@@ -334,7 +338,7 @@ export class RequestQueueBackend extends BaseClient {
334
338
  // racing a concurrent mutator (e.g. `addBatchOfRequests`) at its `await` points.
335
339
  await this.#queueStateMutex.wait();
336
340
  try {
337
- const { items, hasInProgressRequests } = await this.listPendingHead(1, true);
341
+ const { items, hasInProgressRequests } = await this.#listPendingHead(1, true);
338
342
  return items.length === 0 && !hasInProgressRequests;
339
343
  }
340
344
  finally {
@@ -347,13 +351,13 @@ export class RequestQueueBackend extends BaseClient {
347
351
  * nothing is marked in progress.
348
352
  */
349
353
  async listItems() {
350
- this.updateTimestamps(false);
354
+ this.#updateTimestamps(false);
351
355
  // `listPendingHead` prunes `forefrontRequestIds` as it scans, so we must hold the queue-state
352
356
  // mutex to avoid racing a concurrent mutator at its `await` points.
353
357
  await this.#queueStateMutex.wait();
354
358
  try {
355
- const { items } = await this.listPendingHead(Number.POSITIVE_INFINITY);
356
- return items.map((request) => this.jsonToRequest(request.json));
359
+ const { items } = await this.#listPendingHead(Number.POSITIVE_INFINITY);
360
+ return items.map((request) => this.#jsonToRequest(request.json));
357
361
  }
358
362
  finally {
359
363
  this.#queueStateMutex.shift();
@@ -371,20 +375,20 @@ export class RequestQueueBackend extends BaseClient {
371
375
  totalRequestCount: this.#requests.size,
372
376
  };
373
377
  }
374
- updateTimestamps(hasBeenModified) {
378
+ #updateTimestamps(hasBeenModified) {
375
379
  this.accessedAt = new Date();
376
380
  if (hasBeenModified) {
377
381
  this.modifiedAt = new Date();
378
382
  }
379
383
  }
380
- jsonToRequest(requestJson) {
384
+ #jsonToRequest(requestJson) {
381
385
  if (!requestJson)
382
386
  return undefined;
383
387
  const request = JSON.parse(requestJson);
384
388
  return purgeNullsFromObject(request);
385
389
  }
386
- createInternalRequest(request, forefront) {
387
- const orderNo = this.calculateOrderNo(request, forefront);
390
+ #createInternalRequest(request, forefront) {
391
+ const orderNo = this.#calculateOrderNo(request, forefront);
388
392
  const id = uniqueKeyToRequestId(request.uniqueKey);
389
393
  if (request.id && request.id !== id) {
390
394
  throw new Error('Request ID does not match its uniqueKey.');
@@ -400,7 +404,7 @@ export class RequestQueueBackend extends BaseClient {
400
404
  url: request.url,
401
405
  };
402
406
  }
403
- calculateOrderNo(request, forefront) {
407
+ #calculateOrderNo(request, forefront) {
404
408
  if (request.handledAt)
405
409
  return null;
406
410
  const timestamp = Date.now();
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@crawlee/core",
3
- "version": "4.0.0-rc.0",
3
+ "version": "4.0.0-rc.1",
4
4
  "description": "The scalable web crawling and scraping library for JavaScript/Node.js. Enables development of data extraction and web automation jobs (not only) with headless Chrome and Puppeteer.",
5
5
  "engines": {
6
- "node": ">=22.0.0"
6
+ "node": ">=22.13.0"
7
7
  },
8
8
  "type": "module",
9
9
  "exports": {
10
10
  ".": "./index.js",
11
+ "./internal": "./internal.js",
11
12
  "./package.json": "./package.json"
12
13
  },
13
14
  "keywords": [
@@ -47,29 +48,26 @@
47
48
  "access": "public"
48
49
  },
49
50
  "dependencies": {
50
- "@apify/consts": "^2.41.0",
51
- "@apify/datastructures": "^2.0.3",
52
- "@apify/log": "^2.5.18",
53
- "@apify/timeout": "^0.4.4",
54
- "@apify/utilities": "^2.15.5",
55
- "@crawlee/fs-storage": "4.0.0-rc.0",
56
- "@crawlee/http-client": "4.0.0-rc.0",
57
- "@crawlee/types": "4.0.0-rc.0",
58
- "@crawlee/utils": "4.0.0-rc.0",
51
+ "@apify/consts": "^3.0.1",
52
+ "@apify/datastructures": "^3.0.1",
53
+ "@apify/log": "^3.0.1",
54
+ "@apify/timeout": "^1.0.1",
55
+ "@apify/utilities": "^3.0.1",
56
+ "@crawlee/fs-storage": "4.0.0-rc.1",
57
+ "@crawlee/http-client": "4.0.0-rc.1",
58
+ "@crawlee/types": "4.0.0-rc.1",
59
+ "@crawlee/utils": "4.0.0-rc.1",
59
60
  "@sapphire/async-queue": "^1.5.5",
60
61
  "@standard-schema/spec": "^1.0.0",
61
62
  "@vladfrangu/async_event_emitter": "^2.4.6",
62
- "content-type": "^1.0.5",
63
+ "content-type": "^3.1.1",
63
64
  "csv-stringify": "^6.5.2",
64
65
  "json5": "^2.2.3",
65
66
  "mime-types": "^3.0.1",
66
- "minimatch": "^10.0.1",
67
- "stream-json": "^1.9.1",
68
- "tldts": "^7.0.6",
69
- "tough-cookie": "^6.0.0",
67
+ "stream-json": "^3.7.0",
70
68
  "tslib": "^2.8.1",
71
69
  "type-fest": "^4.41.0",
72
- "zod": "^4.4.3"
70
+ "zod": "^4.5.4"
73
71
  },
74
72
  "lerna": {
75
73
  "command": {
@@ -78,5 +76,5 @@
78
76
  }
79
77
  }
80
78
  },
81
- "gitHead": "79ab33dacdacb83e0197e6516d145f3aceef80c7"
79
+ "gitHead": "f354ca5e943bed1a5c657a1fce1974c053f9fff0"
82
80
  }
@@ -1,28 +1,28 @@
1
1
  import type { ProxyInfo } from '@crawlee/types';
2
- import type { Request } from './request.js';
3
2
  export interface ProxyConfigurationFunction {
4
- (options?: {
5
- request?: Request;
6
- }): string | null | Promise<string | null>;
3
+ (): string | null | Promise<string | null>;
7
4
  }
8
- type UrlList = (string | null)[];
9
5
  export interface ProxyConfigurationOptions {
10
6
  /**
11
7
  * An array of custom proxy URLs to be rotated.
12
8
  * Custom proxies are not compatible with Apify Proxy and an attempt to use both
13
9
  * configuration options will cause an error to be thrown on initialize.
14
10
  */
15
- proxyUrls?: UrlList;
11
+ proxyUrls?: (string | null)[];
16
12
  /**
17
- * Custom function that allows you to generate the new proxy URL dynamically. It gets an optional parameter with the `Request` object when applicable.
13
+ * Custom function that allows you to generate the new proxy URL dynamically.
18
14
  * Can return either stringified proxy URL or `null` if the proxy should not be used. Can be asynchronous.
19
15
  *
20
16
  * This function is used to generate the URL when {@link ProxyConfiguration.newUrl} or {@link ProxyConfiguration.newProxyInfo} is called.
21
17
  */
22
18
  newUrlFunction?: ProxyConfigurationFunction;
23
- }
24
- interface NewUrlOptions {
25
- request?: Request;
19
+ /**
20
+ * When truthy, the constructor throws unless one of `proxyUrls` / `newUrlFunction` was given. Falsy by
21
+ * default, so a bare `ProxyConfiguration` can be constructed. Set by the Apify SDK, which builds the options
22
+ * object itself; declared here only so that it stays type-checkable.
23
+ * @internal
24
+ */
25
+ validateRequired?: boolean;
26
26
  }
27
27
  /**
28
28
  * Minimal contract that any object passed to a crawler as its `proxyConfiguration`
@@ -36,10 +36,14 @@ interface NewUrlOptions {
36
36
  */
37
37
  export interface IProxyConfiguration {
38
38
  /**
39
- * Creates a new {@link ProxyInfo} object describing the proxy to use for the given
40
- * request. Returns `undefined` when no proxy should be used.
39
+ * Creates a new {@link ProxyInfo} object describing the proxy to use.
40
+ * Returns `undefined` when no proxy should be used.
41
+ *
42
+ * @param proxyInfo A previously created `ProxyInfo`, e.g. one restored with a persisted session. Implementations
43
+ * should return an equivalent `ProxyInfo` that is usable in the current environment, or the argument itself when
44
+ * there is nothing to refresh.
41
45
  */
42
- newProxyInfo(options?: NewUrlOptions): Promise<ProxyInfo | undefined>;
46
+ newProxyInfo(proxyInfo?: ProxyInfo): Promise<ProxyInfo | undefined>;
43
47
  }
44
48
  /**
45
49
  * Configures connection to a proxy server with the provided options. Proxy servers are used to prevent target websites from blocking
@@ -100,22 +104,15 @@ export declare class ProxyConfiguration implements IProxyConfiguration {
100
104
  * Use it if you want to work with a rich representation of a proxy URL.
101
105
  * If you need the URL string only, use {@link ProxyConfiguration.newUrl}.
102
106
  *
107
+ * @param proxyInfo A previously created `ProxyInfo`, returned unchanged.
103
108
  * @return Represents information about used proxy and its configuration.
104
109
  */
105
- newProxyInfo(options?: NewUrlOptions): Promise<ProxyInfo | undefined>;
110
+ newProxyInfo(proxyInfo?: ProxyInfo): Promise<ProxyInfo | undefined>;
106
111
  /**
107
112
  * Returns a new proxy URL based on provided configuration options.
108
113
  *
109
114
  * @return A string with a proxy URL, including authentication credentials and port number.
110
115
  * For example, `http://bob:password123@proxy.example.com:8000`
111
116
  */
112
- newUrl(options?: NewUrlOptions): Promise<string | undefined>;
113
- private handleProxyUrlsList;
114
- /**
115
- * Calls the custom newUrlFunction and checks format of its return value
116
- */
117
- private callNewUrlFunction;
118
- private throwCannotCombineCustomMethods;
119
- private throwNoOptionsProvided;
117
+ newUrl(): Promise<string | undefined>;
120
118
  }
121
- export {};
@@ -61,6 +61,9 @@ export class ProxyConfiguration {
61
61
  * ```
62
62
  */
63
63
  constructor(options = {}) {
64
+ // `validateRequired` is destructured off before the strict-object parse on purpose: the Apify SDK passes it
65
+ // through a computed key (`['validateRequired' as string]: false`), and leaving it in `rest` would make
66
+ // `Actor.createProxyConfiguration()` fail the `z.strictObject` check with a `ZodError`.
64
67
  const { validateRequired, ...rest } = options;
65
68
  if ('tieredProxyUrls' in rest) {
66
69
  throw new Error('The `tieredProxyUrls` option has been removed in Crawlee v4. ' +
@@ -68,9 +71,9 @@ export class ProxyConfiguration {
68
71
  }
69
72
  const { proxyUrls, newUrlFunction } = parseArgument(rest, proxyConfigurationOptionsSchema);
70
73
  if (proxyUrls && newUrlFunction)
71
- this.throwCannotCombineCustomMethods();
74
+ this.#throwCannotCombineCustomMethods();
72
75
  if (!proxyUrls && !newUrlFunction && validateRequired)
73
- this.throwNoOptionsProvided();
76
+ this.#throwNoOptionsProvided();
74
77
  this.#proxyUrls = proxyUrls;
75
78
  this.#newUrlFunction = newUrlFunction;
76
79
  }
@@ -81,10 +84,13 @@ export class ProxyConfiguration {
81
84
  * Use it if you want to work with a rich representation of a proxy URL.
82
85
  * If you need the URL string only, use {@link ProxyConfiguration.newUrl}.
83
86
  *
87
+ * @param proxyInfo A previously created `ProxyInfo`, returned unchanged.
84
88
  * @return Represents information about used proxy and its configuration.
85
89
  */
86
- async newProxyInfo(options) {
87
- const url = await this.newUrl(options);
90
+ async newProxyInfo(proxyInfo) {
91
+ if (proxyInfo)
92
+ return proxyInfo;
93
+ const url = await this.newUrl();
88
94
  if (!url)
89
95
  return undefined;
90
96
  const { username, password, port, hostname } = new URL(url);
@@ -102,20 +108,20 @@ export class ProxyConfiguration {
102
108
  * @return A string with a proxy URL, including authentication credentials and port number.
103
109
  * For example, `http://bob:password123@proxy.example.com:8000`
104
110
  */
105
- async newUrl(options) {
111
+ async newUrl() {
106
112
  if (this.#newUrlFunction) {
107
- return (await this.callNewUrlFunction({ request: options?.request })) ?? undefined;
113
+ return (await this.#callNewUrlFunction()) ?? undefined;
108
114
  }
109
- return this.handleProxyUrlsList() ?? undefined;
115
+ return this.#handleProxyUrlsList() ?? undefined;
110
116
  }
111
- handleProxyUrlsList() {
117
+ #handleProxyUrlsList() {
112
118
  return this.#proxyUrls[this.#nextCustomUrlIndex++ % this.#proxyUrls.length];
113
119
  }
114
120
  /**
115
121
  * Calls the custom newUrlFunction and checks format of its return value
116
122
  */
117
- async callNewUrlFunction(options) {
118
- const proxyUrl = await this.#newUrlFunction(options);
123
+ async #callNewUrlFunction() {
124
+ const proxyUrl = await this.#newUrlFunction();
119
125
  try {
120
126
  if (proxyUrl) {
121
127
  new URL(proxyUrl); // eslint-disable-line no-new
@@ -126,10 +132,10 @@ export class ProxyConfiguration {
126
132
  throw new Error(`The provided newUrlFunction did not return a valid URL.\nCause: ${err.message}`);
127
133
  }
128
134
  }
129
- throwCannotCombineCustomMethods() {
135
+ #throwCannotCombineCustomMethods() {
130
136
  throw new Error('Cannot combine custom proxies "options.proxyUrls" with custom generating function "options.newUrlFunction".');
131
137
  }
132
- throwNoOptionsProvided() {
138
+ #throwNoOptionsProvided() {
133
139
  throw new Error('One of "options.proxyUrls" or "options.newUrlFunction" needs to be provided.');
134
140
  }
135
141
  }
@@ -1,5 +1,6 @@
1
- import type { Configuration, CrawleeLogger } from '@crawlee/core';
2
- import { KeyValueStore } from '@crawlee/core';
1
+ import type { Configuration } from './configuration.js';
2
+ import type { CrawleeLogger } from './log.js';
3
+ import { KeyValueStore } from './storages/key_value_store.js';
3
4
  import type { Awaitable } from '@crawlee/types';
4
5
  import type { StandardSchemaV1 } from '@standard-schema/spec';
5
6
  /**
@@ -47,9 +48,9 @@ export interface RecoverableStatePersistenceOptions {
47
48
  persistenceTimeoutMillis?: number;
48
49
  }
49
50
  /**
50
- * Options for configuring the RecoverableState
51
+ * The fields of {@link RecoverableStateOptions}, without the constraint tying `contentType` to the conversions.
51
52
  */
52
- export interface RecoverableStateOptions<TStateModel = Record<string, unknown>, TPersistedState = TStateModel> extends RecoverableStatePersistenceOptions {
53
+ export interface RecoverableStateBaseOptions<TStateModel = Record<string, unknown>, TPersistedState = TStateModel> extends RecoverableStatePersistenceOptions {
53
54
  /**
54
55
  * The state used when no persisted state is found, and the state {@link RecoverableState.reset} restores.
55
56
  *
@@ -68,14 +69,34 @@ export interface RecoverableStateOptions<TStateModel = Record<string, unknown>,
68
69
  /**
69
70
  * Optional conversion of the state to a plain JSON-serializable value before it is persisted.
70
71
  * If not provided, the state is persisted as is.
72
+ *
73
+ * With {@link RecoverableStateBaseOptions.contentType} set, it has to produce what
74
+ * {@link KeyValueStore.setValue} accepts alongside an explicit content type - a `string`, a `Buffer` or a
75
+ * stream.
71
76
  */
72
77
  serialize?: StateConversion<TStateModel, TPersistedState>;
73
78
  /**
74
79
  * Optional conversion of a persisted value back to the state model, and the place to validate a record before
75
80
  * trusting it. If not provided, the persisted value is used as is.
81
+ *
82
+ * With {@link RecoverableStateBaseOptions.contentType} set, it receives a `Readable` of the record bytes
83
+ * instead of a parsed value.
76
84
  */
77
85
  deserialize?: StateConversion<TPersistedState, TStateModel>;
86
+ /**
87
+ * Content type of the persisted record. Setting it hands the record encoding over to
88
+ * {@link RecoverableStateBaseOptions.serialize} and {@link RecoverableStateBaseOptions.deserialize}, both of
89
+ * which are then required - the default JSON codec is bypassed in both directions. Meant for a state too large
90
+ * for `JSON.stringify`, which `serialize` can then stream out instead.
91
+ */
92
+ contentType?: string;
78
93
  }
94
+ /**
95
+ * Options for configuring the RecoverableState
96
+ */
97
+ export type RecoverableStateOptions<TStateModel = Record<string, unknown>, TPersistedState = TStateModel> = RecoverableStateBaseOptions<TStateModel, TPersistedState> & ({
98
+ contentType?: undefined;
99
+ } | Required<Pick<RecoverableStateBaseOptions<TStateModel, TPersistedState>, 'serialize' | 'deserialize' | 'contentType'>>);
79
100
  /**
80
101
  * A class for managing persistent recoverable state using a plain JavaScript object.
81
102
  *
@@ -102,7 +123,8 @@ export declare class RecoverableState<TStateModel = Record<string, unknown>, TPe
102
123
  * is no record to restore.
103
124
  *
104
125
  * Calling this again after a {@link RecoverableState.teardown} starts a new persistence window - the
105
- * listener is registered again and the record reloaded.
126
+ * listener is registered again. The record is not reloaded: the in-memory state is what the teardown wrote, and
127
+ * reloading it would only drop whatever the deserialization leaves out.
106
128
  *
107
129
  * @returns The loaded state object
108
130
  */
@@ -139,7 +161,7 @@ export declare class RecoverableState<TStateModel = Record<string, unknown>, TPe
139
161
  * one would write the record straight back. Use {@link RecoverableState.reset} to reset the state itself,
140
162
  * or {@link RecoverableState.teardown} before clearing the record.
141
163
  *
142
- * A no-op if persistence is disabled or no KeyValueStore is available yet.
164
+ * A no-op if persistence is disabled.
143
165
  */
144
166
  resetStore(): Promise<void>;
145
167
  /**