@crawlee/core 4.0.0-rc.0 → 4.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/configuration.d.ts +15 -46
- package/configuration.js +8 -20
- package/errors.d.ts +12 -53
- package/errors.js +13 -66
- package/events/index.d.ts +1 -0
- package/events/local_event_manager.d.ts +0 -7
- package/events/local_event_manager.js +8 -8
- package/events/system_info.d.ts +38 -0
- package/index.d.ts +2 -8
- package/index.js +4 -8
- package/internal.d.ts +8 -0
- package/internal.js +9 -0
- package/log.d.ts +10 -11
- package/log.js +53 -25
- package/memory-storage/memory-storage.d.ts +12 -7
- package/memory-storage/memory-storage.js +45 -17
- package/memory-storage/resource-clients/dataset.d.ts +0 -5
- package/memory-storage/resource-clients/dataset.js +17 -20
- package/memory-storage/resource-clients/key-value-store.d.ts +0 -9
- package/memory-storage/resource-clients/key-value-store.js +11 -33
- package/memory-storage/resource-clients/request-queue.d.ts +0 -22
- package/memory-storage/resource-clients/request-queue.js +57 -53
- package/package.json +16 -18
- package/proxy_configuration.d.ts +20 -23
- package/proxy_configuration.js +18 -12
- package/recoverable_state.d.ts +28 -6
- package/recoverable_state.js +51 -14
- package/request.d.ts +18 -104
- package/request.js +41 -220
- package/serialization.js +3 -3
- package/service_locator.d.ts +3 -0
- package/service_locator.js +2 -0
- package/storages/dataset.d.ts +9 -15
- package/storages/dataset.js +22 -14
- package/storages/index.d.ts +3 -4
- package/storages/index.js +1 -4
- package/storages/key_value_store.d.ts +12 -46
- package/storages/key_value_store.js +31 -47
- package/storages/key_value_store_codec.js +6 -11
- package/storages/request_dedup_cache.d.ts +0 -2
- package/storages/request_dedup_cache.js +8 -8
- package/storages/request_list.d.ts +6 -82
- package/storages/request_list.js +175 -179
- package/storages/request_loader.d.ts +48 -22
- package/storages/request_loader.js +36 -1
- package/storages/request_manager.d.ts +86 -0
- package/storages/request_manager_tandem.d.ts +13 -28
- package/storages/request_manager_tandem.js +46 -43
- package/storages/request_queue.d.ts +19 -49
- package/storages/request_queue.js +92 -88
- package/storages/storage_instance_manager.d.ts +1 -2
- package/storages/storage_instance_manager.js +4 -4
- package/storages/transaction.d.ts +27 -9
- package/storages/transaction.js +56 -11
- package/storages/utils.d.ts +2 -2
- package/validators.d.ts +3 -2
- package/validators.js +3 -2
- package/autoscaling/autoscaled_pool.d.ts +0 -195
- package/autoscaling/autoscaled_pool.js +0 -386
- package/autoscaling/concurrency_system.d.ts +0 -268
- package/autoscaling/concurrency_system.js +0 -362
- package/autoscaling/cpu_load_signal.d.ts +0 -43
- package/autoscaling/cpu_load_signal.js +0 -47
- package/autoscaling/event_loop_load_signal.d.ts +0 -51
- package/autoscaling/event_loop_load_signal.js +0 -60
- package/autoscaling/index.d.ts +0 -9
- package/autoscaling/index.js +0 -9
- package/autoscaling/load_signal.d.ts +0 -100
- package/autoscaling/load_signal.js +0 -105
- package/autoscaling/memory_load_signal.d.ts +0 -47
- package/autoscaling/memory_load_signal.js +0 -106
- package/autoscaling/snapshotter.d.ts +0 -84
- package/autoscaling/snapshotter.js +0 -67
- package/autoscaling/storage_backend_load_signal.d.ts +0 -56
- package/autoscaling/storage_backend_load_signal.js +0 -73
- package/autoscaling/system_status.d.ts +0 -159
- package/autoscaling/system_status.js +0 -139
- package/autoscaling/weighted_avg.d.ts +0 -5
- package/autoscaling/weighted_avg.js +0 -14
- package/cookie_utils.d.ts +0 -44
- package/cookie_utils.js +0 -122
- package/crawlers/context_pipeline.d.ts +0 -70
- package/crawlers/context_pipeline.js +0 -122
- package/crawlers/crawler_commons.d.ts +0 -159
- package/crawlers/error_snapshotter.d.ts +0 -57
- package/crawlers/error_snapshotter.js +0 -117
- package/crawlers/error_tracker.d.ts +0 -54
- package/crawlers/error_tracker.js +0 -308
- package/crawlers/index.d.ts +0 -5
- package/crawlers/index.js +0 -4
- package/crawlers/internals/types.d.ts +0 -7
- package/crawlers/internals/types.js +0 -1
- package/crawlers/statistics.d.ts +0 -328
- package/crawlers/statistics.js +0 -536
- package/enqueue_links/enqueue_links.d.ts +0 -156
- package/enqueue_links/enqueue_links.js +0 -78
- package/enqueue_links/index.d.ts +0 -2
- package/enqueue_links/index.js +0 -2
- package/enqueue_links/shared.d.ts +0 -93
- package/enqueue_links/shared.js +0 -239
- package/http.d.ts +0 -9
- package/http.js +0 -28
- package/router.d.ts +0 -306
- package/router.js +0 -309
- package/session_pool/consts.d.ts +0 -3
- package/session_pool/consts.js +0 -3
- package/session_pool/errors.d.ts +0 -7
- package/session_pool/errors.js +0 -11
- package/session_pool/fingerprint.d.ts +0 -9
- package/session_pool/fingerprint.js +0 -30
- package/session_pool/index.d.ts +0 -4
- package/session_pool/index.js +0 -4
- package/session_pool/session.d.ts +0 -150
- package/session_pool/session.js +0 -220
- package/session_pool/session_pool.d.ts +0 -240
- package/session_pool/session_pool.js +0 -394
- package/storages/sitemap_request_loader.d.ts +0 -201
- package/storages/sitemap_request_loader.js +0 -438
- package/storages/throttling_request_manager.d.ts +0 -239
- package/storages/throttling_request_manager.js +0 -646
- /package/{crawlers/crawler_commons.js → events/system_info.js} +0 -0
|
@@ -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 `
|
|
23
|
-
* counts. Those mutations span `await` points, so
|
|
24
|
-
* interleave and corrupt them (e.g. a head scan
|
|
25
|
-
* `addBatchOfRequests` pushes to it). Held by every mutating
|
|
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
|
-
|
|
41
|
-
|
|
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
|
|
51
|
+
this.#storageBackend = options.storageBackend;
|
|
47
52
|
}
|
|
48
53
|
async getMetadata() {
|
|
49
|
-
this
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
90
|
+
this.#updateTimestamps(true);
|
|
86
91
|
}
|
|
87
92
|
finally {
|
|
88
93
|
this.#queueStateMutex.shift();
|
|
89
94
|
}
|
|
90
95
|
}
|
|
91
|
-
|
|
96
|
+
*#requestKeyIterator() {
|
|
92
97
|
for (let i = this.#forefrontRequestIds.length - 1; i >= 0; i--) {
|
|
93
98
|
yield this.#forefrontRequestIds[i];
|
|
94
99
|
}
|
|
95
|
-
|
|
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(
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
158
|
+
this.#updateTimestamps(false);
|
|
157
159
|
await this.#queueStateMutex.wait();
|
|
158
160
|
try {
|
|
159
|
-
const { items: [head], } = await this
|
|
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
|
|
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
|
|
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
|
|
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
|
|
227
|
+
this.#updateTimestamps(false);
|
|
225
228
|
const id = uniqueKeyToRequestId(uniqueKey);
|
|
226
229
|
const json = this.#requests.get(id)?.json;
|
|
227
|
-
return this
|
|
230
|
+
return this.#jsonToRequest(json);
|
|
228
231
|
}
|
|
229
232
|
async markRequestAsHandled(request) {
|
|
230
233
|
parseArgument(request, schemas.storageRequest);
|
|
231
|
-
this
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
356
|
-
return items.map((request) => this
|
|
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
|
|
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.
|
|
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.
|
|
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": "^
|
|
51
|
-
"@apify/datastructures": "^
|
|
52
|
-
"@apify/log": "^
|
|
53
|
-
"@apify/timeout": "^0.
|
|
54
|
-
"@apify/utilities": "^
|
|
55
|
-
"@crawlee/fs-storage": "4.0.0-rc.
|
|
56
|
-
"@crawlee/http-client": "4.0.0-rc.
|
|
57
|
-
"@crawlee/types": "4.0.0-rc.
|
|
58
|
-
"@crawlee/utils": "4.0.0-rc.
|
|
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.
|
|
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
|
-
"
|
|
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
|
|
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": "
|
|
79
|
+
"gitHead": "f354ca5e943bed1a5c657a1fce1974c053f9fff0"
|
|
82
80
|
}
|
package/proxy_configuration.d.ts
CHANGED
|
@@ -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
|
-
(
|
|
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?:
|
|
11
|
+
proxyUrls?: (string | null)[];
|
|
16
12
|
/**
|
|
17
|
-
* Custom function that allows you to generate the new proxy URL dynamically.
|
|
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
|
-
|
|
25
|
-
|
|
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
|
|
40
|
-
*
|
|
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(
|
|
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(
|
|
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(
|
|
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 {};
|
package/proxy_configuration.js
CHANGED
|
@@ -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
|
|
74
|
+
this.#throwCannotCombineCustomMethods();
|
|
72
75
|
if (!proxyUrls && !newUrlFunction && validateRequired)
|
|
73
|
-
this
|
|
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(
|
|
87
|
-
|
|
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(
|
|
111
|
+
async newUrl() {
|
|
106
112
|
if (this.#newUrlFunction) {
|
|
107
|
-
return (await this
|
|
113
|
+
return (await this.#callNewUrlFunction()) ?? undefined;
|
|
108
114
|
}
|
|
109
|
-
return this
|
|
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(
|
|
118
|
-
const proxyUrl = await this.#newUrlFunction(
|
|
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
|
}
|
package/recoverable_state.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { Configuration
|
|
2
|
-
import {
|
|
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
|
-
*
|
|
51
|
+
* The fields of {@link RecoverableStateOptions}, without the constraint tying `contentType` to the conversions.
|
|
51
52
|
*/
|
|
52
|
-
export interface
|
|
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
|
|
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
|
|
164
|
+
* A no-op if persistence is disabled.
|
|
143
165
|
*/
|
|
144
166
|
resetStore(): Promise<void>;
|
|
145
167
|
/**
|