@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.
- package/README.md +1 -1
- package/configuration.d.ts +16 -47
- package/configuration.js +13 -25
- package/debug.js +4 -4
- package/errors.d.ts +28 -38
- package/errors.js +33 -47
- package/events/event_manager.d.ts +2 -2
- package/events/event_manager.js +7 -6
- package/events/index.d.ts +1 -0
- package/events/local_event_manager.d.ts +1 -8
- package/events/local_event_manager.js +13 -13
- 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 +52 -20
- package/memory-storage/memory-storage.d.ts +15 -18
- package/memory-storage/memory-storage.js +80 -58
- package/memory-storage/resource-clients/dataset.d.ts +1 -6
- package/memory-storage/resource-clients/dataset.js +23 -31
- package/memory-storage/resource-clients/key-value-store.d.ts +1 -10
- package/memory-storage/resource-clients/key-value-store.js +43 -67
- package/memory-storage/resource-clients/request-queue.d.ts +1 -42
- package/memory-storage/resource-clients/request-queue.js +109 -117
- package/owned_or_injected.d.ts +1 -3
- package/owned_or_injected.js +17 -17
- package/package.json +17 -20
- package/proxy_configuration.d.ts +21 -26
- package/proxy_configuration.js +35 -25
- package/recoverable_state.d.ts +104 -47
- package/recoverable_state.js +199 -74
- package/request.d.ts +20 -107
- package/request.js +78 -244
- package/serialization.js +17 -16
- package/service_locator.d.ts +22 -10
- package/service_locator.js +59 -48
- package/storages/batched_adds.d.ts +37 -0
- package/storages/batched_adds.js +73 -0
- package/storages/dataset.d.ts +13 -8
- package/storages/dataset.js +149 -40
- package/storages/index.d.ts +4 -4
- package/storages/index.js +2 -4
- package/storages/key_value_store.d.ts +16 -35
- package/storages/key_value_store.js +223 -110
- package/storages/key_value_store_codec.js +6 -11
- package/storages/request_dedup_cache.d.ts +1 -4
- package/storages/request_dedup_cache.js +15 -15
- package/storages/request_list.d.ts +9 -104
- package/storages/request_list.js +236 -233
- package/storages/request_loader.d.ts +49 -18
- package/storages/request_loader.js +36 -1
- package/storages/request_manager.d.ts +86 -0
- package/storages/request_manager_tandem.d.ts +14 -38
- package/storages/request_manager_tandem.js +67 -64
- package/storages/request_queue.d.ts +23 -50
- package/storages/request_queue.js +371 -226
- package/storages/storage_instance_manager.d.ts +2 -4
- package/storages/storage_instance_manager.js +21 -21
- package/storages/storage_stats.d.ts +1 -1
- package/storages/storage_stats.js +4 -4
- package/storages/transaction.d.ts +270 -0
- package/storages/transaction.js +296 -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 +23 -25
- package/validators.js +14 -25
- package/autoscaling/autoscaled_pool.d.ts +0 -213
- package/autoscaling/autoscaled_pool.js +0 -378
- package/autoscaling/client_load_signal.d.ts +0 -59
- package/autoscaling/client_load_signal.js +0 -73
- package/autoscaling/concurrency_system.d.ts +0 -283
- package/autoscaling/concurrency_system.js +0 -350
- package/autoscaling/cpu_load_signal.d.ts +0 -44
- package/autoscaling/cpu_load_signal.js +0 -46
- package/autoscaling/event_loop_load_signal.d.ts +0 -54
- 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 -99
- package/autoscaling/load_signal.js +0 -103
- package/autoscaling/memory_load_signal.d.ts +0 -56
- package/autoscaling/memory_load_signal.js +0 -106
- package/autoscaling/snapshotter.d.ts +0 -87
- package/autoscaling/snapshotter.js +0 -67
- package/autoscaling/system_status.d.ts +0 -161
- 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 -257
- package/crawlers/crawler_commons.js +0 -107
- package/crawlers/error_snapshotter.d.ts +0 -59
- 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 -5
- package/crawlers/internals/types.d.ts +0 -7
- package/crawlers/statistics.d.ts +0 -209
- package/crawlers/statistics.js +0 -350
- package/enqueue_links/enqueue_links.d.ts +0 -264
- package/enqueue_links/enqueue_links.js +0 -271
- package/enqueue_links/index.d.ts +0 -2
- package/enqueue_links/index.js +0 -2
- package/enqueue_links/shared.d.ts +0 -83
- package/enqueue_links/shared.js +0 -221
- package/router.d.ts +0 -309
- 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 -161
- package/session_pool/session.js +0 -218
- package/session_pool/session_pool.d.ts +0 -246
- package/session_pool/session_pool.js +0 -386
- package/storages/access_checking.d.ts +0 -12
- package/storages/access_checking.js +0 -17
- package/storages/sitemap_request_loader.d.ts +0 -249
- package/storages/sitemap_request_loader.js +0 -432
- /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 {
|
|
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
|
/**
|
|
@@ -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 `
|
|
36
|
-
* counts. Those mutations span `await` points, so
|
|
37
|
-
* interleave and corrupt them (e.g. a head scan
|
|
38
|
-
* `addBatchOfRequests` pushes to it). Held by every mutating
|
|
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
|
-
|
|
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
|
|
51
|
+
this.#storageBackend = options.storageBackend;
|
|
59
52
|
}
|
|
60
53
|
async getMetadata() {
|
|
61
|
-
this
|
|
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
|
|
61
|
+
await this.#queueStateMutex.wait();
|
|
69
62
|
try {
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
68
|
+
this.#requests.clear();
|
|
69
|
+
this.#pendingRequestIds.clear();
|
|
70
|
+
this.#forefrontRequestIds = [];
|
|
71
|
+
this.#inProgressRequestIds.clear();
|
|
80
72
|
}
|
|
81
73
|
}
|
|
82
74
|
finally {
|
|
83
|
-
this
|
|
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
|
|
81
|
+
await this.#queueStateMutex.wait();
|
|
90
82
|
try {
|
|
91
83
|
// Clear all in-memory state
|
|
92
|
-
this
|
|
93
|
-
this.
|
|
94
|
-
this
|
|
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
|
|
90
|
+
this.#updateTimestamps(true);
|
|
98
91
|
}
|
|
99
92
|
finally {
|
|
100
|
-
this
|
|
93
|
+
this.#queueStateMutex.shift();
|
|
101
94
|
}
|
|
102
95
|
}
|
|
103
|
-
|
|
104
|
-
for (let i = this
|
|
105
|
-
yield this
|
|
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(
|
|
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
|
|
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
|
|
144
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
169
|
-
await this
|
|
158
|
+
this.#updateTimestamps(false);
|
|
159
|
+
await this.#queueStateMutex.wait();
|
|
170
160
|
try {
|
|
171
|
-
const { items: [head], } = await this
|
|
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
|
|
178
|
-
return this
|
|
167
|
+
this.#inProgressRequestIds.add(head.id);
|
|
168
|
+
return this.#jsonToRequest(head.json) ?? undefined;
|
|
179
169
|
}
|
|
180
170
|
finally {
|
|
181
|
-
this
|
|
171
|
+
this.#queueStateMutex.shift();
|
|
182
172
|
}
|
|
183
173
|
}
|
|
184
174
|
async addBatchOfRequests(requests, options = {}) {
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
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
|
|
198
|
-
const existingRequestWithId = this
|
|
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
|
|
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
|
|
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
|
|
218
|
+
this.#updateTimestamps(true);
|
|
228
219
|
return result;
|
|
229
220
|
}
|
|
230
221
|
finally {
|
|
231
|
-
this
|
|
222
|
+
this.#queueStateMutex.shift();
|
|
232
223
|
}
|
|
233
224
|
}
|
|
234
225
|
async getRequest(uniqueKey) {
|
|
235
|
-
|
|
236
|
-
this
|
|
226
|
+
parseArgument(uniqueKey, uniqueKeySchema);
|
|
227
|
+
this.#updateTimestamps(false);
|
|
237
228
|
const id = uniqueKeyToRequestId(uniqueKey);
|
|
238
|
-
const json = this
|
|
239
|
-
return this
|
|
229
|
+
const json = this.#requests.get(id)?.json;
|
|
230
|
+
return this.#jsonToRequest(json);
|
|
240
231
|
}
|
|
241
232
|
async markRequestAsHandled(request) {
|
|
242
|
-
|
|
243
|
-
this
|
|
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
|
|
238
|
+
await this.#queueStateMutex.wait();
|
|
248
239
|
try {
|
|
249
240
|
const id = uniqueKeyToRequestId(request.uniqueKey);
|
|
250
|
-
const existingRequest = this
|
|
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
|
|
261
|
-
this
|
|
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
|
|
255
|
+
this.#inProgressRequestIds.delete(id);
|
|
264
256
|
if (!wasAlreadyHandled) {
|
|
265
257
|
this.pendingRequestCount -= 1;
|
|
266
258
|
this.handledRequestCount += 1;
|
|
267
259
|
}
|
|
268
|
-
this
|
|
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
|
|
268
|
+
this.#queueStateMutex.shift();
|
|
277
269
|
}
|
|
278
270
|
}
|
|
279
271
|
async reclaimRequest(request, options = {}) {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
this
|
|
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
|
|
278
|
+
await this.#queueStateMutex.wait();
|
|
287
279
|
try {
|
|
288
280
|
const id = uniqueKeyToRequestId(request.uniqueKey);
|
|
289
|
-
const existingRequest = this
|
|
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
|
|
300
|
-
this
|
|
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
|
|
294
|
+
this.#inProgressRequestIds.delete(id);
|
|
303
295
|
if (options.forefront) {
|
|
304
|
-
this
|
|
296
|
+
this.#forefrontRequestIds.push(id);
|
|
305
297
|
}
|
|
306
|
-
this
|
|
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
|
|
306
|
+
this.#queueStateMutex.shift();
|
|
315
307
|
}
|
|
316
308
|
}
|
|
317
309
|
async isEmpty() {
|
|
318
|
-
this
|
|
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
|
|
319
|
+
await this.#queueStateMutex.wait();
|
|
328
320
|
try {
|
|
329
|
-
const { items } = await this
|
|
321
|
+
const { items } = await this.#listPendingHead(1);
|
|
330
322
|
return items.length === 0;
|
|
331
323
|
}
|
|
332
324
|
finally {
|
|
333
|
-
this
|
|
325
|
+
this.#queueStateMutex.shift();
|
|
334
326
|
}
|
|
335
327
|
}
|
|
336
328
|
async isFinished() {
|
|
337
|
-
this
|
|
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
|
|
339
|
+
await this.#queueStateMutex.wait();
|
|
348
340
|
try {
|
|
349
|
-
const { items, hasInProgressRequests } = await this
|
|
341
|
+
const { items, hasInProgressRequests } = await this.#listPendingHead(1, true);
|
|
350
342
|
return items.length === 0 && !hasInProgressRequests;
|
|
351
343
|
}
|
|
352
344
|
finally {
|
|
353
|
-
this
|
|
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
|
|
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
|
|
357
|
+
await this.#queueStateMutex.wait();
|
|
366
358
|
try {
|
|
367
|
-
const { items } = await this
|
|
368
|
-
return items.map((request) => this
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
399
|
-
const orderNo = this
|
|
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
|
-
|
|
407
|
+
#calculateOrderNo(request, forefront) {
|
|
416
408
|
if (request.handledAt)
|
|
417
409
|
return null;
|
|
418
410
|
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,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crawlee/core",
|
|
3
|
-
"version": "4.0.0-
|
|
3
|
+
"version": "4.0.0-rc.1",
|
|
4
4
|
"description": "The scalable web crawling and scraping library for JavaScript/Node.js. Enables development of data extraction and web automation jobs (not only) with headless Chrome and Puppeteer.",
|
|
5
5
|
"engines": {
|
|
6
|
-
"node": ">=22.
|
|
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": "^
|
|
51
|
-
"@apify/datastructures": "^
|
|
52
|
-
"@apify/log": "^
|
|
53
|
-
"@apify/
|
|
54
|
-
"@apify/
|
|
55
|
-
"@
|
|
56
|
-
"@crawlee/
|
|
57
|
-
"@crawlee/types": "4.0.0-
|
|
58
|
-
"@crawlee/utils": "4.0.0-
|
|
51
|
+
"@apify/consts": "^3.0.1",
|
|
52
|
+
"@apify/datastructures": "^3.0.1",
|
|
53
|
+
"@apify/log": "^3.0.1",
|
|
54
|
+
"@apify/timeout": "^1.0.1",
|
|
55
|
+
"@apify/utilities": "^3.0.1",
|
|
56
|
+
"@crawlee/fs-storage": "4.0.0-rc.1",
|
|
57
|
+
"@crawlee/http-client": "4.0.0-rc.1",
|
|
58
|
+
"@crawlee/types": "4.0.0-rc.1",
|
|
59
|
+
"@crawlee/utils": "4.0.0-rc.1",
|
|
59
60
|
"@sapphire/async-queue": "^1.5.5",
|
|
60
|
-
"@
|
|
61
|
+
"@standard-schema/spec": "^1.0.0",
|
|
61
62
|
"@vladfrangu/async_event_emitter": "^2.4.6",
|
|
62
|
-
"content-type": "^1.
|
|
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
|
-
"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": "^
|
|
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": "
|
|
79
|
+
"gitHead": "f354ca5e943bed1a5c657a1fce1974c053f9fff0"
|
|
83
80
|
}
|