@crawlee/core 4.0.0-beta.99 → 4.0.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/autoscaling/autoscaled_pool.d.ts +3 -21
- package/autoscaling/autoscaled_pool.js +105 -97
- package/autoscaling/concurrency_system.d.ts +7 -22
- package/autoscaling/concurrency_system.js +112 -100
- package/autoscaling/cpu_load_signal.d.ts +1 -2
- package/autoscaling/cpu_load_signal.js +11 -10
- package/autoscaling/event_loop_load_signal.d.ts +1 -4
- package/autoscaling/event_loop_load_signal.js +18 -18
- package/autoscaling/index.d.ts +1 -1
- package/autoscaling/index.js +1 -1
- package/autoscaling/load_signal.d.ts +8 -7
- package/autoscaling/load_signal.js +14 -12
- package/autoscaling/memory_load_signal.d.ts +3 -12
- package/autoscaling/memory_load_signal.js +41 -41
- package/autoscaling/snapshotter.d.ts +7 -10
- package/autoscaling/snapshotter.js +18 -18
- package/autoscaling/{client_load_signal.d.ts → storage_backend_load_signal.d.ts} +14 -17
- package/autoscaling/{client_load_signal.js → storage_backend_load_signal.js} +27 -27
- package/autoscaling/system_status.d.ts +9 -11
- package/autoscaling/system_status.js +13 -13
- package/configuration.d.ts +16 -16
- package/configuration.js +6 -6
- package/crawlers/context_pipeline.js +6 -6
- package/crawlers/crawler_commons.d.ts +14 -112
- package/crawlers/crawler_commons.js +1 -107
- package/crawlers/error_snapshotter.d.ts +2 -4
- package/crawlers/error_snapshotter.js +5 -5
- package/crawlers/index.d.ts +1 -1
- package/crawlers/index.js +0 -1
- package/crawlers/statistics.d.ts +164 -45
- package/crawlers/statistics.js +339 -153
- package/debug.js +4 -4
- package/enqueue_links/enqueue_links.d.ts +51 -159
- package/enqueue_links/enqueue_links.js +38 -231
- package/enqueue_links/shared.d.ts +34 -24
- package/enqueue_links/shared.js +54 -36
- package/errors.d.ts +43 -12
- package/errors.js +48 -9
- package/events/event_manager.d.ts +2 -2
- package/events/event_manager.js +7 -6
- package/events/local_event_manager.d.ts +1 -1
- package/events/local_event_manager.js +5 -5
- package/http.d.ts +9 -0
- package/http.js +28 -0
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/log.js +5 -1
- package/memory-storage/memory-storage.d.ts +3 -11
- package/memory-storage/memory-storage.js +42 -48
- package/memory-storage/resource-clients/dataset.d.ts +1 -1
- package/memory-storage/resource-clients/dataset.js +8 -13
- package/memory-storage/resource-clients/key-value-store.d.ts +1 -1
- package/memory-storage/resource-clients/key-value-store.js +36 -38
- package/memory-storage/resource-clients/request-queue.d.ts +4 -23
- package/memory-storage/resource-clients/request-queue.js +68 -80
- package/owned_or_injected.d.ts +1 -3
- package/owned_or_injected.js +17 -17
- package/package.json +8 -9
- package/proxy_configuration.d.ts +1 -3
- package/proxy_configuration.js +18 -14
- package/recoverable_state.d.ts +80 -45
- package/recoverable_state.js +163 -75
- package/request.d.ts +3 -4
- package/request.js +57 -44
- package/router.d.ts +6 -9
- package/router.js +23 -23
- package/serialization.js +14 -13
- package/service_locator.d.ts +19 -10
- package/service_locator.js +57 -48
- package/session_pool/session.d.ts +3 -14
- package/session_pool/session.js +76 -74
- package/session_pool/session_pool.d.ts +6 -12
- package/session_pool/session_pool.js +85 -77
- package/storages/batched_adds.d.ts +37 -0
- package/storages/batched_adds.js +73 -0
- package/storages/dataset.d.ts +12 -1
- package/storages/dataset.js +133 -32
- package/storages/index.d.ts +2 -1
- package/storages/index.js +2 -1
- package/storages/key_value_store.d.ts +19 -4
- package/storages/key_value_store.js +204 -75
- package/storages/request_dedup_cache.d.ts +1 -2
- package/storages/request_dedup_cache.js +9 -9
- package/storages/request_list.d.ts +4 -23
- package/storages/request_list.js +103 -96
- package/storages/request_loader.d.ts +5 -0
- package/storages/request_manager_tandem.d.ts +1 -10
- package/storages/request_manager_tandem.js +27 -27
- package/storages/request_queue.d.ts +21 -18
- package/storages/request_queue.js +318 -177
- package/storages/sitemap_request_loader.d.ts +22 -70
- package/storages/sitemap_request_loader.js +122 -116
- package/storages/storage_instance_manager.d.ts +1 -2
- package/storages/storage_instance_manager.js +17 -17
- package/storages/storage_stats.d.ts +1 -1
- package/storages/storage_stats.js +4 -4
- package/storages/throttling_request_manager.d.ts +239 -0
- package/storages/throttling_request_manager.js +646 -0
- package/storages/transaction.d.ts +252 -0
- package/storages/transaction.js +251 -0
- package/storages/utils.d.ts +6 -3
- package/storages/utils.js +11 -2
- package/system-info/runtime.js +7 -7
- package/url.d.ts +9 -0
- package/url.js +11 -0
- package/validators.d.ts +22 -25
- package/validators.js +13 -25
- package/storages/access_checking.d.ts +0 -12
- package/storages/access_checking.js +0 -17
|
@@ -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 {
|
|
4
|
+
import { z } from 'zod';
|
|
4
5
|
import { purgeNullsFromObject, uniqueKeyToRequestId } from '../utils.js';
|
|
5
6
|
import { BaseClient } from './common/base-client.js';
|
|
6
|
-
const
|
|
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
|
/**
|
|
@@ -38,8 +25,8 @@ export class RequestQueueBackend extends BaseClient {
|
|
|
38
25
|
* `addBatchOfRequests` pushes to it). Held by every mutating method as well as by `isEmpty`/
|
|
39
26
|
* `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,8 +35,9 @@ 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();
|
|
38
|
+
#inProgressRequestIds = new Set();
|
|
39
|
+
#requests = new Map();
|
|
40
|
+
// kept as TS-private: storage-backend tests read this field at runtime
|
|
53
41
|
storageBackend;
|
|
54
42
|
constructor(options) {
|
|
55
43
|
super(options.id ?? randomUUID());
|
|
@@ -65,7 +53,7 @@ export class RequestQueueBackend extends BaseClient {
|
|
|
65
53
|
// Serialize against other mutators (and the head scans in `isEmpty`/`isFinished`) so a concurrent
|
|
66
54
|
// operation cannot observe half-cleared state — e.g. a forefront id whose request has already been
|
|
67
55
|
// removed, which `listPendingHead` would then dereference as `undefined`.
|
|
68
|
-
await this
|
|
56
|
+
await this.#queueStateMutex.wait();
|
|
69
57
|
try {
|
|
70
58
|
const storeIndex = this.storageBackend.requestQueueBackendCache.findIndex((queue) => queue.id === this.id);
|
|
71
59
|
if (storeIndex !== -1) {
|
|
@@ -74,37 +62,37 @@ export class RequestQueueBackend extends BaseClient {
|
|
|
74
62
|
// Clear all in-memory state, consistent with `purge`. Clearing `requests` alone would
|
|
75
63
|
// leave dangling ids in `forefrontRequestIds`/`inProgressRequestIds`, which a later head
|
|
76
64
|
// scan would resolve to a missing request and dereference.
|
|
77
|
-
oldBackend
|
|
78
|
-
oldBackend
|
|
79
|
-
oldBackend
|
|
65
|
+
oldBackend.#requests.clear();
|
|
66
|
+
oldBackend.#forefrontRequestIds = [];
|
|
67
|
+
oldBackend.#inProgressRequestIds.clear();
|
|
80
68
|
}
|
|
81
69
|
}
|
|
82
70
|
finally {
|
|
83
|
-
this
|
|
71
|
+
this.#queueStateMutex.shift();
|
|
84
72
|
}
|
|
85
73
|
}
|
|
86
74
|
async purge() {
|
|
87
75
|
// Serialize against other mutators (and the head scans in `isEmpty`/`isFinished`) so a concurrent
|
|
88
76
|
// operation cannot observe or repopulate half-cleared state across the `await` below.
|
|
89
|
-
await this
|
|
77
|
+
await this.#queueStateMutex.wait();
|
|
90
78
|
try {
|
|
91
79
|
// Clear all in-memory state
|
|
92
|
-
this
|
|
93
|
-
this
|
|
94
|
-
this
|
|
80
|
+
this.#requests.clear();
|
|
81
|
+
this.#forefrontRequestIds = [];
|
|
82
|
+
this.#inProgressRequestIds.clear();
|
|
95
83
|
this.handledRequestCount = 0;
|
|
96
84
|
this.pendingRequestCount = 0;
|
|
97
85
|
this.updateTimestamps(true);
|
|
98
86
|
}
|
|
99
87
|
finally {
|
|
100
|
-
this
|
|
88
|
+
this.#queueStateMutex.shift();
|
|
101
89
|
}
|
|
102
90
|
}
|
|
103
91
|
*requestKeyIterator() {
|
|
104
|
-
for (let i = this
|
|
105
|
-
yield this
|
|
92
|
+
for (let i = this.#forefrontRequestIds.length - 1; i >= 0; i--) {
|
|
93
|
+
yield this.#forefrontRequestIds[i];
|
|
106
94
|
}
|
|
107
|
-
for (const key of this
|
|
95
|
+
for (const key of this.#requests.keys()) {
|
|
108
96
|
yield key;
|
|
109
97
|
}
|
|
110
98
|
}
|
|
@@ -140,17 +128,17 @@ export class RequestQueueBackend extends BaseClient {
|
|
|
140
128
|
continue;
|
|
141
129
|
}
|
|
142
130
|
seenRequestIds.add(requestId);
|
|
143
|
-
const request = this
|
|
131
|
+
const request = this.#requests.get(requestId);
|
|
144
132
|
// Permanently-handled requests (`orderNo === null`) are in a terminal state and can be skipped.
|
|
145
133
|
if (request.orderNo === null) {
|
|
146
|
-
if (this
|
|
134
|
+
if (this.#forefrontRequestIds.includes(requestId)) {
|
|
147
135
|
handledForefrontIds.add(requestId);
|
|
148
136
|
}
|
|
149
137
|
continue;
|
|
150
138
|
}
|
|
151
139
|
// In progress (fetched but not yet handled or reclaimed) — skip it, but remember that the
|
|
152
140
|
// queue is not truly empty.
|
|
153
|
-
if (this
|
|
141
|
+
if (this.#inProgressRequestIds.has(requestId)) {
|
|
154
142
|
hasInProgressRequests = true;
|
|
155
143
|
continue;
|
|
156
144
|
}
|
|
@@ -158,7 +146,7 @@ export class RequestQueueBackend extends BaseClient {
|
|
|
158
146
|
items.push(request);
|
|
159
147
|
}
|
|
160
148
|
}
|
|
161
|
-
this
|
|
149
|
+
this.#forefrontRequestIds = this.#forefrontRequestIds.filter((id) => !handledForefrontIds.has(id));
|
|
162
150
|
return {
|
|
163
151
|
items: items.sort((a, b) => a.orderNo - b.orderNo),
|
|
164
152
|
hasInProgressRequests: detectInProgressRequests ? hasInProgressRequests : undefined,
|
|
@@ -166,7 +154,7 @@ export class RequestQueueBackend extends BaseClient {
|
|
|
166
154
|
}
|
|
167
155
|
async fetchNextRequest() {
|
|
168
156
|
this.updateTimestamps(false);
|
|
169
|
-
await this
|
|
157
|
+
await this.#queueStateMutex.wait();
|
|
170
158
|
try {
|
|
171
159
|
const { items: [head], } = await this.listPendingHead(1);
|
|
172
160
|
if (!head) {
|
|
@@ -174,28 +162,28 @@ export class RequestQueueBackend extends BaseClient {
|
|
|
174
162
|
}
|
|
175
163
|
// Mark the request as in progress so it is not handed out again until it is handled or
|
|
176
164
|
// reclaimed. The request keeps its `orderNo` (and thus its forefront / normal ordering).
|
|
177
|
-
this
|
|
178
|
-
return this.
|
|
165
|
+
this.#inProgressRequestIds.add(head.id);
|
|
166
|
+
return this.jsonToRequest(head.json) ?? undefined;
|
|
179
167
|
}
|
|
180
168
|
finally {
|
|
181
|
-
this
|
|
169
|
+
this.#queueStateMutex.shift();
|
|
182
170
|
}
|
|
183
171
|
}
|
|
184
172
|
async addBatchOfRequests(requests, options = {}) {
|
|
185
|
-
|
|
186
|
-
|
|
173
|
+
parseArgument(requests, schemas.storageRequestBatch);
|
|
174
|
+
parseArgument(options, schemas.requestQueueOperationOptions);
|
|
187
175
|
// Serialize against other mutators (and the head scans in `isEmpty`/`isFinished`) so that the
|
|
188
176
|
// shared `requests` map, `forefrontRequestIds` array and request counts are not corrupted by a
|
|
189
177
|
// concurrent operation interleaving at one of the `await` points below.
|
|
190
|
-
await this
|
|
178
|
+
await this.#queueStateMutex.wait();
|
|
191
179
|
try {
|
|
192
180
|
const result = {
|
|
193
181
|
processedRequests: [],
|
|
194
182
|
unprocessedRequests: [],
|
|
195
183
|
};
|
|
196
184
|
for (const model of requests) {
|
|
197
|
-
const requestModel = this.
|
|
198
|
-
const existingRequestWithId = this
|
|
185
|
+
const requestModel = this.createInternalRequest(model, options.forefront);
|
|
186
|
+
const existingRequestWithId = this.#requests.get(requestModel.id);
|
|
199
187
|
if (existingRequestWithId) {
|
|
200
188
|
result.processedRequests.push({
|
|
201
189
|
requestId: existingRequestWithId.id,
|
|
@@ -205,7 +193,7 @@ export class RequestQueueBackend extends BaseClient {
|
|
|
205
193
|
});
|
|
206
194
|
continue;
|
|
207
195
|
}
|
|
208
|
-
this
|
|
196
|
+
this.#requests.set(requestModel.id, requestModel);
|
|
209
197
|
if (requestModel.orderNo) {
|
|
210
198
|
this.pendingRequestCount += 1;
|
|
211
199
|
}
|
|
@@ -213,7 +201,7 @@ export class RequestQueueBackend extends BaseClient {
|
|
|
213
201
|
this.handledRequestCount += 1;
|
|
214
202
|
}
|
|
215
203
|
if (options.forefront) {
|
|
216
|
-
this
|
|
204
|
+
this.#forefrontRequestIds.push(requestModel.id);
|
|
217
205
|
}
|
|
218
206
|
result.processedRequests.push({
|
|
219
207
|
requestId: requestModel.id,
|
|
@@ -228,26 +216,26 @@ export class RequestQueueBackend extends BaseClient {
|
|
|
228
216
|
return result;
|
|
229
217
|
}
|
|
230
218
|
finally {
|
|
231
|
-
this
|
|
219
|
+
this.#queueStateMutex.shift();
|
|
232
220
|
}
|
|
233
221
|
}
|
|
234
222
|
async getRequest(uniqueKey) {
|
|
235
|
-
|
|
223
|
+
parseArgument(uniqueKey, uniqueKeySchema);
|
|
236
224
|
this.updateTimestamps(false);
|
|
237
225
|
const id = uniqueKeyToRequestId(uniqueKey);
|
|
238
|
-
const json = this
|
|
239
|
-
return this.
|
|
226
|
+
const json = this.#requests.get(id)?.json;
|
|
227
|
+
return this.jsonToRequest(json);
|
|
240
228
|
}
|
|
241
229
|
async markRequestAsHandled(request) {
|
|
242
|
-
|
|
230
|
+
parseArgument(request, schemas.storageRequest);
|
|
243
231
|
this.updateTimestamps(false);
|
|
244
232
|
// Serialize against other mutators (and the head scans in `isEmpty`/`isFinished`) so the shared
|
|
245
233
|
// `requests` map, `inProgressRequestIds` set and request counts stay consistent across the
|
|
246
234
|
// `await` points below.
|
|
247
|
-
await this
|
|
235
|
+
await this.#queueStateMutex.wait();
|
|
248
236
|
try {
|
|
249
237
|
const id = uniqueKeyToRequestId(request.uniqueKey);
|
|
250
|
-
const existingRequest = this
|
|
238
|
+
const existingRequest = this.#requests.get(id);
|
|
251
239
|
// The request must exist to be marked as handled. We intentionally do NOT require it to still
|
|
252
240
|
// be in progress: marking an already-released request handled must still succeed, otherwise
|
|
253
241
|
// the request could be handed out again and the queue would never finish.
|
|
@@ -257,10 +245,10 @@ export class RequestQueueBackend extends BaseClient {
|
|
|
257
245
|
// A handled request has `orderNo === null`. Marking it again is an idempotent no-op.
|
|
258
246
|
const wasAlreadyHandled = existingRequest.orderNo === null;
|
|
259
247
|
const handledAt = request.handledAt ?? new Date().toISOString();
|
|
260
|
-
const requestModel = this.
|
|
261
|
-
this
|
|
248
|
+
const requestModel = this.createInternalRequest({ ...request, handledAt }, false);
|
|
249
|
+
this.#requests.set(id, requestModel);
|
|
262
250
|
// The request is no longer in progress for this client.
|
|
263
|
-
this
|
|
251
|
+
this.#inProgressRequestIds.delete(id);
|
|
264
252
|
if (!wasAlreadyHandled) {
|
|
265
253
|
this.pendingRequestCount -= 1;
|
|
266
254
|
this.handledRequestCount += 1;
|
|
@@ -273,20 +261,20 @@ export class RequestQueueBackend extends BaseClient {
|
|
|
273
261
|
};
|
|
274
262
|
}
|
|
275
263
|
finally {
|
|
276
|
-
this
|
|
264
|
+
this.#queueStateMutex.shift();
|
|
277
265
|
}
|
|
278
266
|
}
|
|
279
267
|
async reclaimRequest(request, options = {}) {
|
|
280
|
-
|
|
281
|
-
|
|
268
|
+
parseArgument(request, schemas.storageRequest);
|
|
269
|
+
parseArgument(options, schemas.requestQueueOperationOptions);
|
|
282
270
|
this.updateTimestamps(false);
|
|
283
271
|
// Serialize against other mutators (and the head scans in `isEmpty`/`isFinished`) so the shared
|
|
284
272
|
// `requests` map, `forefrontRequestIds` array and `inProgressRequestIds` set stay consistent
|
|
285
273
|
// across the `await` points below.
|
|
286
|
-
await this
|
|
274
|
+
await this.#queueStateMutex.wait();
|
|
287
275
|
try {
|
|
288
276
|
const id = uniqueKeyToRequestId(request.uniqueKey);
|
|
289
|
-
const existingRequest = this
|
|
277
|
+
const existingRequest = this.#requests.get(id);
|
|
290
278
|
// The request must exist and not already be handled to be reclaimed. As with
|
|
291
279
|
// `markRequestAsHandled`, we do NOT require it to still be in progress — returning an
|
|
292
280
|
// already-released request to the queue (e.g. to honor a `forefront` reorder) must still
|
|
@@ -296,12 +284,12 @@ export class RequestQueueBackend extends BaseClient {
|
|
|
296
284
|
}
|
|
297
285
|
// Reclaiming resets the `orderNo` to a fresh timestamp, restoring the request to the queue
|
|
298
286
|
// (at the front if `forefront`).
|
|
299
|
-
const requestModel = this.
|
|
300
|
-
this
|
|
287
|
+
const requestModel = this.createInternalRequest(request, options.forefront);
|
|
288
|
+
this.#requests.set(id, requestModel);
|
|
301
289
|
// The request is no longer in progress for this client.
|
|
302
|
-
this
|
|
290
|
+
this.#inProgressRequestIds.delete(id);
|
|
303
291
|
if (options.forefront) {
|
|
304
|
-
this
|
|
292
|
+
this.#forefrontRequestIds.push(id);
|
|
305
293
|
}
|
|
306
294
|
this.updateTimestamps(true);
|
|
307
295
|
return {
|
|
@@ -311,7 +299,7 @@ export class RequestQueueBackend extends BaseClient {
|
|
|
311
299
|
};
|
|
312
300
|
}
|
|
313
301
|
finally {
|
|
314
|
-
this
|
|
302
|
+
this.#queueStateMutex.shift();
|
|
315
303
|
}
|
|
316
304
|
}
|
|
317
305
|
async isEmpty() {
|
|
@@ -324,13 +312,13 @@ export class RequestQueueBackend extends BaseClient {
|
|
|
324
312
|
//
|
|
325
313
|
// `listPendingHead` prunes `forefrontRequestIds` as it scans, so we must hold the queue-state mutex to avoid
|
|
326
314
|
// racing a concurrent mutator (e.g. `addBatchOfRequests`) at its `await` points.
|
|
327
|
-
await this
|
|
315
|
+
await this.#queueStateMutex.wait();
|
|
328
316
|
try {
|
|
329
317
|
const { items } = await this.listPendingHead(1);
|
|
330
318
|
return items.length === 0;
|
|
331
319
|
}
|
|
332
320
|
finally {
|
|
333
|
-
this
|
|
321
|
+
this.#queueStateMutex.shift();
|
|
334
322
|
}
|
|
335
323
|
}
|
|
336
324
|
async isFinished() {
|
|
@@ -344,13 +332,13 @@ export class RequestQueueBackend extends BaseClient {
|
|
|
344
332
|
//
|
|
345
333
|
// `listPendingHead` prunes `forefrontRequestIds` as it scans, so we must hold the queue-state mutex to avoid
|
|
346
334
|
// racing a concurrent mutator (e.g. `addBatchOfRequests`) at its `await` points.
|
|
347
|
-
await this
|
|
335
|
+
await this.#queueStateMutex.wait();
|
|
348
336
|
try {
|
|
349
337
|
const { items, hasInProgressRequests } = await this.listPendingHead(1, true);
|
|
350
338
|
return items.length === 0 && !hasInProgressRequests;
|
|
351
339
|
}
|
|
352
340
|
finally {
|
|
353
|
-
this
|
|
341
|
+
this.#queueStateMutex.shift();
|
|
354
342
|
}
|
|
355
343
|
}
|
|
356
344
|
/**
|
|
@@ -362,13 +350,13 @@ export class RequestQueueBackend extends BaseClient {
|
|
|
362
350
|
this.updateTimestamps(false);
|
|
363
351
|
// `listPendingHead` prunes `forefrontRequestIds` as it scans, so we must hold the queue-state
|
|
364
352
|
// mutex to avoid racing a concurrent mutator at its `await` points.
|
|
365
|
-
await this
|
|
353
|
+
await this.#queueStateMutex.wait();
|
|
366
354
|
try {
|
|
367
355
|
const { items } = await this.listPendingHead(Number.POSITIVE_INFINITY);
|
|
368
|
-
return items.map((request) => this.
|
|
356
|
+
return items.map((request) => this.jsonToRequest(request.json));
|
|
369
357
|
}
|
|
370
358
|
finally {
|
|
371
|
-
this
|
|
359
|
+
this.#queueStateMutex.shift();
|
|
372
360
|
}
|
|
373
361
|
}
|
|
374
362
|
toRequestQueueInfo() {
|
|
@@ -380,7 +368,7 @@ export class RequestQueueBackend extends BaseClient {
|
|
|
380
368
|
modifiedAt: this.modifiedAt,
|
|
381
369
|
name: this.name,
|
|
382
370
|
pendingRequestCount: this.pendingRequestCount,
|
|
383
|
-
totalRequestCount: this
|
|
371
|
+
totalRequestCount: this.#requests.size,
|
|
384
372
|
};
|
|
385
373
|
}
|
|
386
374
|
updateTimestamps(hasBeenModified) {
|
|
@@ -389,14 +377,14 @@ export class RequestQueueBackend extends BaseClient {
|
|
|
389
377
|
this.modifiedAt = new Date();
|
|
390
378
|
}
|
|
391
379
|
}
|
|
392
|
-
|
|
380
|
+
jsonToRequest(requestJson) {
|
|
393
381
|
if (!requestJson)
|
|
394
382
|
return undefined;
|
|
395
383
|
const request = JSON.parse(requestJson);
|
|
396
384
|
return purgeNullsFromObject(request);
|
|
397
385
|
}
|
|
398
|
-
|
|
399
|
-
const orderNo = this.
|
|
386
|
+
createInternalRequest(request, forefront) {
|
|
387
|
+
const orderNo = this.calculateOrderNo(request, forefront);
|
|
400
388
|
const id = uniqueKeyToRequestId(request.uniqueKey);
|
|
401
389
|
if (request.id && request.id !== id) {
|
|
402
390
|
throw new Error('Request ID does not match its uniqueKey.');
|
|
@@ -412,7 +400,7 @@ export class RequestQueueBackend extends BaseClient {
|
|
|
412
400
|
url: request.url,
|
|
413
401
|
};
|
|
414
402
|
}
|
|
415
|
-
|
|
403
|
+
calculateOrderNo(request, forefront) {
|
|
416
404
|
if (request.handledAt)
|
|
417
405
|
return null;
|
|
418
406
|
const timestamp = Date.now();
|
package/owned_or_injected.d.ts
CHANGED
|
@@ -10,9 +10,7 @@
|
|
|
10
10
|
* @internal
|
|
11
11
|
*/
|
|
12
12
|
export declare class OwnedOrInjected<Injected, Owned extends Injected = Injected> {
|
|
13
|
-
private
|
|
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
|
package/owned_or_injected.js
CHANGED
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
* @internal
|
|
11
11
|
*/
|
|
12
12
|
export class OwnedOrInjected {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
#value;
|
|
14
|
+
#owned;
|
|
15
|
+
#present;
|
|
16
16
|
constructor(value, owned, present) {
|
|
17
|
-
this
|
|
18
|
-
this
|
|
19
|
-
this
|
|
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
|
|
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
|
|
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
|
|
59
|
+
if (!this.#present) {
|
|
60
60
|
throw new Error('OwnedOrInjected value is not initialized yet');
|
|
61
61
|
}
|
|
62
|
-
return this
|
|
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
|
|
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
|
|
78
|
+
if (!this.#owned) {
|
|
79
79
|
throw new Error('Cannot set() a borrowed OwnedOrInjected value');
|
|
80
80
|
}
|
|
81
|
-
if (this
|
|
81
|
+
if (this.#present) {
|
|
82
82
|
throw new Error('OwnedOrInjected value is already initialized');
|
|
83
83
|
}
|
|
84
|
-
this
|
|
85
|
-
this
|
|
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
|
|
93
|
+
if (!this.#owned || !this.#present) {
|
|
94
94
|
return undefined;
|
|
95
95
|
}
|
|
96
|
-
return fn(this
|
|
96
|
+
return fn(this.#value);
|
|
97
97
|
}
|
|
98
98
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crawlee/core",
|
|
3
|
-
"version": "4.0.0-
|
|
3
|
+
"version": "4.0.0-rc.0",
|
|
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
6
|
"node": ">=22.0.0"
|
|
@@ -50,27 +50,26 @@
|
|
|
50
50
|
"@apify/consts": "^2.41.0",
|
|
51
51
|
"@apify/datastructures": "^2.0.3",
|
|
52
52
|
"@apify/log": "^2.5.18",
|
|
53
|
-
"@apify/pseudo_url": "^2.0.59",
|
|
54
53
|
"@apify/timeout": "^0.4.4",
|
|
55
54
|
"@apify/utilities": "^2.15.5",
|
|
56
|
-
"@crawlee/fs-storage": "4.0.0-
|
|
57
|
-
"@crawlee/
|
|
58
|
-
"@crawlee/
|
|
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",
|
|
59
59
|
"@sapphire/async-queue": "^1.5.5",
|
|
60
|
-
"@
|
|
60
|
+
"@standard-schema/spec": "^1.0.0",
|
|
61
61
|
"@vladfrangu/async_event_emitter": "^2.4.6",
|
|
62
62
|
"content-type": "^1.0.5",
|
|
63
63
|
"csv-stringify": "^6.5.2",
|
|
64
64
|
"json5": "^2.2.3",
|
|
65
65
|
"mime-types": "^3.0.1",
|
|
66
66
|
"minimatch": "^10.0.1",
|
|
67
|
-
"ow": "^2.0.0",
|
|
68
67
|
"stream-json": "^1.9.1",
|
|
69
68
|
"tldts": "^7.0.6",
|
|
70
69
|
"tough-cookie": "^6.0.0",
|
|
71
70
|
"tslib": "^2.8.1",
|
|
72
71
|
"type-fest": "^4.41.0",
|
|
73
|
-
"zod": "^
|
|
72
|
+
"zod": "^4.4.3"
|
|
74
73
|
},
|
|
75
74
|
"lerna": {
|
|
76
75
|
"command": {
|
|
@@ -79,5 +78,5 @@
|
|
|
79
78
|
}
|
|
80
79
|
}
|
|
81
80
|
},
|
|
82
|
-
"gitHead": "
|
|
81
|
+
"gitHead": "79ab33dacdacb83e0197e6516d145f3aceef80c7"
|
|
83
82
|
}
|
package/proxy_configuration.d.ts
CHANGED
|
@@ -70,10 +70,8 @@ export interface IProxyConfiguration {
|
|
|
70
70
|
* @category Scaling
|
|
71
71
|
*/
|
|
72
72
|
export declare class ProxyConfiguration implements IProxyConfiguration {
|
|
73
|
+
#private;
|
|
73
74
|
readonly isManInTheMiddle = false;
|
|
74
|
-
private nextCustomUrlIndex;
|
|
75
|
-
private proxyUrls?;
|
|
76
|
-
private newUrlFunction?;
|
|
77
75
|
/**
|
|
78
76
|
* Creates a {@link ProxyConfiguration} instance based on the provided options. Proxy servers are used to prevent target websites from
|
|
79
77
|
* blocking your crawlers based on IP address rate limits or blacklists. Setting proxy configuration in your crawlers automatically configures
|
package/proxy_configuration.js
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { parseArgument, schemas } from './validators.js';
|
|
3
|
+
const proxyConfigurationOptionsSchema = z.strictObject({
|
|
4
|
+
proxyUrls: z
|
|
5
|
+
.array(z.union([z.url(), z.null()]))
|
|
6
|
+
.nonempty()
|
|
7
|
+
.optional(),
|
|
8
|
+
newUrlFunction: schemas.anyFunction.optional(),
|
|
9
|
+
});
|
|
2
10
|
/**
|
|
3
11
|
* Configures connection to a proxy server with the provided options. Proxy servers are used to prevent target websites from blocking
|
|
4
12
|
* your crawlers based on IP address rate limits or blacklists. Setting proxy configuration in your crawlers automatically configures
|
|
@@ -29,9 +37,9 @@ import ow from 'ow';
|
|
|
29
37
|
*/
|
|
30
38
|
export class ProxyConfiguration {
|
|
31
39
|
isManInTheMiddle = false;
|
|
32
|
-
nextCustomUrlIndex = 0;
|
|
33
|
-
proxyUrls;
|
|
34
|
-
newUrlFunction;
|
|
40
|
+
#nextCustomUrlIndex = 0;
|
|
41
|
+
#proxyUrls;
|
|
42
|
+
#newUrlFunction;
|
|
35
43
|
/**
|
|
36
44
|
* Creates a {@link ProxyConfiguration} instance based on the provided options. Proxy servers are used to prevent target websites from
|
|
37
45
|
* blocking your crawlers based on IP address rate limits or blacklists. Setting proxy configuration in your crawlers automatically configures
|
|
@@ -58,17 +66,13 @@ export class ProxyConfiguration {
|
|
|
58
66
|
throw new Error('The `tieredProxyUrls` option has been removed in Crawlee v4. ' +
|
|
59
67
|
'See the v4 upgrading guide for the recommended migration to named sessions.');
|
|
60
68
|
}
|
|
61
|
-
|
|
62
|
-
proxyUrls: ow.optional.array.nonEmpty.ofType(ow.any(ow.string.url, ow.null)),
|
|
63
|
-
newUrlFunction: ow.optional.function,
|
|
64
|
-
}));
|
|
65
|
-
const { proxyUrls, newUrlFunction } = options;
|
|
69
|
+
const { proxyUrls, newUrlFunction } = parseArgument(rest, proxyConfigurationOptionsSchema);
|
|
66
70
|
if (proxyUrls && newUrlFunction)
|
|
67
71
|
this.throwCannotCombineCustomMethods();
|
|
68
72
|
if (!proxyUrls && !newUrlFunction && validateRequired)
|
|
69
73
|
this.throwNoOptionsProvided();
|
|
70
|
-
this
|
|
71
|
-
this
|
|
74
|
+
this.#proxyUrls = proxyUrls;
|
|
75
|
+
this.#newUrlFunction = newUrlFunction;
|
|
72
76
|
}
|
|
73
77
|
/**
|
|
74
78
|
* This function creates a new {@link ProxyInfo} info object.
|
|
@@ -99,19 +103,19 @@ export class ProxyConfiguration {
|
|
|
99
103
|
* For example, `http://bob:password123@proxy.example.com:8000`
|
|
100
104
|
*/
|
|
101
105
|
async newUrl(options) {
|
|
102
|
-
if (this
|
|
106
|
+
if (this.#newUrlFunction) {
|
|
103
107
|
return (await this.callNewUrlFunction({ request: options?.request })) ?? undefined;
|
|
104
108
|
}
|
|
105
109
|
return this.handleProxyUrlsList() ?? undefined;
|
|
106
110
|
}
|
|
107
111
|
handleProxyUrlsList() {
|
|
108
|
-
return this
|
|
112
|
+
return this.#proxyUrls[this.#nextCustomUrlIndex++ % this.#proxyUrls.length];
|
|
109
113
|
}
|
|
110
114
|
/**
|
|
111
115
|
* Calls the custom newUrlFunction and checks format of its return value
|
|
112
116
|
*/
|
|
113
117
|
async callNewUrlFunction(options) {
|
|
114
|
-
const proxyUrl = await this
|
|
118
|
+
const proxyUrl = await this.#newUrlFunction(options);
|
|
115
119
|
try {
|
|
116
120
|
if (proxyUrl) {
|
|
117
121
|
new URL(proxyUrl); // eslint-disable-line no-new
|