@crawlee/core 4.0.0-beta.98 → 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 -116
- 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 -10
- 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 -21
- 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 -4
- package/storages/storage_instance_manager.js +17 -19
- 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
package/storages/request_list.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { downloadListOfUrls } from '@crawlee/utils';
|
|
2
|
-
import
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { EventType } from '../events/event_manager.js';
|
|
3
4
|
import { Request } from '../request.js';
|
|
4
5
|
import { createDeserialize, serializeArray } from '../serialization.js';
|
|
5
6
|
import { serviceLocator } from '../service_locator.js';
|
|
7
|
+
import { parseArgument, schemas, validators } from '../validators.js';
|
|
6
8
|
import { KeyValueStore } from './key_value_store.js';
|
|
7
9
|
import { purgeDefaultStorages } from './utils.js';
|
|
8
10
|
/** @internal */
|
|
@@ -10,6 +12,24 @@ export const STATE_PERSISTENCE_KEY = 'REQUEST_LIST_STATE';
|
|
|
10
12
|
/** @internal */
|
|
11
13
|
export const REQUESTS_PERSISTENCE_KEY = 'REQUEST_LIST_REQUESTS';
|
|
12
14
|
const CONTENT_TYPE_BINARY = 'application/octet-stream';
|
|
15
|
+
const requestListOptionsSchema = z.strictObject({
|
|
16
|
+
sources: schemas.anyArray.optional(), // check only for array and not subtypes to avoid iteration over the whole thing
|
|
17
|
+
sourcesFunction: schemas.anyFunction.optional(),
|
|
18
|
+
persistStateKey: z.string().optional(),
|
|
19
|
+
persistRequestsKey: z.string().optional(),
|
|
20
|
+
state: z
|
|
21
|
+
.strictObject({
|
|
22
|
+
nextIndex: schemas.anyNumber,
|
|
23
|
+
nextUniqueKey: z.string(),
|
|
24
|
+
inProgress: schemas.anyObject, // persisted as an array of unique keys
|
|
25
|
+
})
|
|
26
|
+
.optional(),
|
|
27
|
+
keepDuplicateUrls: z.boolean().default(false),
|
|
28
|
+
proxyConfiguration: validators.proxyConfiguration.optional(),
|
|
29
|
+
httpClient: schemas.httpClient.optional(),
|
|
30
|
+
});
|
|
31
|
+
const listNameSchema = z.string().nullish();
|
|
32
|
+
const openOptionsSchema = z.looseObject({});
|
|
13
33
|
/**
|
|
14
34
|
* Represents a static list of URLs to crawl.
|
|
15
35
|
* The URLs can be provided either in code or parsed from a text file hosted on the web.
|
|
@@ -72,7 +92,7 @@ const CONTENT_TYPE_BINARY = 'application/octet-stream';
|
|
|
72
92
|
* @category Sources
|
|
73
93
|
*/
|
|
74
94
|
export class RequestList {
|
|
75
|
-
log = serviceLocator.getLogger().child({ prefix: 'RequestList' });
|
|
95
|
+
#log = serviceLocator.getLogger().child({ prefix: 'RequestList' });
|
|
76
96
|
/**
|
|
77
97
|
* Array of all requests from all sources, in the order as they appeared in sources.
|
|
78
98
|
* All requests in the array have distinct uniqueKey!
|
|
@@ -80,9 +100,9 @@ export class RequestList {
|
|
|
80
100
|
*/
|
|
81
101
|
requests = [];
|
|
82
102
|
/** Index to the next item in requests array to fetch. All previous requests are either handled or in progress. */
|
|
83
|
-
nextIndex = 0;
|
|
103
|
+
#nextIndex = 0;
|
|
84
104
|
/** Dictionary, key is Request.uniqueKey, value is corresponding index in the requests array. */
|
|
85
|
-
uniqueKeyToIndex = {};
|
|
105
|
+
#uniqueKeyToIndex = {};
|
|
86
106
|
/**
|
|
87
107
|
* Set of `uniqueKey`s of requests that were returned by fetchNextRequest().
|
|
88
108
|
* @internal
|
|
@@ -93,7 +113,7 @@ export class RequestList {
|
|
|
93
113
|
* re-crawled after a restart. They are served before advancing through the rest of the sources.
|
|
94
114
|
* @internal
|
|
95
115
|
*/
|
|
96
|
-
requestsToRetry = [];
|
|
116
|
+
#requestsToRetry = [];
|
|
97
117
|
/**
|
|
98
118
|
* Starts as true because until we handle the first request, the list is effectively persisted by doing nothing.
|
|
99
119
|
* @internal
|
|
@@ -104,52 +124,39 @@ export class RequestList {
|
|
|
104
124
|
* @internal
|
|
105
125
|
*/
|
|
106
126
|
areRequestsPersisted = false;
|
|
107
|
-
isLoading = false;
|
|
108
|
-
isInitialized = false;
|
|
109
|
-
persistStateKey;
|
|
110
|
-
persistRequestsKey;
|
|
111
|
-
initialState;
|
|
112
|
-
store;
|
|
113
|
-
keepDuplicateUrls;
|
|
127
|
+
#isLoading = false;
|
|
128
|
+
#isInitialized = false;
|
|
129
|
+
#persistStateKey;
|
|
130
|
+
#persistRequestsKey;
|
|
131
|
+
#initialState;
|
|
132
|
+
#store;
|
|
133
|
+
#keepDuplicateUrls;
|
|
134
|
+
// kept as TS-private: request_list tests read this field directly
|
|
114
135
|
sources;
|
|
115
|
-
sourcesFunction;
|
|
116
|
-
proxyConfiguration;
|
|
117
|
-
httpClient;
|
|
136
|
+
#sourcesFunction;
|
|
137
|
+
#proxyConfiguration;
|
|
138
|
+
#httpClient;
|
|
118
139
|
/**
|
|
119
140
|
* To create new instance of `RequestList` we need to use `RequestList.open()` factory method.
|
|
120
141
|
* @param options All `RequestList` configuration options
|
|
121
142
|
* @internal
|
|
122
143
|
*/
|
|
123
144
|
constructor(options = {}) {
|
|
124
|
-
const { sources, sourcesFunction, persistStateKey, persistRequestsKey, state, proxyConfiguration, keepDuplicateUrls
|
|
145
|
+
const { sources, sourcesFunction, persistStateKey, persistRequestsKey, state, proxyConfiguration, keepDuplicateUrls, httpClient, } = parseArgument(options, requestListOptionsSchema);
|
|
125
146
|
if (!(sources || sourcesFunction)) {
|
|
126
|
-
throw new
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
persistRequestsKey: ow.optional.string,
|
|
133
|
-
state: ow.optional.object.exactShape({
|
|
134
|
-
nextIndex: ow.number,
|
|
135
|
-
nextUniqueKey: ow.string,
|
|
136
|
-
inProgress: ow.object,
|
|
137
|
-
}),
|
|
138
|
-
keepDuplicateUrls: ow.optional.boolean,
|
|
139
|
-
proxyConfiguration: ow.optional.object,
|
|
140
|
-
httpClient: ow.optional.object,
|
|
141
|
-
}));
|
|
142
|
-
this.persistStateKey = persistStateKey ? `CRAWLEE_${persistStateKey}` : persistStateKey;
|
|
143
|
-
this.persistRequestsKey = persistRequestsKey ? `CRAWLEE_${persistRequestsKey}` : persistRequestsKey;
|
|
144
|
-
this.initialState = state;
|
|
145
|
-
this.httpClient = httpClient;
|
|
147
|
+
throw new Error('At least one of "sources" or "sourcesFunction" must be provided.');
|
|
148
|
+
}
|
|
149
|
+
this.#persistStateKey = persistStateKey ? `CRAWLEE_${persistStateKey}` : persistStateKey;
|
|
150
|
+
this.#persistRequestsKey = persistRequestsKey ? `CRAWLEE_${persistRequestsKey}` : persistRequestsKey;
|
|
151
|
+
this.#initialState = state;
|
|
152
|
+
this.#httpClient = httpClient;
|
|
146
153
|
// If this option is set then all requests will get a pre-generated unique ID and duplicate URLs will be kept in the list.
|
|
147
|
-
this
|
|
154
|
+
this.#keepDuplicateUrls = keepDuplicateUrls;
|
|
148
155
|
// Will be empty after initialization to save memory.
|
|
149
156
|
this.sources = sources ? [...sources] : [];
|
|
150
|
-
this
|
|
157
|
+
this.#sourcesFunction = sourcesFunction;
|
|
151
158
|
// The proxy configuration used for `requestsFromUrl` requests.
|
|
152
|
-
this
|
|
159
|
+
this.#proxyConfiguration = proxyConfiguration;
|
|
153
160
|
this.persistState = this.persistState.bind(this);
|
|
154
161
|
}
|
|
155
162
|
/**
|
|
@@ -157,10 +164,10 @@ export class RequestList {
|
|
|
157
164
|
* This function must be called before you can start using the instance in a meaningful way.
|
|
158
165
|
*/
|
|
159
166
|
async initialize() {
|
|
160
|
-
if (this
|
|
167
|
+
if (this.#isLoading) {
|
|
161
168
|
throw new Error('RequestList sources are already loading or were loaded.');
|
|
162
169
|
}
|
|
163
|
-
this
|
|
170
|
+
this.#isLoading = true;
|
|
164
171
|
await purgeDefaultStorages({ onlyPurgeOnce: true });
|
|
165
172
|
const [state, persistedRequests] = await this.loadStateAndPersistedRequests();
|
|
166
173
|
// Add persisted requests / new sources in a memory efficient way because with very
|
|
@@ -172,11 +179,11 @@ export class RequestList {
|
|
|
172
179
|
await this.addRequestsFromSources();
|
|
173
180
|
}
|
|
174
181
|
this.restoreState(state);
|
|
175
|
-
this
|
|
176
|
-
if (this
|
|
182
|
+
this.#isInitialized = true;
|
|
183
|
+
if (this.#persistRequestsKey && !this.areRequestsPersisted)
|
|
177
184
|
await this.persistRequests();
|
|
178
|
-
if (this
|
|
179
|
-
serviceLocator.getEventManager().on(
|
|
185
|
+
if (this.#persistStateKey) {
|
|
186
|
+
serviceLocator.getEventManager().on(EventType.PERSIST_STATE, this.persistState);
|
|
180
187
|
}
|
|
181
188
|
return this;
|
|
182
189
|
}
|
|
@@ -224,9 +231,9 @@ export class RequestList {
|
|
|
224
231
|
}
|
|
225
232
|
// Drop the original array full of empty indexes.
|
|
226
233
|
this.sources = [];
|
|
227
|
-
if (this
|
|
234
|
+
if (this.#sourcesFunction) {
|
|
228
235
|
try {
|
|
229
|
-
const sourcesFromFunction = await this
|
|
236
|
+
const sourcesFromFunction = await this.#sourcesFunction();
|
|
230
237
|
const sourcesFromFunctionCount = sourcesFromFunction.length;
|
|
231
238
|
for (let i = 0; i < sourcesFromFunctionCount; i++) {
|
|
232
239
|
const source = sourcesFromFunction[i];
|
|
@@ -246,19 +253,19 @@ export class RequestList {
|
|
|
246
253
|
* @inheritDoc
|
|
247
254
|
*/
|
|
248
255
|
async persistState() {
|
|
249
|
-
if (!this
|
|
256
|
+
if (!this.#persistStateKey) {
|
|
250
257
|
throw new Error('Cannot persist state. options.persistStateKey is not set.');
|
|
251
258
|
}
|
|
252
259
|
if (this.isStatePersisted)
|
|
253
260
|
return;
|
|
254
261
|
try {
|
|
255
|
-
this
|
|
256
|
-
await this
|
|
262
|
+
this.#store ??= await KeyValueStore.open();
|
|
263
|
+
await this.#store.setValue(this.#persistStateKey, this.getState());
|
|
257
264
|
this.isStatePersisted = true;
|
|
258
265
|
}
|
|
259
266
|
catch (e) {
|
|
260
267
|
const err = e;
|
|
261
|
-
this
|
|
268
|
+
this.#log.exception(err, 'Attempted to persist state, but failed.');
|
|
262
269
|
}
|
|
263
270
|
}
|
|
264
271
|
/**
|
|
@@ -267,8 +274,8 @@ export class RequestList {
|
|
|
267
274
|
* leaking the listener (and the requests it retains) on the shared event manager.
|
|
268
275
|
*/
|
|
269
276
|
async teardown() {
|
|
270
|
-
serviceLocator.getEventManager().off(
|
|
271
|
-
if (this
|
|
277
|
+
serviceLocator.getEventManager().off(EventType.PERSIST_STATE, this.persistState);
|
|
278
|
+
if (this.#persistStateKey) {
|
|
272
279
|
await this.persistState();
|
|
273
280
|
}
|
|
274
281
|
}
|
|
@@ -279,8 +286,8 @@ export class RequestList {
|
|
|
279
286
|
*/
|
|
280
287
|
async persistRequests() {
|
|
281
288
|
const serializedRequests = await serializeArray(this.requests);
|
|
282
|
-
this
|
|
283
|
-
await this
|
|
289
|
+
this.#store ??= await KeyValueStore.open();
|
|
290
|
+
await this.#store.setValue(this.#persistRequestsKey, serializedRequests, { contentType: CONTENT_TYPE_BINARY });
|
|
284
291
|
this.areRequestsPersisted = true;
|
|
285
292
|
}
|
|
286
293
|
/**
|
|
@@ -303,7 +310,7 @@ export class RequestList {
|
|
|
303
310
|
}
|
|
304
311
|
const deleteFromInProgress = [];
|
|
305
312
|
state.inProgress.forEach((uniqueKey) => {
|
|
306
|
-
const index = this
|
|
313
|
+
const index = this.#uniqueKeyToIndex[uniqueKey];
|
|
307
314
|
if (typeof index !== 'number') {
|
|
308
315
|
throw new Error('The state object is not consistent with RequestList. Unknown uniqueKey is present in the state.');
|
|
309
316
|
}
|
|
@@ -311,7 +318,7 @@ export class RequestList {
|
|
|
311
318
|
deleteFromInProgress.push(uniqueKey);
|
|
312
319
|
}
|
|
313
320
|
});
|
|
314
|
-
this
|
|
321
|
+
this.#nextIndex = state.nextIndex;
|
|
315
322
|
this.inProgress = new Set(state.inProgress);
|
|
316
323
|
// WORKAROUND:
|
|
317
324
|
// It happened to some users that state object contained something like:
|
|
@@ -327,7 +334,7 @@ export class RequestList {
|
|
|
327
334
|
// As a workaround, we just remove all inProgress requests whose index >= nextIndex,
|
|
328
335
|
// since they will be crawled again.
|
|
329
336
|
if (deleteFromInProgress.length) {
|
|
330
|
-
this
|
|
337
|
+
this.#log.warning("RequestList's in-progress field is not consistent, skipping invalid in-progress entries", {
|
|
331
338
|
deleteFromInProgress,
|
|
332
339
|
});
|
|
333
340
|
for (const uniqueKey of deleteFromInProgress) {
|
|
@@ -335,7 +342,7 @@ export class RequestList {
|
|
|
335
342
|
}
|
|
336
343
|
}
|
|
337
344
|
// All in-progress requests were interrupted and need to be re-crawled.
|
|
338
|
-
this
|
|
345
|
+
this.#requestsToRetry = [...this.inProgress];
|
|
339
346
|
}
|
|
340
347
|
/**
|
|
341
348
|
* Attempts to load state and requests using the `RequestList` configuration
|
|
@@ -344,19 +351,19 @@ export class RequestList {
|
|
|
344
351
|
async loadStateAndPersistedRequests() {
|
|
345
352
|
let state;
|
|
346
353
|
let persistedRequests;
|
|
347
|
-
if (this
|
|
348
|
-
state = this
|
|
349
|
-
this
|
|
354
|
+
if (this.#initialState) {
|
|
355
|
+
state = this.#initialState;
|
|
356
|
+
this.#log.debug('Loaded state from options.state argument.');
|
|
350
357
|
}
|
|
351
|
-
else if (this
|
|
352
|
-
state = await this.getPersistedState(this
|
|
358
|
+
else if (this.#persistStateKey) {
|
|
359
|
+
state = await this.getPersistedState(this.#persistStateKey);
|
|
353
360
|
if (state)
|
|
354
|
-
this
|
|
361
|
+
this.#log.debug('Loaded state from key value store using the persistStateKey.');
|
|
355
362
|
}
|
|
356
|
-
if (this
|
|
357
|
-
persistedRequests = await this.getPersistedState(this
|
|
363
|
+
if (this.#persistRequestsKey) {
|
|
364
|
+
persistedRequests = await this.getPersistedState(this.#persistRequestsKey);
|
|
358
365
|
if (persistedRequests)
|
|
359
|
-
this
|
|
366
|
+
this.#log.debug('Loaded requests from key value store using the persistRequestsKey.');
|
|
360
367
|
}
|
|
361
368
|
return [state, persistedRequests];
|
|
362
369
|
}
|
|
@@ -367,8 +374,8 @@ export class RequestList {
|
|
|
367
374
|
getState() {
|
|
368
375
|
this.ensureIsInitialized();
|
|
369
376
|
return {
|
|
370
|
-
nextIndex: this
|
|
371
|
-
nextUniqueKey: this
|
|
377
|
+
nextIndex: this.#nextIndex,
|
|
378
|
+
nextUniqueKey: this.#nextIndex < this.requests.length ? this.requests[this.#nextIndex].uniqueKey : null,
|
|
372
379
|
inProgress: [...this.inProgress],
|
|
373
380
|
};
|
|
374
381
|
}
|
|
@@ -377,14 +384,14 @@ export class RequestList {
|
|
|
377
384
|
*/
|
|
378
385
|
async isEmpty() {
|
|
379
386
|
this.ensureIsInitialized();
|
|
380
|
-
return this
|
|
387
|
+
return this.#requestsToRetry.length === 0 && this.#nextIndex >= this.requests.length;
|
|
381
388
|
}
|
|
382
389
|
/**
|
|
383
390
|
* @inheritDoc
|
|
384
391
|
*/
|
|
385
392
|
async isFinished() {
|
|
386
393
|
this.ensureIsInitialized();
|
|
387
|
-
return this.inProgress.size === 0 && this
|
|
394
|
+
return this.inProgress.size === 0 && this.#nextIndex >= this.requests.length;
|
|
388
395
|
}
|
|
389
396
|
/**
|
|
390
397
|
* @inheritDoc
|
|
@@ -392,17 +399,17 @@ export class RequestList {
|
|
|
392
399
|
async fetchNextRequest() {
|
|
393
400
|
this.ensureIsInitialized();
|
|
394
401
|
// First re-serve any requests that were interrupted before the last state persist.
|
|
395
|
-
const uniqueKey = this
|
|
402
|
+
const uniqueKey = this.#requestsToRetry.shift();
|
|
396
403
|
if (uniqueKey) {
|
|
397
|
-
const index = this
|
|
404
|
+
const index = this.#uniqueKeyToIndex[uniqueKey];
|
|
398
405
|
return this.ensureRequest(this.requests[index], index);
|
|
399
406
|
}
|
|
400
407
|
// Otherwise return next request.
|
|
401
|
-
if (this
|
|
402
|
-
const index = this
|
|
408
|
+
if (this.#nextIndex < this.requests.length) {
|
|
409
|
+
const index = this.#nextIndex;
|
|
403
410
|
const request = this.requests[index];
|
|
404
411
|
this.inProgress.add(request.uniqueKey);
|
|
405
|
-
this
|
|
412
|
+
this.#nextIndex++;
|
|
406
413
|
this.isStatePersisted = false;
|
|
407
414
|
return this.ensureRequest(request, index);
|
|
408
415
|
}
|
|
@@ -446,7 +453,7 @@ export class RequestList {
|
|
|
446
453
|
fetchedRequests.forEach((request) => this.addRequest(request));
|
|
447
454
|
const fetchedCount = fetchedRequests.length;
|
|
448
455
|
const importedCount = this.requests.length - originalLength;
|
|
449
|
-
this
|
|
456
|
+
this.#log.info('Fetched and loaded Requests from a remote resource.', {
|
|
450
457
|
requestsFromUrl,
|
|
451
458
|
regex,
|
|
452
459
|
fetchedCount,
|
|
@@ -456,8 +463,8 @@ export class RequestList {
|
|
|
456
463
|
});
|
|
457
464
|
}
|
|
458
465
|
async getPersistedState(key) {
|
|
459
|
-
this
|
|
460
|
-
const state = await this
|
|
466
|
+
this.#store ??= await KeyValueStore.open();
|
|
467
|
+
const state = await this.#store.getValue(key);
|
|
461
468
|
return state;
|
|
462
469
|
}
|
|
463
470
|
/**
|
|
@@ -468,10 +475,10 @@ export class RequestList {
|
|
|
468
475
|
// Download remote resource and parse URLs.
|
|
469
476
|
let urlsArr;
|
|
470
477
|
try {
|
|
471
|
-
urlsArr = await this.
|
|
478
|
+
urlsArr = await this.downloadListOfUrls({
|
|
472
479
|
url: requestsFromUrl,
|
|
473
480
|
urlRegExp: regex,
|
|
474
|
-
proxyUrl: (await this
|
|
481
|
+
proxyUrl: (await this.#proxyConfiguration?.newProxyInfo())?.url,
|
|
475
482
|
});
|
|
476
483
|
}
|
|
477
484
|
catch (err) {
|
|
@@ -479,7 +486,7 @@ export class RequestList {
|
|
|
479
486
|
}
|
|
480
487
|
// Skip if resource contained no URLs.
|
|
481
488
|
if (!urlsArr.length) {
|
|
482
|
-
this
|
|
489
|
+
this.#log.warning('The fetched list contains no valid URLs.', { requestsFromUrl, regex });
|
|
483
490
|
return [];
|
|
484
491
|
}
|
|
485
492
|
return urlsArr.map((url) => ({ url, ...sharedOpts }));
|
|
@@ -507,18 +514,18 @@ export class RequestList {
|
|
|
507
514
|
const hasUniqueKey = Reflect.has(Object(source), 'uniqueKey');
|
|
508
515
|
request.uniqueKey ??= Request.computeUniqueKey(request);
|
|
509
516
|
// Add index to uniqueKey if duplicates are to be kept
|
|
510
|
-
if (this
|
|
517
|
+
if (this.#keepDuplicateUrls && !hasUniqueKey) {
|
|
511
518
|
request.uniqueKey += `-${this.requests.length}`;
|
|
512
519
|
}
|
|
513
520
|
const { uniqueKey } = request;
|
|
514
521
|
this.ensureUniqueKeyValid(uniqueKey);
|
|
515
522
|
// Skip requests with duplicate uniqueKey
|
|
516
|
-
if (!Object.hasOwn(this
|
|
517
|
-
this
|
|
523
|
+
if (!Object.hasOwn(this.#uniqueKeyToIndex, uniqueKey)) {
|
|
524
|
+
this.#uniqueKeyToIndex[uniqueKey] = this.requests.length;
|
|
518
525
|
this.requests.push(request);
|
|
519
526
|
}
|
|
520
|
-
else if (this
|
|
521
|
-
this
|
|
527
|
+
else if (this.#keepDuplicateUrls) {
|
|
528
|
+
this.#log.warning(`Duplicate uniqueKey: ${uniqueKey} found while the keepDuplicateUrls option was set. Check your sources' unique keys.`);
|
|
522
529
|
}
|
|
523
530
|
}
|
|
524
531
|
/**
|
|
@@ -542,7 +549,7 @@ export class RequestList {
|
|
|
542
549
|
* Throws an error if request list wasn't initialized.
|
|
543
550
|
*/
|
|
544
551
|
ensureIsInitialized() {
|
|
545
|
-
if (!this
|
|
552
|
+
if (!this.#isInitialized) {
|
|
546
553
|
throw new Error('RequestList is not initialized; you must call "await requestList.initialize()" before using it!');
|
|
547
554
|
}
|
|
548
555
|
}
|
|
@@ -558,7 +565,7 @@ export class RequestList {
|
|
|
558
565
|
*/
|
|
559
566
|
async getPendingCount() {
|
|
560
567
|
this.ensureIsInitialized();
|
|
561
|
-
return this.requests.length - (this
|
|
568
|
+
return this.requests.length - (this.#nextIndex - this.inProgress.size);
|
|
562
569
|
}
|
|
563
570
|
/**
|
|
564
571
|
* Combines this list with a request manager (a {@link RequestQueue} by default) into a
|
|
@@ -576,7 +583,7 @@ export class RequestList {
|
|
|
576
583
|
*/
|
|
577
584
|
async getHandledCount() {
|
|
578
585
|
this.ensureIsInitialized();
|
|
579
|
-
return this
|
|
586
|
+
return this.#nextIndex - this.inProgress.size;
|
|
580
587
|
}
|
|
581
588
|
/**
|
|
582
589
|
* Opens a request list and returns a promise resolving to an instance
|
|
@@ -639,9 +646,9 @@ export class RequestList {
|
|
|
639
646
|
return rl;
|
|
640
647
|
}
|
|
641
648
|
const listName = listNameOrOptions;
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
649
|
+
parseArgument(listName, listNameSchema);
|
|
650
|
+
parseArgument(sources, schemas.anyArray);
|
|
651
|
+
parseArgument(options, openOptionsSchema);
|
|
645
652
|
const rl = new RequestList({
|
|
646
653
|
...options,
|
|
647
654
|
persistStateKey: listName ? `${listName}-${STATE_PERSISTENCE_KEY}` : options.persistStateKey,
|
|
@@ -654,10 +661,10 @@ export class RequestList {
|
|
|
654
661
|
/**
|
|
655
662
|
* @internal wraps public utility for mocking purposes
|
|
656
663
|
*/
|
|
657
|
-
async
|
|
664
|
+
async downloadListOfUrls(options) {
|
|
658
665
|
return downloadListOfUrls({
|
|
659
666
|
...options,
|
|
660
|
-
httpClient: this
|
|
667
|
+
httpClient: this.#httpClient,
|
|
661
668
|
});
|
|
662
669
|
}
|
|
663
670
|
}
|
|
@@ -54,6 +54,11 @@ export interface IRequestLoader {
|
|
|
54
54
|
* Resolves to `true` if the next call to {@link IRequestLoader.fetchNextRequest} function
|
|
55
55
|
* would return `null`, otherwise it resolves to `false`.
|
|
56
56
|
* Note that even if the loader is empty, there might be some pending requests currently being processed.
|
|
57
|
+
*
|
|
58
|
+
* This is a statement about what the *next fetch* would return, not about how much work is left, so it
|
|
59
|
+
* may report `true` while {@link IRequestLoader.getPendingCount} is non-zero - a loader that withholds
|
|
60
|
+
* requests for a while (as {@link ThrottlingRequestManager} does for a rate-limited domain) is empty
|
|
61
|
+
* for as long as it will not hand anything over. Use `isFinished()` to ask whether the work is done.
|
|
57
62
|
*/
|
|
58
63
|
isEmpty(): Promise<boolean>;
|
|
59
64
|
/**
|
|
@@ -9,16 +9,7 @@ import type { AddRequestsBatchedOptions, AddRequestsBatchedResult, RequestQueueO
|
|
|
9
9
|
* It first reads requests from the loader and then, when needed, transfers them in batches to the manager.
|
|
10
10
|
*/
|
|
11
11
|
export declare class RequestManagerTandem implements IRequestManager {
|
|
12
|
-
private
|
|
13
|
-
private requestLoader;
|
|
14
|
-
private requestManagerPromise?;
|
|
15
|
-
private resolvedRequestManager?;
|
|
16
|
-
private requestManagerFactory;
|
|
17
|
-
/**
|
|
18
|
-
* The latest expected request-processing time hinted via {@link setExpectedRequestProcessingTimeSecs}.
|
|
19
|
-
* Remembered so it can be applied to the writable manager once it is lazily resolved.
|
|
20
|
-
*/
|
|
21
|
-
private expectedRequestProcessingSecs?;
|
|
12
|
+
#private;
|
|
22
13
|
/**
|
|
23
14
|
* @param requestLoader The read-only loader to read requests from first.
|
|
24
15
|
* @param requestManager The writable manager to transfer requests into and enqueue new ones. May be passed as a
|
|
@@ -5,16 +5,16 @@ import { serviceLocator } from '../service_locator.js';
|
|
|
5
5
|
* It first reads requests from the loader and then, when needed, transfers them in batches to the manager.
|
|
6
6
|
*/
|
|
7
7
|
export class RequestManagerTandem {
|
|
8
|
-
log;
|
|
9
|
-
requestLoader;
|
|
10
|
-
requestManagerPromise;
|
|
11
|
-
resolvedRequestManager;
|
|
12
|
-
requestManagerFactory;
|
|
8
|
+
#log;
|
|
9
|
+
#requestLoader;
|
|
10
|
+
#requestManagerPromise;
|
|
11
|
+
#resolvedRequestManager;
|
|
12
|
+
#requestManagerFactory;
|
|
13
13
|
/**
|
|
14
14
|
* The latest expected request-processing time hinted via {@link setExpectedRequestProcessingTimeSecs}.
|
|
15
15
|
* Remembered so it can be applied to the writable manager once it is lazily resolved.
|
|
16
16
|
*/
|
|
17
|
-
expectedRequestProcessingSecs;
|
|
17
|
+
#expectedRequestProcessingSecs;
|
|
18
18
|
/**
|
|
19
19
|
* @param requestLoader The read-only loader to read requests from first.
|
|
20
20
|
* @param requestManager The writable manager to transfer requests into and enqueue new ones. May be passed as a
|
|
@@ -22,24 +22,24 @@ export class RequestManagerTandem {
|
|
|
22
22
|
* (e.g. a lazily-opened default {@link RequestQueue}).
|
|
23
23
|
*/
|
|
24
24
|
constructor(requestLoader, requestManager) {
|
|
25
|
-
this
|
|
26
|
-
this
|
|
27
|
-
this
|
|
25
|
+
this.#log = serviceLocator.getLogger().child({ prefix: 'RequestManagerTandem' });
|
|
26
|
+
this.#requestLoader = requestLoader;
|
|
27
|
+
this.#requestManagerFactory = typeof requestManager === 'function' ? requestManager : () => requestManager;
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
30
|
* Resolves the writable request manager, opening it lazily (via the factory) on first use and memoizing the result.
|
|
31
31
|
* @private
|
|
32
32
|
*/
|
|
33
33
|
async getRequestManager() {
|
|
34
|
-
if (this
|
|
35
|
-
this
|
|
36
|
-
this
|
|
34
|
+
if (this.#resolvedRequestManager === undefined) {
|
|
35
|
+
this.#requestManagerPromise ??= Promise.resolve(this.#requestManagerFactory());
|
|
36
|
+
this.#resolvedRequestManager = await this.#requestManagerPromise;
|
|
37
37
|
// Apply any hint received before the manager was resolved.
|
|
38
|
-
if (this
|
|
39
|
-
await this
|
|
38
|
+
if (this.#expectedRequestProcessingSecs !== undefined) {
|
|
39
|
+
await this.#resolvedRequestManager.setExpectedRequestProcessingTimeSecs?.(this.#expectedRequestProcessingSecs);
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
-
return this
|
|
42
|
+
return this.#resolvedRequestManager;
|
|
43
43
|
}
|
|
44
44
|
/**
|
|
45
45
|
* Transfers a single request from the read-only loader to the writable manager.
|
|
@@ -50,7 +50,7 @@ export class RequestManagerTandem {
|
|
|
50
50
|
* @private
|
|
51
51
|
*/
|
|
52
52
|
async transferNextRequestToQueue() {
|
|
53
|
-
const request = await this
|
|
53
|
+
const request = await this.#requestLoader.fetchNextRequest();
|
|
54
54
|
if (request === null) {
|
|
55
55
|
return true;
|
|
56
56
|
}
|
|
@@ -60,12 +60,12 @@ export class RequestManagerTandem {
|
|
|
60
60
|
return true;
|
|
61
61
|
}
|
|
62
62
|
catch (error) {
|
|
63
|
-
this
|
|
63
|
+
this.#log.exception(error, 'Adding request from the RequestLoader to the RequestManager failed, the request has been dropped.', { url: request.url, uniqueKey: request.uniqueKey });
|
|
64
64
|
return false;
|
|
65
65
|
}
|
|
66
66
|
finally {
|
|
67
67
|
// Mark it as handled so that the request doesn't get stuck in the `inProgress` state in the loader.
|
|
68
|
-
await this
|
|
68
|
+
await this.#requestLoader.markRequestAsHandled(request);
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
/**
|
|
@@ -76,8 +76,8 @@ export class RequestManagerTandem {
|
|
|
76
76
|
async fetchNextRequest() {
|
|
77
77
|
// First, try to transfer a request from the requestList
|
|
78
78
|
const [listEmpty, listFinished] = await Promise.all([
|
|
79
|
-
this
|
|
80
|
-
this
|
|
79
|
+
this.#requestLoader.isEmpty(),
|
|
80
|
+
this.#requestLoader.isFinished(),
|
|
81
81
|
]);
|
|
82
82
|
if (!listEmpty && !listFinished) {
|
|
83
83
|
// If the transfer failed, the request was dropped; don't fetch from the manager this round (matching
|
|
@@ -94,7 +94,7 @@ export class RequestManagerTandem {
|
|
|
94
94
|
*/
|
|
95
95
|
async isFinished() {
|
|
96
96
|
const requestManager = await this.getRequestManager();
|
|
97
|
-
const storagesFinished = await Promise.all([this
|
|
97
|
+
const storagesFinished = await Promise.all([this.#requestLoader.isFinished(), requestManager.isFinished()]);
|
|
98
98
|
return storagesFinished.every(Boolean);
|
|
99
99
|
}
|
|
100
100
|
/**
|
|
@@ -102,7 +102,7 @@ export class RequestManagerTandem {
|
|
|
102
102
|
*/
|
|
103
103
|
async isEmpty() {
|
|
104
104
|
const requestManager = await this.getRequestManager();
|
|
105
|
-
const storagesEmpty = await Promise.all([this
|
|
105
|
+
const storagesEmpty = await Promise.all([this.#requestLoader.isEmpty(), requestManager.isEmpty()]);
|
|
106
106
|
return storagesEmpty.every(Boolean);
|
|
107
107
|
}
|
|
108
108
|
/**
|
|
@@ -120,7 +120,7 @@ export class RequestManagerTandem {
|
|
|
120
120
|
const [managerTotal, loaderTotal] = await Promise.all([
|
|
121
121
|
requestManager.getTotalCount(),
|
|
122
122
|
// count only pending to avoid double counting, requests marked as "handled" have been moved to requestManager
|
|
123
|
-
this
|
|
123
|
+
this.#requestLoader.getPendingCount(),
|
|
124
124
|
]);
|
|
125
125
|
return managerTotal + loaderTotal;
|
|
126
126
|
}
|
|
@@ -131,7 +131,7 @@ export class RequestManagerTandem {
|
|
|
131
131
|
const requestManager = await this.getRequestManager();
|
|
132
132
|
const [managerPending, loaderPending] = await Promise.all([
|
|
133
133
|
requestManager.getPendingCount(),
|
|
134
|
-
this
|
|
134
|
+
this.#requestLoader.getPendingCount(),
|
|
135
135
|
]);
|
|
136
136
|
return managerPending + loaderPending;
|
|
137
137
|
}
|
|
@@ -175,7 +175,7 @@ export class RequestManagerTandem {
|
|
|
175
175
|
* @inheritdoc
|
|
176
176
|
*/
|
|
177
177
|
async persistState() {
|
|
178
|
-
await this
|
|
178
|
+
await this.#requestLoader.persistState?.();
|
|
179
179
|
}
|
|
180
180
|
/**
|
|
181
181
|
* Purges the writable request manager so the tandem can be reused (e.g. across repeated `crawler.run()` calls).
|
|
@@ -191,7 +191,7 @@ export class RequestManagerTandem {
|
|
|
191
191
|
* @inheritdoc
|
|
192
192
|
*/
|
|
193
193
|
async setExpectedRequestProcessingTimeSecs(secs) {
|
|
194
|
-
this
|
|
195
|
-
await this
|
|
194
|
+
this.#expectedRequestProcessingSecs = secs;
|
|
195
|
+
await this.#resolvedRequestManager?.setExpectedRequestProcessingTimeSecs?.(secs);
|
|
196
196
|
}
|
|
197
197
|
}
|