@crawlee/core 4.0.0-beta.99 → 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 (133) hide show
  1. package/README.md +1 -1
  2. package/configuration.d.ts +16 -47
  3. package/configuration.js +13 -25
  4. package/debug.js +4 -4
  5. package/errors.d.ts +28 -38
  6. package/errors.js +33 -47
  7. package/events/event_manager.d.ts +2 -2
  8. package/events/event_manager.js +7 -6
  9. package/events/index.d.ts +1 -0
  10. package/events/local_event_manager.d.ts +1 -8
  11. package/events/local_event_manager.js +13 -13
  12. package/events/system_info.d.ts +38 -0
  13. package/index.d.ts +2 -8
  14. package/index.js +4 -8
  15. package/internal.d.ts +8 -0
  16. package/internal.js +9 -0
  17. package/log.d.ts +10 -11
  18. package/log.js +52 -20
  19. package/memory-storage/memory-storage.d.ts +15 -18
  20. package/memory-storage/memory-storage.js +80 -58
  21. package/memory-storage/resource-clients/dataset.d.ts +1 -6
  22. package/memory-storage/resource-clients/dataset.js +23 -31
  23. package/memory-storage/resource-clients/key-value-store.d.ts +1 -10
  24. package/memory-storage/resource-clients/key-value-store.js +43 -67
  25. package/memory-storage/resource-clients/request-queue.d.ts +1 -42
  26. package/memory-storage/resource-clients/request-queue.js +109 -117
  27. package/owned_or_injected.d.ts +1 -3
  28. package/owned_or_injected.js +17 -17
  29. package/package.json +17 -20
  30. package/proxy_configuration.d.ts +21 -26
  31. package/proxy_configuration.js +35 -25
  32. package/recoverable_state.d.ts +104 -47
  33. package/recoverable_state.js +199 -74
  34. package/request.d.ts +20 -107
  35. package/request.js +78 -244
  36. package/serialization.js +17 -16
  37. package/service_locator.d.ts +22 -10
  38. package/service_locator.js +59 -48
  39. package/storages/batched_adds.d.ts +37 -0
  40. package/storages/batched_adds.js +73 -0
  41. package/storages/dataset.d.ts +13 -8
  42. package/storages/dataset.js +149 -40
  43. package/storages/index.d.ts +4 -4
  44. package/storages/index.js +2 -4
  45. package/storages/key_value_store.d.ts +16 -35
  46. package/storages/key_value_store.js +223 -110
  47. package/storages/key_value_store_codec.js +6 -11
  48. package/storages/request_dedup_cache.d.ts +1 -4
  49. package/storages/request_dedup_cache.js +15 -15
  50. package/storages/request_list.d.ts +9 -104
  51. package/storages/request_list.js +236 -233
  52. package/storages/request_loader.d.ts +49 -18
  53. package/storages/request_loader.js +36 -1
  54. package/storages/request_manager.d.ts +86 -0
  55. package/storages/request_manager_tandem.d.ts +14 -38
  56. package/storages/request_manager_tandem.js +67 -64
  57. package/storages/request_queue.d.ts +23 -50
  58. package/storages/request_queue.js +371 -226
  59. package/storages/storage_instance_manager.d.ts +2 -4
  60. package/storages/storage_instance_manager.js +21 -21
  61. package/storages/storage_stats.d.ts +1 -1
  62. package/storages/storage_stats.js +4 -4
  63. package/storages/transaction.d.ts +270 -0
  64. package/storages/transaction.js +296 -0
  65. package/storages/utils.d.ts +6 -3
  66. package/storages/utils.js +11 -2
  67. package/system-info/runtime.js +7 -7
  68. package/url.d.ts +9 -0
  69. package/url.js +11 -0
  70. package/validators.d.ts +23 -25
  71. package/validators.js +14 -25
  72. package/autoscaling/autoscaled_pool.d.ts +0 -213
  73. package/autoscaling/autoscaled_pool.js +0 -378
  74. package/autoscaling/client_load_signal.d.ts +0 -59
  75. package/autoscaling/client_load_signal.js +0 -73
  76. package/autoscaling/concurrency_system.d.ts +0 -283
  77. package/autoscaling/concurrency_system.js +0 -350
  78. package/autoscaling/cpu_load_signal.d.ts +0 -44
  79. package/autoscaling/cpu_load_signal.js +0 -46
  80. package/autoscaling/event_loop_load_signal.d.ts +0 -54
  81. package/autoscaling/event_loop_load_signal.js +0 -60
  82. package/autoscaling/index.d.ts +0 -9
  83. package/autoscaling/index.js +0 -9
  84. package/autoscaling/load_signal.d.ts +0 -99
  85. package/autoscaling/load_signal.js +0 -103
  86. package/autoscaling/memory_load_signal.d.ts +0 -56
  87. package/autoscaling/memory_load_signal.js +0 -106
  88. package/autoscaling/snapshotter.d.ts +0 -87
  89. package/autoscaling/snapshotter.js +0 -67
  90. package/autoscaling/system_status.d.ts +0 -161
  91. package/autoscaling/system_status.js +0 -139
  92. package/autoscaling/weighted_avg.d.ts +0 -5
  93. package/autoscaling/weighted_avg.js +0 -14
  94. package/cookie_utils.d.ts +0 -44
  95. package/cookie_utils.js +0 -122
  96. package/crawlers/context_pipeline.d.ts +0 -70
  97. package/crawlers/context_pipeline.js +0 -122
  98. package/crawlers/crawler_commons.d.ts +0 -257
  99. package/crawlers/crawler_commons.js +0 -107
  100. package/crawlers/error_snapshotter.d.ts +0 -59
  101. package/crawlers/error_snapshotter.js +0 -117
  102. package/crawlers/error_tracker.d.ts +0 -54
  103. package/crawlers/error_tracker.js +0 -308
  104. package/crawlers/index.d.ts +0 -5
  105. package/crawlers/index.js +0 -5
  106. package/crawlers/internals/types.d.ts +0 -7
  107. package/crawlers/statistics.d.ts +0 -209
  108. package/crawlers/statistics.js +0 -350
  109. package/enqueue_links/enqueue_links.d.ts +0 -264
  110. package/enqueue_links/enqueue_links.js +0 -271
  111. package/enqueue_links/index.d.ts +0 -2
  112. package/enqueue_links/index.js +0 -2
  113. package/enqueue_links/shared.d.ts +0 -83
  114. package/enqueue_links/shared.js +0 -221
  115. package/router.d.ts +0 -309
  116. package/router.js +0 -309
  117. package/session_pool/consts.d.ts +0 -3
  118. package/session_pool/consts.js +0 -3
  119. package/session_pool/errors.d.ts +0 -7
  120. package/session_pool/errors.js +0 -11
  121. package/session_pool/fingerprint.d.ts +0 -9
  122. package/session_pool/fingerprint.js +0 -30
  123. package/session_pool/index.d.ts +0 -4
  124. package/session_pool/index.js +0 -4
  125. package/session_pool/session.d.ts +0 -161
  126. package/session_pool/session.js +0 -218
  127. package/session_pool/session_pool.d.ts +0 -246
  128. package/session_pool/session_pool.js +0 -386
  129. package/storages/access_checking.d.ts +0 -12
  130. package/storages/access_checking.js +0 -17
  131. package/storages/sitemap_request_loader.d.ts +0 -249
  132. package/storages/sitemap_request_loader.js +0 -432
  133. /package/{crawlers/internals/types.js → events/system_info.js} +0 -0
@@ -1,23 +1,10 @@
1
1
  import { randomUUID } from 'node:crypto';
2
+ import { parseArgument, schemas } from '@crawlee/utils/internal';
2
3
  import { AsyncQueue } from '@sapphire/async-queue';
3
- import { s } from '@sapphire/shapeshift';
4
+ import { z } from 'zod';
4
5
  import { purgeNullsFromObject, uniqueKeyToRequestId } from '../utils.js';
5
6
  import { BaseClient } from './common/base-client.js';
6
- const requestShape = s
7
- .object({
8
- id: s.string(),
9
- url: s.string().url({ allowedProtocols: ['http:', 'https:'] }),
10
- uniqueKey: s.string(),
11
- method: s.string().optional(),
12
- retryCount: s.number().int().optional(),
13
- handledAt: s.union([s.string(), s.date().valid()]).optional(),
14
- })
15
- .passthrough();
16
- const requestShapeWithoutId = requestShape.omit(['id']);
17
- const batchRequestShapeWithoutId = requestShapeWithoutId.array();
18
- const requestOptionsShape = s.object({
19
- forefront: s.boolean().optional(),
20
- });
7
+ const uniqueKeySchema = z.string();
21
8
  export class RequestQueueBackend extends BaseClient {
22
9
  name;
23
10
  /**
@@ -32,14 +19,14 @@ export class RequestQueueBackend extends BaseClient {
32
19
  pendingRequestCount = 0;
33
20
  /**
34
21
  * Serializes every operation that reads-then-writes this backend's shared queue state — the
35
- * `requests` map, the `forefrontRequestIds` array, the `inProgressRequestIds` set and the request
36
- * counts. Those mutations span `await` points, so without this mutex a concurrent operation could
37
- * interleave and corrupt them (e.g. a head scan pruning `forefrontRequestIds` while
38
- * `addBatchOfRequests` pushes to it). Held by every mutating method as well as by `isEmpty`/
39
- * `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`.
40
27
  */
41
- queueStateMutex = new AsyncQueue();
42
- forefrontRequestIds = [];
28
+ #queueStateMutex = new AsyncQueue();
29
+ #forefrontRequestIds = [];
43
30
  /**
44
31
  * IDs of requests currently fetched but not yet handled or reclaimed. A request in this set is
45
32
  * "in progress" and will not be handed out again by {@link fetchNextRequest}.
@@ -48,65 +35,69 @@ export class RequestQueueBackend extends BaseClient {
48
35
  * process and is never shared with another consumer, so there is no need for an expiring,
49
36
  * cross-process-visible lock — tracking in-progress requests in this set is enough.
50
37
  */
51
- inProgressRequestIds = new Set();
52
- requests = new Map();
53
- storageBackend;
38
+ #inProgressRequestIds = new Set();
39
+ #requests = new Map();
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;
54
47
  constructor(options) {
55
48
  super(options.id ?? randomUUID());
56
49
  this.name = options.name;
57
50
  this.cacheKey = options.cacheKey ?? this.name ?? this.id;
58
- this.storageBackend = options.storageBackend;
51
+ this.#storageBackend = options.storageBackend;
59
52
  }
60
53
  async getMetadata() {
61
- this.updateTimestamps(false);
54
+ this.#updateTimestamps(false);
62
55
  return this.toRequestQueueInfo();
63
56
  }
64
57
  async drop() {
65
58
  // Serialize against other mutators (and the head scans in `isEmpty`/`isFinished`) so a concurrent
66
59
  // operation cannot observe half-cleared state — e.g. a forefront id whose request has already been
67
60
  // removed, which `listPendingHead` would then dereference as `undefined`.
68
- await this.queueStateMutex.wait();
61
+ await this.#queueStateMutex.wait();
69
62
  try {
70
- const storeIndex = this.storageBackend.requestQueueBackendCache.findIndex((queue) => queue.id === this.id);
71
- if (storeIndex !== -1) {
72
- const [oldBackend] = this.storageBackend.requestQueueBackendCache.splice(storeIndex, 1);
73
- oldBackend.pendingRequestCount = 0;
63
+ if (this.#storageBackend.evictBackend('RequestQueue', this.id)) {
64
+ this.pendingRequestCount = 0;
74
65
  // Clear all in-memory state, consistent with `purge`. Clearing `requests` alone would
75
66
  // leave dangling ids in `forefrontRequestIds`/`inProgressRequestIds`, which a later head
76
67
  // scan would resolve to a missing request and dereference.
77
- oldBackend.requests.clear();
78
- oldBackend.forefrontRequestIds = [];
79
- oldBackend.inProgressRequestIds.clear();
68
+ this.#requests.clear();
69
+ this.#pendingRequestIds.clear();
70
+ this.#forefrontRequestIds = [];
71
+ this.#inProgressRequestIds.clear();
80
72
  }
81
73
  }
82
74
  finally {
83
- this.queueStateMutex.shift();
75
+ this.#queueStateMutex.shift();
84
76
  }
85
77
  }
86
78
  async purge() {
87
79
  // Serialize against other mutators (and the head scans in `isEmpty`/`isFinished`) so a concurrent
88
80
  // operation cannot observe or repopulate half-cleared state across the `await` below.
89
- await this.queueStateMutex.wait();
81
+ await this.#queueStateMutex.wait();
90
82
  try {
91
83
  // Clear all in-memory state
92
- this.requests.clear();
93
- this.forefrontRequestIds = [];
94
- this.inProgressRequestIds.clear();
84
+ this.#requests.clear();
85
+ this.#pendingRequestIds.clear();
86
+ this.#forefrontRequestIds = [];
87
+ this.#inProgressRequestIds.clear();
95
88
  this.handledRequestCount = 0;
96
89
  this.pendingRequestCount = 0;
97
- this.updateTimestamps(true);
90
+ this.#updateTimestamps(true);
98
91
  }
99
92
  finally {
100
- this.queueStateMutex.shift();
93
+ this.#queueStateMutex.shift();
101
94
  }
102
95
  }
103
- *requestKeyIterator() {
104
- for (let i = this.forefrontRequestIds.length - 1; i >= 0; i--) {
105
- yield this.forefrontRequestIds[i];
106
- }
107
- for (const key of this.requests.keys()) {
108
- yield key;
96
+ *#requestKeyIterator() {
97
+ for (let i = this.#forefrontRequestIds.length - 1; i >= 0; i--) {
98
+ yield this.#forefrontRequestIds[i];
109
99
  }
100
+ yield* this.#pendingRequestIds;
110
101
  }
111
102
  /**
112
103
  * Scans the queue and returns the pending head — requests that are neither handled nor currently
@@ -121,16 +112,16 @@ export class RequestQueueBackend extends BaseClient {
121
112
  * Computing the flag is expensive: because an in-progress request may sit anywhere in the queue, it
122
113
  * forces a scan of every pending entry even when only `limit` items are wanted. Callers that only
123
114
  * need the head (e.g. {@link fetchNextRequest}, {@link isEmpty}) leave it off so the scan can stop as
124
- * 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).
125
116
  */
126
- async listPendingHead(limit, detectInProgressRequests = false) {
117
+ async #listPendingHead(limit, detectInProgressRequests = false) {
127
118
  const items = [];
128
119
  let hasInProgressRequests = false;
129
120
  // Tracks processed request IDs to avoid duplicates (request in both `forefrontRequestIds` and `requests`).
130
121
  const seenRequestIds = new Set();
131
122
  // Tracks handled request IDs from `forefrontRequestIds` to be removed.
132
123
  const handledForefrontIds = new Set();
133
- for (const requestId of this.requestKeyIterator()) {
124
+ for (const requestId of this.#requestKeyIterator()) {
134
125
  // Once the requested page is filled we can stop — unless the caller asked us to detect
135
126
  // in-progress requests and we have not yet seen one, in which case we must keep scanning.
136
127
  if (items.length >= limit && (!detectInProgressRequests || hasInProgressRequests)) {
@@ -140,17 +131,16 @@ export class RequestQueueBackend extends BaseClient {
140
131
  continue;
141
132
  }
142
133
  seenRequestIds.add(requestId);
143
- const request = this.requests.get(requestId);
144
- // Permanently-handled requests (`orderNo === null`) are in a terminal state and can be skipped.
134
+ const request = this.#requests.get(requestId);
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.
145
137
  if (request.orderNo === null) {
146
- if (this.forefrontRequestIds.includes(requestId)) {
147
- handledForefrontIds.add(requestId);
148
- }
138
+ handledForefrontIds.add(requestId);
149
139
  continue;
150
140
  }
151
141
  // In progress (fetched but not yet handled or reclaimed) — skip it, but remember that the
152
142
  // queue is not truly empty.
153
- if (this.inProgressRequestIds.has(requestId)) {
143
+ if (this.#inProgressRequestIds.has(requestId)) {
154
144
  hasInProgressRequests = true;
155
145
  continue;
156
146
  }
@@ -158,44 +148,44 @@ export class RequestQueueBackend extends BaseClient {
158
148
  items.push(request);
159
149
  }
160
150
  }
161
- this.forefrontRequestIds = this.forefrontRequestIds.filter((id) => !handledForefrontIds.has(id));
151
+ this.#forefrontRequestIds = this.#forefrontRequestIds.filter((id) => !handledForefrontIds.has(id));
162
152
  return {
163
153
  items: items.sort((a, b) => a.orderNo - b.orderNo),
164
154
  hasInProgressRequests: detectInProgressRequests ? hasInProgressRequests : undefined,
165
155
  };
166
156
  }
167
157
  async fetchNextRequest() {
168
- this.updateTimestamps(false);
169
- await this.queueStateMutex.wait();
158
+ this.#updateTimestamps(false);
159
+ await this.#queueStateMutex.wait();
170
160
  try {
171
- const { items: [head], } = await this.listPendingHead(1);
161
+ const { items: [head], } = await this.#listPendingHead(1);
172
162
  if (!head) {
173
163
  return undefined;
174
164
  }
175
165
  // Mark the request as in progress so it is not handed out again until it is handled or
176
166
  // reclaimed. The request keeps its `orderNo` (and thus its forefront / normal ordering).
177
- this.inProgressRequestIds.add(head.id);
178
- return this._jsonToRequest(head.json) ?? undefined;
167
+ this.#inProgressRequestIds.add(head.id);
168
+ return this.#jsonToRequest(head.json) ?? undefined;
179
169
  }
180
170
  finally {
181
- this.queueStateMutex.shift();
171
+ this.#queueStateMutex.shift();
182
172
  }
183
173
  }
184
174
  async addBatchOfRequests(requests, options = {}) {
185
- batchRequestShapeWithoutId.parse(requests);
186
- requestOptionsShape.parse(options);
175
+ parseArgument(requests, schemas.storageRequestBatch);
176
+ parseArgument(options, schemas.requestQueueOperationOptions);
187
177
  // Serialize against other mutators (and the head scans in `isEmpty`/`isFinished`) so that the
188
178
  // shared `requests` map, `forefrontRequestIds` array and request counts are not corrupted by a
189
179
  // concurrent operation interleaving at one of the `await` points below.
190
- await this.queueStateMutex.wait();
180
+ await this.#queueStateMutex.wait();
191
181
  try {
192
182
  const result = {
193
183
  processedRequests: [],
194
184
  unprocessedRequests: [],
195
185
  };
196
186
  for (const model of requests) {
197
- const requestModel = this._createInternalRequest(model, options.forefront);
198
- const existingRequestWithId = this.requests.get(requestModel.id);
187
+ const requestModel = this.#createInternalRequest(model, options.forefront);
188
+ const existingRequestWithId = this.#requests.get(requestModel.id);
199
189
  if (existingRequestWithId) {
200
190
  result.processedRequests.push({
201
191
  requestId: existingRequestWithId.id,
@@ -205,15 +195,16 @@ export class RequestQueueBackend extends BaseClient {
205
195
  });
206
196
  continue;
207
197
  }
208
- this.requests.set(requestModel.id, requestModel);
198
+ this.#requests.set(requestModel.id, requestModel);
209
199
  if (requestModel.orderNo) {
200
+ this.#pendingRequestIds.add(requestModel.id);
210
201
  this.pendingRequestCount += 1;
211
202
  }
212
203
  else {
213
204
  this.handledRequestCount += 1;
214
205
  }
215
206
  if (options.forefront) {
216
- this.forefrontRequestIds.push(requestModel.id);
207
+ this.#forefrontRequestIds.push(requestModel.id);
217
208
  }
218
209
  result.processedRequests.push({
219
210
  requestId: requestModel.id,
@@ -224,30 +215,30 @@ export class RequestQueueBackend extends BaseClient {
224
215
  wasAlreadyPresent: false,
225
216
  });
226
217
  }
227
- this.updateTimestamps(true);
218
+ this.#updateTimestamps(true);
228
219
  return result;
229
220
  }
230
221
  finally {
231
- this.queueStateMutex.shift();
222
+ this.#queueStateMutex.shift();
232
223
  }
233
224
  }
234
225
  async getRequest(uniqueKey) {
235
- s.string().parse(uniqueKey);
236
- this.updateTimestamps(false);
226
+ parseArgument(uniqueKey, uniqueKeySchema);
227
+ this.#updateTimestamps(false);
237
228
  const id = uniqueKeyToRequestId(uniqueKey);
238
- const json = this.requests.get(id)?.json;
239
- return this._jsonToRequest(json);
229
+ const json = this.#requests.get(id)?.json;
230
+ return this.#jsonToRequest(json);
240
231
  }
241
232
  async markRequestAsHandled(request) {
242
- requestShape.parse(request);
243
- this.updateTimestamps(false);
233
+ parseArgument(request, schemas.storageRequest);
234
+ this.#updateTimestamps(false);
244
235
  // Serialize against other mutators (and the head scans in `isEmpty`/`isFinished`) so the shared
245
236
  // `requests` map, `inProgressRequestIds` set and request counts stay consistent across the
246
237
  // `await` points below.
247
- await this.queueStateMutex.wait();
238
+ await this.#queueStateMutex.wait();
248
239
  try {
249
240
  const id = uniqueKeyToRequestId(request.uniqueKey);
250
- const existingRequest = this.requests.get(id);
241
+ const existingRequest = this.#requests.get(id);
251
242
  // The request must exist to be marked as handled. We intentionally do NOT require it to still
252
243
  // be in progress: marking an already-released request handled must still succeed, otherwise
253
244
  // the request could be handed out again and the queue would never finish.
@@ -257,15 +248,16 @@ export class RequestQueueBackend extends BaseClient {
257
248
  // A handled request has `orderNo === null`. Marking it again is an idempotent no-op.
258
249
  const wasAlreadyHandled = existingRequest.orderNo === null;
259
250
  const handledAt = request.handledAt ?? new Date().toISOString();
260
- const requestModel = this._createInternalRequest({ ...request, handledAt }, false);
261
- this.requests.set(id, requestModel);
251
+ const requestModel = this.#createInternalRequest({ ...request, handledAt }, false);
252
+ this.#requests.set(id, requestModel);
253
+ this.#pendingRequestIds.delete(id);
262
254
  // The request is no longer in progress for this client.
263
- this.inProgressRequestIds.delete(id);
255
+ this.#inProgressRequestIds.delete(id);
264
256
  if (!wasAlreadyHandled) {
265
257
  this.pendingRequestCount -= 1;
266
258
  this.handledRequestCount += 1;
267
259
  }
268
- this.updateTimestamps(true);
260
+ this.#updateTimestamps(true);
269
261
  return {
270
262
  requestId: id,
271
263
  wasAlreadyHandled,
@@ -273,20 +265,20 @@ export class RequestQueueBackend extends BaseClient {
273
265
  };
274
266
  }
275
267
  finally {
276
- this.queueStateMutex.shift();
268
+ this.#queueStateMutex.shift();
277
269
  }
278
270
  }
279
271
  async reclaimRequest(request, options = {}) {
280
- requestShape.parse(request);
281
- requestOptionsShape.parse(options);
282
- this.updateTimestamps(false);
272
+ parseArgument(request, schemas.storageRequest);
273
+ parseArgument(options, schemas.requestQueueOperationOptions);
274
+ this.#updateTimestamps(false);
283
275
  // Serialize against other mutators (and the head scans in `isEmpty`/`isFinished`) so the shared
284
276
  // `requests` map, `forefrontRequestIds` array and `inProgressRequestIds` set stay consistent
285
277
  // across the `await` points below.
286
- await this.queueStateMutex.wait();
278
+ await this.#queueStateMutex.wait();
287
279
  try {
288
280
  const id = uniqueKeyToRequestId(request.uniqueKey);
289
- const existingRequest = this.requests.get(id);
281
+ const existingRequest = this.#requests.get(id);
290
282
  // The request must exist and not already be handled to be reclaimed. As with
291
283
  // `markRequestAsHandled`, we do NOT require it to still be in progress — returning an
292
284
  // already-released request to the queue (e.g. to honor a `forefront` reorder) must still
@@ -296,14 +288,14 @@ export class RequestQueueBackend extends BaseClient {
296
288
  }
297
289
  // Reclaiming resets the `orderNo` to a fresh timestamp, restoring the request to the queue
298
290
  // (at the front if `forefront`).
299
- const requestModel = this._createInternalRequest(request, options.forefront);
300
- this.requests.set(id, requestModel);
291
+ const requestModel = this.#createInternalRequest(request, options.forefront);
292
+ this.#requests.set(id, requestModel);
301
293
  // The request is no longer in progress for this client.
302
- this.inProgressRequestIds.delete(id);
294
+ this.#inProgressRequestIds.delete(id);
303
295
  if (options.forefront) {
304
- this.forefrontRequestIds.push(id);
296
+ this.#forefrontRequestIds.push(id);
305
297
  }
306
- this.updateTimestamps(true);
298
+ this.#updateTimestamps(true);
307
299
  return {
308
300
  requestId: id,
309
301
  wasAlreadyHandled: false,
@@ -311,11 +303,11 @@ export class RequestQueueBackend extends BaseClient {
311
303
  };
312
304
  }
313
305
  finally {
314
- this.queueStateMutex.shift();
306
+ this.#queueStateMutex.shift();
315
307
  }
316
308
  }
317
309
  async isEmpty() {
318
- this.updateTimestamps(false);
310
+ this.#updateTimestamps(false);
319
311
  // "Empty" means there is nothing left to fetch right now — i.e. the next `fetchNextRequest`
320
312
  // would return `null`. Requests that are currently in progress are intentionally NOT counted
321
313
  // here: they are not fetchable, so the queue is empty from a consumer's point of view. Whether
@@ -324,17 +316,17 @@ export class RequestQueueBackend extends BaseClient {
324
316
  //
325
317
  // `listPendingHead` prunes `forefrontRequestIds` as it scans, so we must hold the queue-state mutex to avoid
326
318
  // racing a concurrent mutator (e.g. `addBatchOfRequests`) at its `await` points.
327
- await this.queueStateMutex.wait();
319
+ await this.#queueStateMutex.wait();
328
320
  try {
329
- const { items } = await this.listPendingHead(1);
321
+ const { items } = await this.#listPendingHead(1);
330
322
  return items.length === 0;
331
323
  }
332
324
  finally {
333
- this.queueStateMutex.shift();
325
+ this.#queueStateMutex.shift();
334
326
  }
335
327
  }
336
328
  async isFinished() {
337
- this.updateTimestamps(false);
329
+ this.#updateTimestamps(false);
338
330
  // The queue is finished only when there is nothing left to fetch AND nothing currently in
339
331
  // progress. Counting in-progress requests is what allows a crawler with concurrency to keep
340
332
  // waiting while it still holds the last requests, instead of finishing prematurely.
@@ -344,13 +336,13 @@ export class RequestQueueBackend extends BaseClient {
344
336
  //
345
337
  // `listPendingHead` prunes `forefrontRequestIds` as it scans, so we must hold the queue-state mutex to avoid
346
338
  // racing a concurrent mutator (e.g. `addBatchOfRequests`) at its `await` points.
347
- await this.queueStateMutex.wait();
339
+ await this.#queueStateMutex.wait();
348
340
  try {
349
- const { items, hasInProgressRequests } = await this.listPendingHead(1, true);
341
+ const { items, hasInProgressRequests } = await this.#listPendingHead(1, true);
350
342
  return items.length === 0 && !hasInProgressRequests;
351
343
  }
352
344
  finally {
353
- this.queueStateMutex.shift();
345
+ this.#queueStateMutex.shift();
354
346
  }
355
347
  }
356
348
  /**
@@ -359,16 +351,16 @@ export class RequestQueueBackend extends BaseClient {
359
351
  * nothing is marked in progress.
360
352
  */
361
353
  async listItems() {
362
- this.updateTimestamps(false);
354
+ this.#updateTimestamps(false);
363
355
  // `listPendingHead` prunes `forefrontRequestIds` as it scans, so we must hold the queue-state
364
356
  // mutex to avoid racing a concurrent mutator at its `await` points.
365
- await this.queueStateMutex.wait();
357
+ await this.#queueStateMutex.wait();
366
358
  try {
367
- const { items } = await this.listPendingHead(Number.POSITIVE_INFINITY);
368
- 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));
369
361
  }
370
362
  finally {
371
- this.queueStateMutex.shift();
363
+ this.#queueStateMutex.shift();
372
364
  }
373
365
  }
374
366
  toRequestQueueInfo() {
@@ -380,23 +372,23 @@ export class RequestQueueBackend extends BaseClient {
380
372
  modifiedAt: this.modifiedAt,
381
373
  name: this.name,
382
374
  pendingRequestCount: this.pendingRequestCount,
383
- totalRequestCount: this.requests.size,
375
+ totalRequestCount: this.#requests.size,
384
376
  };
385
377
  }
386
- updateTimestamps(hasBeenModified) {
378
+ #updateTimestamps(hasBeenModified) {
387
379
  this.accessedAt = new Date();
388
380
  if (hasBeenModified) {
389
381
  this.modifiedAt = new Date();
390
382
  }
391
383
  }
392
- _jsonToRequest(requestJson) {
384
+ #jsonToRequest(requestJson) {
393
385
  if (!requestJson)
394
386
  return undefined;
395
387
  const request = JSON.parse(requestJson);
396
388
  return purgeNullsFromObject(request);
397
389
  }
398
- _createInternalRequest(request, forefront) {
399
- const orderNo = this._calculateOrderNo(request, forefront);
390
+ #createInternalRequest(request, forefront) {
391
+ const orderNo = this.#calculateOrderNo(request, forefront);
400
392
  const id = uniqueKeyToRequestId(request.uniqueKey);
401
393
  if (request.id && request.id !== id) {
402
394
  throw new Error('Request ID does not match its uniqueKey.');
@@ -412,7 +404,7 @@ export class RequestQueueBackend extends BaseClient {
412
404
  url: request.url,
413
405
  };
414
406
  }
415
- _calculateOrderNo(request, forefront) {
407
+ #calculateOrderNo(request, forefront) {
416
408
  if (request.handledAt)
417
409
  return null;
418
410
  const timestamp = Date.now();
@@ -10,9 +10,7 @@
10
10
  * @internal
11
11
  */
12
12
  export declare class OwnedOrInjected<Injected, Owned extends Injected = Injected> {
13
- private _value;
14
- private readonly _owned;
15
- private _present;
13
+ #private;
16
14
  private constructor();
17
15
  /**
18
16
  * Resolves a collaborator from an optionally-injected instance. `Injected` is the public/borrowed type exposed via
@@ -10,13 +10,13 @@
10
10
  * @internal
11
11
  */
12
12
  export class OwnedOrInjected {
13
- _value;
14
- _owned;
15
- _present;
13
+ #value;
14
+ #owned;
15
+ #present;
16
16
  constructor(value, owned, present) {
17
- this._value = value;
18
- this._owned = owned;
19
- this._present = present;
17
+ this.#value = value;
18
+ this.#owned = owned;
19
+ this.#present = present;
20
20
  }
21
21
  /**
22
22
  * Resolves a collaborator from an optionally-injected instance. `Injected` is the public/borrowed type exposed via
@@ -42,24 +42,24 @@ export class OwnedOrInjected {
42
42
  * crawler-built defaults, `false` for user-injected instances.
43
43
  */
44
44
  get isOwned() {
45
- return this._owned;
45
+ return this.#owned;
46
46
  }
47
47
  /**
48
48
  * Whether a value is currently available. `false` for an owned slot whose default hasn't been built yet
49
49
  * (e.g. a lazily-opened request queue before its first use).
50
50
  */
51
51
  get isPresent() {
52
- return this._present;
52
+ return this.#present;
53
53
  }
54
54
  /**
55
55
  * The resolved instance, typed as the public `Injected` type. Throws if the value is not present yet — callers that
56
56
  * expect a lazily-filled owned slot should read {@link OwnedOrInjected.maybeValue|`maybeValue`} instead.
57
57
  */
58
58
  get value() {
59
- if (!this._present) {
59
+ if (!this.#present) {
60
60
  throw new Error('OwnedOrInjected value is not initialized yet');
61
61
  }
62
- return this._value;
62
+ return this.#value;
63
63
  }
64
64
  /**
65
65
  * The resolved instance, or `undefined` when a lazily-filled owned slot hasn't been built yet. The non-throwing
@@ -67,7 +67,7 @@ export class OwnedOrInjected {
67
67
  * a possibly-empty slot without the `isPresent ? value : …` dance.
68
68
  */
69
69
  get maybeValue() {
70
- return this._present ? this._value : undefined;
70
+ return this.#present ? this.#value : undefined;
71
71
  }
72
72
  /**
73
73
  * Fills the (owned) slot with the crawler-built default, returning it for convenience. Only valid on an owned,
@@ -75,14 +75,14 @@ export class OwnedOrInjected {
75
75
  * would silently orphan the previous instance's lifecycle).
76
76
  */
77
77
  set(value) {
78
- if (!this._owned) {
78
+ if (!this.#owned) {
79
79
  throw new Error('Cannot set() a borrowed OwnedOrInjected value');
80
80
  }
81
- if (this._present) {
81
+ if (this.#present) {
82
82
  throw new Error('OwnedOrInjected value is already initialized');
83
83
  }
84
- this._value = value;
85
- this._present = true;
84
+ this.#value = value;
85
+ this.#present = true;
86
86
  return value;
87
87
  }
88
88
  /**
@@ -90,9 +90,9 @@ export class OwnedOrInjected {
90
90
  * owns a present instance — a no-op for a borrowed instance or an owned-but-not-yet-built slot.
91
91
  */
92
92
  async ifOwned(fn) {
93
- if (!this._owned || !this._present) {
93
+ if (!this.#owned || !this.#present) {
94
94
  return undefined;
95
95
  }
96
- return fn(this._value);
96
+ return fn(this.#value);
97
97
  }
98
98
  }
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@crawlee/core",
3
- "version": "4.0.0-beta.99",
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,30 +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/pseudo_url": "^2.0.59",
54
- "@apify/timeout": "^0.4.4",
55
- "@apify/utilities": "^2.15.5",
56
- "@crawlee/fs-storage": "4.0.0-beta.99",
57
- "@crawlee/types": "4.0.0-beta.99",
58
- "@crawlee/utils": "4.0.0-beta.99",
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
- "@sapphire/shapeshift": "^4.0.0",
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
- "ow": "^2.0.0",
68
- "stream-json": "^1.9.1",
69
- "tldts": "^7.0.6",
70
- "tough-cookie": "^6.0.0",
67
+ "stream-json": "^3.7.0",
71
68
  "tslib": "^2.8.1",
72
69
  "type-fest": "^4.41.0",
73
- "zod": "^3.24.0 || ^4.0.0"
70
+ "zod": "^4.5.4"
74
71
  },
75
72
  "lerna": {
76
73
  "command": {
@@ -79,5 +76,5 @@
79
76
  }
80
77
  }
81
78
  },
82
- "gitHead": "ad2748380941842bb10cff100f4b4caad92049e3"
79
+ "gitHead": "f354ca5e943bed1a5c657a1fce1974c053f9fff0"
83
80
  }