@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
package/storages/request_list.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { downloadListOfUrls } from '@crawlee/utils';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import {
|
|
3
|
+
import { convertStateSync, RecoverableState } from '../recoverable_state.js';
|
|
4
4
|
import { Request } from '../request.js';
|
|
5
5
|
import { createDeserialize, serializeArray } from '../serialization.js';
|
|
6
6
|
import { serviceLocator } from '../service_locator.js';
|
|
@@ -12,24 +12,87 @@ export const STATE_PERSISTENCE_KEY = 'REQUEST_LIST_STATE';
|
|
|
12
12
|
/** @internal */
|
|
13
13
|
export const REQUESTS_PERSISTENCE_KEY = 'REQUEST_LIST_REQUESTS';
|
|
14
14
|
const CONTENT_TYPE_BINARY = 'application/octet-stream';
|
|
15
|
+
const persistedRequestListState = z.object({
|
|
16
|
+
nextIndex: z.number().int().nonnegative(),
|
|
17
|
+
nextUniqueKey: z.string().nullable(),
|
|
18
|
+
inProgress: z.array(z.string()),
|
|
19
|
+
});
|
|
20
|
+
const requestListInternalState = z.object({
|
|
21
|
+
/** Position of the next request to be processed. */
|
|
22
|
+
nextIndex: z.number().int().nonnegative(),
|
|
23
|
+
/** `uniqueKey`s of the requests being processed at the moment. */
|
|
24
|
+
inProgress: z.set(z.string()),
|
|
25
|
+
});
|
|
15
26
|
const requestListOptionsSchema = z.strictObject({
|
|
16
27
|
sources: schemas.anyArray.optional(), // check only for array and not subtypes to avoid iteration over the whole thing
|
|
17
28
|
sourcesFunction: schemas.anyFunction.optional(),
|
|
18
29
|
persistStateKey: z.string().optional(),
|
|
19
30
|
persistRequestsKey: z.string().optional(),
|
|
20
|
-
state:
|
|
21
|
-
.strictObject({
|
|
22
|
-
nextIndex: schemas.anyNumber,
|
|
23
|
-
nextUniqueKey: z.string(),
|
|
24
|
-
inProgress: schemas.anyObject, // persisted as an array of unique keys
|
|
25
|
-
})
|
|
26
|
-
.optional(),
|
|
31
|
+
state: persistedRequestListState.optional(),
|
|
27
32
|
keepDuplicateUrls: z.boolean().default(false),
|
|
28
33
|
proxyConfiguration: validators.proxyConfiguration.optional(),
|
|
29
34
|
httpClient: schemas.httpClient.optional(),
|
|
30
35
|
});
|
|
31
36
|
const listNameSchema = z.string().nullish();
|
|
32
37
|
const openOptionsSchema = z.looseObject({});
|
|
38
|
+
/**
|
|
39
|
+
* The conversion between the crawling position and its persisted record. Decoding also checks the record against
|
|
40
|
+
* the loaded requests, which is why the codec is built per list.
|
|
41
|
+
*/
|
|
42
|
+
function buildRequestListStateCodec(list) {
|
|
43
|
+
return z.codec(persistedRequestListState, requestListInternalState, {
|
|
44
|
+
decode: (record, ctx) => {
|
|
45
|
+
const { requests } = list;
|
|
46
|
+
const { nextIndex, nextUniqueKey } = record;
|
|
47
|
+
const inconsistent = (message) => {
|
|
48
|
+
ctx.issues.push({ code: 'custom', message, input: record });
|
|
49
|
+
return z.NEVER;
|
|
50
|
+
};
|
|
51
|
+
if (nextIndex > requests.length) {
|
|
52
|
+
return inconsistent('The state object is not consistent with RequestList, too few requests loaded.');
|
|
53
|
+
}
|
|
54
|
+
if (nextIndex < requests.length && requests[nextIndex].uniqueKey !== nextUniqueKey) {
|
|
55
|
+
return inconsistent('The state object is not consistent with RequestList the order of URLs seems to have changed.');
|
|
56
|
+
}
|
|
57
|
+
const inProgress = new Set(record.inProgress);
|
|
58
|
+
const deleteFromInProgress = [];
|
|
59
|
+
for (const uniqueKey of inProgress) {
|
|
60
|
+
const index = list.indexOf(uniqueKey);
|
|
61
|
+
if (index === undefined) {
|
|
62
|
+
return inconsistent('The state object is not consistent with RequestList. Unknown uniqueKey is present in the state.');
|
|
63
|
+
}
|
|
64
|
+
if (index >= nextIndex) {
|
|
65
|
+
deleteFromInProgress.push(uniqueKey);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// WORKAROUND:
|
|
69
|
+
// It happened to some users that state object contained something like:
|
|
70
|
+
// {
|
|
71
|
+
// "nextIndex": 11308,
|
|
72
|
+
// "nextUniqueKey": "https://www.anychart.com",
|
|
73
|
+
// "inProgress": {
|
|
74
|
+
// "https://www.ams360.com": true,
|
|
75
|
+
// ...
|
|
76
|
+
// "https://www.anychart.com": true,
|
|
77
|
+
// }
|
|
78
|
+
// Which then caused error "The request is not being processed (uniqueKey: https://www.anychart.com)"
|
|
79
|
+
// As a workaround, we just remove all inProgress requests whose index >= nextIndex,
|
|
80
|
+
// since they will be crawled again.
|
|
81
|
+
if (deleteFromInProgress.length) {
|
|
82
|
+
list.log.warning("RequestList's in-progress field is not consistent, skipping invalid in-progress entries", { deleteFromInProgress });
|
|
83
|
+
for (const uniqueKey of deleteFromInProgress) {
|
|
84
|
+
inProgress.delete(uniqueKey);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { nextIndex, inProgress };
|
|
88
|
+
},
|
|
89
|
+
encode: ({ nextIndex, inProgress }) => ({
|
|
90
|
+
nextIndex,
|
|
91
|
+
nextUniqueKey: nextIndex < list.requests.length ? list.requests[nextIndex].uniqueKey : null,
|
|
92
|
+
inProgress: [...inProgress],
|
|
93
|
+
}),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
33
96
|
/**
|
|
34
97
|
* Represents a static list of URLs to crawl.
|
|
35
98
|
* The URLs can be provided either in code or parsed from a text file hosted on the web.
|
|
@@ -99,26 +162,27 @@ export class RequestList {
|
|
|
99
162
|
* @internal
|
|
100
163
|
*/
|
|
101
164
|
requests = [];
|
|
102
|
-
/** Index to the next item in requests array to fetch. All previous requests are either handled or in progress. */
|
|
103
|
-
#nextIndex = 0;
|
|
104
165
|
/** Dictionary, key is Request.uniqueKey, value is corresponding index in the requests array. */
|
|
105
166
|
#uniqueKeyToIndex = {};
|
|
167
|
+
/**
|
|
168
|
+
* The crawling position, kept in a {@link RecoverableState} that handles the loading, validation and
|
|
169
|
+
* periodic persistence of the record under `persistStateKey`.
|
|
170
|
+
*/
|
|
171
|
+
#state;
|
|
172
|
+
#stateCodec;
|
|
106
173
|
/**
|
|
107
174
|
* Set of `uniqueKey`s of requests that were returned by fetchNextRequest().
|
|
108
175
|
* @internal
|
|
109
176
|
*/
|
|
110
|
-
inProgress
|
|
177
|
+
get inProgress() {
|
|
178
|
+
return this.#state.currentValue.inProgress;
|
|
179
|
+
}
|
|
111
180
|
/**
|
|
112
181
|
* `uniqueKey`s of requests that were in progress when the state was last persisted and thus need to be
|
|
113
182
|
* re-crawled after a restart. They are served before advancing through the rest of the sources.
|
|
114
183
|
* @internal
|
|
115
184
|
*/
|
|
116
185
|
#requestsToRetry = [];
|
|
117
|
-
/**
|
|
118
|
-
* Starts as true because until we handle the first request, the list is effectively persisted by doing nothing.
|
|
119
|
-
* @internal
|
|
120
|
-
*/
|
|
121
|
-
isStatePersisted = true;
|
|
122
186
|
/**
|
|
123
187
|
* Starts as false because we don't know yet and sources might change in the meantime (eg. download from live list).
|
|
124
188
|
* @internal
|
|
@@ -128,11 +192,9 @@ export class RequestList {
|
|
|
128
192
|
#isInitialized = false;
|
|
129
193
|
#persistStateKey;
|
|
130
194
|
#persistRequestsKey;
|
|
131
|
-
#initialState;
|
|
132
195
|
#store;
|
|
133
196
|
#keepDuplicateUrls;
|
|
134
|
-
|
|
135
|
-
sources;
|
|
197
|
+
#sources;
|
|
136
198
|
#sourcesFunction;
|
|
137
199
|
#proxyConfiguration;
|
|
138
200
|
#httpClient;
|
|
@@ -148,43 +210,69 @@ export class RequestList {
|
|
|
148
210
|
}
|
|
149
211
|
this.#persistStateKey = persistStateKey ? `CRAWLEE_${persistStateKey}` : persistStateKey;
|
|
150
212
|
this.#persistRequestsKey = persistRequestsKey ? `CRAWLEE_${persistRequestsKey}` : persistRequestsKey;
|
|
151
|
-
this.#initialState = state;
|
|
152
213
|
this.#httpClient = httpClient;
|
|
214
|
+
this.#stateCodec = buildRequestListStateCodec({
|
|
215
|
+
requests: this.requests,
|
|
216
|
+
indexOf: (uniqueKey) => {
|
|
217
|
+
const index = this.#uniqueKeyToIndex[uniqueKey];
|
|
218
|
+
return typeof index === 'number' ? index : undefined;
|
|
219
|
+
},
|
|
220
|
+
log: this.#log,
|
|
221
|
+
});
|
|
222
|
+
const stateKey = this.#persistStateKey ?? STATE_PERSISTENCE_KEY;
|
|
223
|
+
this.#state = new RecoverableState({
|
|
224
|
+
// The `state` option is where the list starts when there is no persisted record to restore. The factory
|
|
225
|
+
// runs on `initialize()`, once the requests it is validated against are loaded.
|
|
226
|
+
defaultState: () => state
|
|
227
|
+
? convertStateSync(this.#stateCodec, state, stateKey)
|
|
228
|
+
: { nextIndex: 0, inProgress: new Set() },
|
|
229
|
+
persistStateKey: stateKey,
|
|
230
|
+
persistenceEnabled: !!this.#persistStateKey,
|
|
231
|
+
logger: this.#log,
|
|
232
|
+
// The codec validates in the decode direction, so it is a Standard Schema as-is; encoding needs a call.
|
|
233
|
+
deserialize: this.#stateCodec,
|
|
234
|
+
serialize: (internalState) => this.#stateCodec.encode(internalState),
|
|
235
|
+
});
|
|
153
236
|
// If this option is set then all requests will get a pre-generated unique ID and duplicate URLs will be kept in the list.
|
|
154
237
|
this.#keepDuplicateUrls = keepDuplicateUrls;
|
|
155
238
|
// Will be empty after initialization to save memory.
|
|
156
|
-
this
|
|
239
|
+
this.#sources = sources ? [...sources] : [];
|
|
157
240
|
this.#sourcesFunction = sourcesFunction;
|
|
158
241
|
// The proxy configuration used for `requestsFromUrl` requests.
|
|
159
242
|
this.#proxyConfiguration = proxyConfiguration;
|
|
160
|
-
this.persistState = this.persistState.bind(this);
|
|
161
243
|
}
|
|
162
244
|
/**
|
|
163
245
|
* Loads all remote sources of URLs and potentially starts periodic state persistence.
|
|
164
246
|
* This function must be called before you can start using the instance in a meaningful way.
|
|
165
247
|
*/
|
|
166
|
-
async initialize() {
|
|
248
|
+
async #initialize() {
|
|
167
249
|
if (this.#isLoading) {
|
|
168
250
|
throw new Error('RequestList sources are already loading or were loaded.');
|
|
169
251
|
}
|
|
170
252
|
this.#isLoading = true;
|
|
171
253
|
await purgeDefaultStorages({ onlyPurgeOnce: true });
|
|
172
|
-
|
|
254
|
+
let persistedRequests;
|
|
255
|
+
if (this.#persistRequestsKey) {
|
|
256
|
+
persistedRequests = await this.#getPersistedState(this.#persistRequestsKey);
|
|
257
|
+
if (persistedRequests)
|
|
258
|
+
this.#log.debug('Loaded requests from key value store using the persistRequestsKey.');
|
|
259
|
+
}
|
|
173
260
|
// Add persisted requests / new sources in a memory efficient way because with very
|
|
174
261
|
// large lists, we were running out of memory.
|
|
175
262
|
if (persistedRequests) {
|
|
176
|
-
await this
|
|
263
|
+
await this.#addPersistedRequests(persistedRequests);
|
|
177
264
|
}
|
|
178
265
|
else {
|
|
179
|
-
await this
|
|
266
|
+
await this.#addRequestsFromSources();
|
|
180
267
|
}
|
|
181
|
-
|
|
268
|
+
// Restores the crawling position - the requests have to be loaded first, as the record is validated
|
|
269
|
+
// against them. Also starts the periodic state persistence when `persistStateKey` is set.
|
|
270
|
+
await this.#state.initialize();
|
|
271
|
+
// All in-progress requests were interrupted and need to be re-crawled.
|
|
272
|
+
this.#requestsToRetry = [...this.inProgress];
|
|
182
273
|
this.#isInitialized = true;
|
|
183
274
|
if (this.#persistRequestsKey && !this.areRequestsPersisted)
|
|
184
|
-
await this
|
|
185
|
-
if (this.#persistStateKey) {
|
|
186
|
-
serviceLocator.getEventManager().on(EventType.PERSIST_STATE, this.persistState);
|
|
187
|
-
}
|
|
275
|
+
await this.#persistRequests();
|
|
188
276
|
return this;
|
|
189
277
|
}
|
|
190
278
|
/**
|
|
@@ -192,18 +280,18 @@ export class RequestList {
|
|
|
192
280
|
* This needs to be done in a memory efficient way. We should update the input
|
|
193
281
|
* to a Stream once apify-client supports streams.
|
|
194
282
|
*/
|
|
195
|
-
async addPersistedRequests(persistedRequests) {
|
|
283
|
+
async #addPersistedRequests(persistedRequests) {
|
|
196
284
|
// We don't need the sources so we purge them to
|
|
197
285
|
// prevent them from hanging in memory.
|
|
198
|
-
for (let i = 0; i < this
|
|
286
|
+
for (let i = 0; i < this.#sources.length; i++) {
|
|
199
287
|
// oxlint-disable-next-line typescript/no-array-delete -- intentional, drop the slot so V8 can collect the object
|
|
200
|
-
delete this
|
|
288
|
+
delete this.#sources[i];
|
|
201
289
|
}
|
|
202
|
-
this
|
|
290
|
+
this.#sources = [];
|
|
203
291
|
this.areRequestsPersisted = true;
|
|
204
292
|
const requestStream = createDeserialize(persistedRequests);
|
|
205
293
|
for await (const request of requestStream) {
|
|
206
|
-
this
|
|
294
|
+
this.#addRequest(request);
|
|
207
295
|
}
|
|
208
296
|
}
|
|
209
297
|
/**
|
|
@@ -212,25 +300,25 @@ export class RequestList {
|
|
|
212
300
|
* We need to avoid keeping both sources and requests in memory
|
|
213
301
|
* to reduce memory footprint with very large sources.
|
|
214
302
|
*/
|
|
215
|
-
async addRequestsFromSources() {
|
|
303
|
+
async #addRequestsFromSources() {
|
|
216
304
|
// We'll load all sources in sequence to ensure that they get loaded in the right order.
|
|
217
|
-
const sourcesCount = this
|
|
305
|
+
const sourcesCount = this.#sources.length;
|
|
218
306
|
for (let i = 0; i < sourcesCount; i++) {
|
|
219
|
-
const source = this
|
|
307
|
+
const source = this.#sources[i];
|
|
220
308
|
// Using delete here to drop the original object ASAP to free memory
|
|
221
309
|
// .pop would reverse the array and .shift is SLOW.
|
|
222
310
|
// oxlint-disable-next-line typescript/no-array-delete
|
|
223
|
-
delete this
|
|
311
|
+
delete this.#sources[i];
|
|
224
312
|
if (typeof source === 'object' && source.requestsFromUrl) {
|
|
225
|
-
const fetchedRequests = await this
|
|
226
|
-
await this
|
|
313
|
+
const fetchedRequests = await this.#fetchRequestsFromUrl(source);
|
|
314
|
+
await this.#addFetchedRequests(source, fetchedRequests);
|
|
227
315
|
}
|
|
228
316
|
else {
|
|
229
|
-
this
|
|
317
|
+
this.#addRequest(source);
|
|
230
318
|
}
|
|
231
319
|
}
|
|
232
320
|
// Drop the original array full of empty indexes.
|
|
233
|
-
this
|
|
321
|
+
this.#sources = [];
|
|
234
322
|
if (this.#sourcesFunction) {
|
|
235
323
|
try {
|
|
236
324
|
const sourcesFromFunction = await this.#sourcesFunction();
|
|
@@ -239,7 +327,7 @@ export class RequestList {
|
|
|
239
327
|
const source = sourcesFromFunction[i];
|
|
240
328
|
// oxlint-disable-next-line typescript/no-array-delete -- intentional, drop the slot so V8 can collect the object
|
|
241
329
|
delete sourcesFromFunction[i];
|
|
242
|
-
this
|
|
330
|
+
this.#addRequest(source);
|
|
243
331
|
}
|
|
244
332
|
sourcesFromFunction.length = 0;
|
|
245
333
|
}
|
|
@@ -256,12 +344,8 @@ export class RequestList {
|
|
|
256
344
|
if (!this.#persistStateKey) {
|
|
257
345
|
throw new Error('Cannot persist state. options.persistStateKey is not set.');
|
|
258
346
|
}
|
|
259
|
-
if (this.isStatePersisted)
|
|
260
|
-
return;
|
|
261
347
|
try {
|
|
262
|
-
this.#
|
|
263
|
-
await this.#store.setValue(this.#persistStateKey, this.getState());
|
|
264
|
-
this.isStatePersisted = true;
|
|
348
|
+
await this.#state.persistState();
|
|
265
349
|
}
|
|
266
350
|
catch (e) {
|
|
267
351
|
const err = e;
|
|
@@ -274,144 +358,57 @@ export class RequestList {
|
|
|
274
358
|
* leaking the listener (and the requests it retains) on the shared event manager.
|
|
275
359
|
*/
|
|
276
360
|
async teardown() {
|
|
277
|
-
|
|
278
|
-
if (this.#persistStateKey) {
|
|
279
|
-
await this.persistState();
|
|
280
|
-
}
|
|
361
|
+
await this.#state.teardown();
|
|
281
362
|
}
|
|
282
363
|
/**
|
|
283
364
|
* Unlike persistState(), this is used only internally, since the sources
|
|
284
365
|
* are automatically persisted at RequestList initialization (if the persistRequestsKey is set),
|
|
285
366
|
* but there's no reason to persist it again afterwards, because RequestList is immutable.
|
|
286
367
|
*/
|
|
287
|
-
async persistRequests() {
|
|
368
|
+
async #persistRequests() {
|
|
288
369
|
const serializedRequests = await serializeArray(this.requests);
|
|
289
370
|
this.#store ??= await KeyValueStore.open();
|
|
290
371
|
await this.#store.setValue(this.#persistRequestsKey, serializedRequests, { contentType: CONTENT_TYPE_BINARY });
|
|
291
372
|
this.areRequestsPersisted = true;
|
|
292
373
|
}
|
|
293
|
-
/**
|
|
294
|
-
* Restores RequestList state from a state object.
|
|
295
|
-
*/
|
|
296
|
-
restoreState(state) {
|
|
297
|
-
// If there's no state it means we've not persisted any (yet).
|
|
298
|
-
if (!state)
|
|
299
|
-
return;
|
|
300
|
-
// Restore previous state.
|
|
301
|
-
if (typeof state.nextIndex !== 'number' || state.nextIndex < 0) {
|
|
302
|
-
throw new Error('The state object is invalid: nextIndex must be a non-negative number.');
|
|
303
|
-
}
|
|
304
|
-
if (state.nextIndex > this.requests.length) {
|
|
305
|
-
throw new Error('The state object is not consistent with RequestList, too few requests loaded.');
|
|
306
|
-
}
|
|
307
|
-
if (state.nextIndex < this.requests.length &&
|
|
308
|
-
this.requests[state.nextIndex].uniqueKey !== state.nextUniqueKey) {
|
|
309
|
-
throw new Error('The state object is not consistent with RequestList the order of URLs seems to have changed.');
|
|
310
|
-
}
|
|
311
|
-
const deleteFromInProgress = [];
|
|
312
|
-
state.inProgress.forEach((uniqueKey) => {
|
|
313
|
-
const index = this.#uniqueKeyToIndex[uniqueKey];
|
|
314
|
-
if (typeof index !== 'number') {
|
|
315
|
-
throw new Error('The state object is not consistent with RequestList. Unknown uniqueKey is present in the state.');
|
|
316
|
-
}
|
|
317
|
-
if (index >= state.nextIndex) {
|
|
318
|
-
deleteFromInProgress.push(uniqueKey);
|
|
319
|
-
}
|
|
320
|
-
});
|
|
321
|
-
this.#nextIndex = state.nextIndex;
|
|
322
|
-
this.inProgress = new Set(state.inProgress);
|
|
323
|
-
// WORKAROUND:
|
|
324
|
-
// It happened to some users that state object contained something like:
|
|
325
|
-
// {
|
|
326
|
-
// "nextIndex": 11308,
|
|
327
|
-
// "nextUniqueKey": "https://www.anychart.com",
|
|
328
|
-
// "inProgress": {
|
|
329
|
-
// "https://www.ams360.com": true,
|
|
330
|
-
// ...
|
|
331
|
-
// "https://www.anychart.com": true,
|
|
332
|
-
// }
|
|
333
|
-
// Which then caused error "The request is not being processed (uniqueKey: https://www.anychart.com)"
|
|
334
|
-
// As a workaround, we just remove all inProgress requests whose index >= nextIndex,
|
|
335
|
-
// since they will be crawled again.
|
|
336
|
-
if (deleteFromInProgress.length) {
|
|
337
|
-
this.#log.warning("RequestList's in-progress field is not consistent, skipping invalid in-progress entries", {
|
|
338
|
-
deleteFromInProgress,
|
|
339
|
-
});
|
|
340
|
-
for (const uniqueKey of deleteFromInProgress) {
|
|
341
|
-
this.inProgress.delete(uniqueKey);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
// All in-progress requests were interrupted and need to be re-crawled.
|
|
345
|
-
this.#requestsToRetry = [...this.inProgress];
|
|
346
|
-
}
|
|
347
|
-
/**
|
|
348
|
-
* Attempts to load state and requests using the `RequestList` configuration
|
|
349
|
-
* and returns a tuple of [state, requests] where each may be null if not loaded.
|
|
350
|
-
*/
|
|
351
|
-
async loadStateAndPersistedRequests() {
|
|
352
|
-
let state;
|
|
353
|
-
let persistedRequests;
|
|
354
|
-
if (this.#initialState) {
|
|
355
|
-
state = this.#initialState;
|
|
356
|
-
this.#log.debug('Loaded state from options.state argument.');
|
|
357
|
-
}
|
|
358
|
-
else if (this.#persistStateKey) {
|
|
359
|
-
state = await this.getPersistedState(this.#persistStateKey);
|
|
360
|
-
if (state)
|
|
361
|
-
this.#log.debug('Loaded state from key value store using the persistStateKey.');
|
|
362
|
-
}
|
|
363
|
-
if (this.#persistRequestsKey) {
|
|
364
|
-
persistedRequests = await this.getPersistedState(this.#persistRequestsKey);
|
|
365
|
-
if (persistedRequests)
|
|
366
|
-
this.#log.debug('Loaded requests from key value store using the persistRequestsKey.');
|
|
367
|
-
}
|
|
368
|
-
return [state, persistedRequests];
|
|
369
|
-
}
|
|
370
374
|
/**
|
|
371
375
|
* Returns an object representing the internal state of the `RequestList` instance.
|
|
372
376
|
* Note that the object's fields can change in future releases.
|
|
373
377
|
*/
|
|
374
378
|
getState() {
|
|
375
|
-
this
|
|
376
|
-
return
|
|
377
|
-
nextIndex: this.#nextIndex,
|
|
378
|
-
nextUniqueKey: this.#nextIndex < this.requests.length ? this.requests[this.#nextIndex].uniqueKey : null,
|
|
379
|
-
inProgress: [...this.inProgress],
|
|
380
|
-
};
|
|
379
|
+
this.#ensureIsInitialized();
|
|
380
|
+
return this.#stateCodec.encode(this.#state.currentValue);
|
|
381
381
|
}
|
|
382
382
|
/**
|
|
383
383
|
* @inheritDoc
|
|
384
384
|
*/
|
|
385
|
-
async
|
|
386
|
-
this
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
async isFinished() {
|
|
393
|
-
this.ensureIsInitialized();
|
|
394
|
-
return this.inProgress.size === 0 && this.#nextIndex >= this.requests.length;
|
|
385
|
+
async checkReadiness() {
|
|
386
|
+
this.#ensureIsInitialized();
|
|
387
|
+
if (this.#requestsToRetry.length > 0 || this.#state.currentValue.nextIndex < this.requests.length) {
|
|
388
|
+
return { status: 'ready' };
|
|
389
|
+
}
|
|
390
|
+
// `#requestsToRetry` is a subset of `inProgress`, so nothing in progress means nothing left to re-serve.
|
|
391
|
+
return this.inProgress.size === 0 ? { status: 'finished' } : { status: 'waiting' };
|
|
395
392
|
}
|
|
396
393
|
/**
|
|
397
394
|
* @inheritDoc
|
|
398
395
|
*/
|
|
399
396
|
async fetchNextRequest() {
|
|
400
|
-
this
|
|
397
|
+
this.#ensureIsInitialized();
|
|
401
398
|
// First re-serve any requests that were interrupted before the last state persist.
|
|
402
399
|
const uniqueKey = this.#requestsToRetry.shift();
|
|
403
400
|
if (uniqueKey) {
|
|
404
401
|
const index = this.#uniqueKeyToIndex[uniqueKey];
|
|
405
|
-
return this
|
|
402
|
+
return this.#ensureRequest(this.requests[index], index);
|
|
406
403
|
}
|
|
407
404
|
// Otherwise return next request.
|
|
408
|
-
|
|
409
|
-
|
|
405
|
+
const state = this.#state.currentValue;
|
|
406
|
+
if (state.nextIndex < this.requests.length) {
|
|
407
|
+
const index = state.nextIndex;
|
|
410
408
|
const request = this.requests[index];
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
this
|
|
414
|
-
return this.ensureRequest(request, index);
|
|
409
|
+
state.inProgress.add(request.uniqueKey);
|
|
410
|
+
state.nextIndex++;
|
|
411
|
+
return this.#ensureRequest(request, index);
|
|
415
412
|
}
|
|
416
413
|
return null;
|
|
417
414
|
}
|
|
@@ -426,7 +423,7 @@ export class RequestList {
|
|
|
426
423
|
yield req;
|
|
427
424
|
}
|
|
428
425
|
}
|
|
429
|
-
ensureRequest(requestLike, index) {
|
|
426
|
+
#ensureRequest(requestLike, index) {
|
|
430
427
|
if (requestLike instanceof Request) {
|
|
431
428
|
return requestLike;
|
|
432
429
|
}
|
|
@@ -438,19 +435,18 @@ export class RequestList {
|
|
|
438
435
|
*/
|
|
439
436
|
async markRequestAsHandled(request) {
|
|
440
437
|
const { uniqueKey } = request;
|
|
441
|
-
this
|
|
442
|
-
this
|
|
443
|
-
this
|
|
438
|
+
this.#ensureUniqueKeyValid(uniqueKey);
|
|
439
|
+
this.#ensureInProgress(uniqueKey);
|
|
440
|
+
this.#ensureIsInitialized();
|
|
444
441
|
this.inProgress.delete(uniqueKey);
|
|
445
|
-
this.isStatePersisted = false;
|
|
446
442
|
}
|
|
447
443
|
/**
|
|
448
444
|
* Adds all fetched requests from a URL from a remote resource.
|
|
449
445
|
*/
|
|
450
|
-
async addFetchedRequests(source, fetchedRequests) {
|
|
446
|
+
async #addFetchedRequests(source, fetchedRequests) {
|
|
451
447
|
const { requestsFromUrl, regex } = source;
|
|
452
448
|
const originalLength = this.requests.length;
|
|
453
|
-
fetchedRequests.forEach((request) => this
|
|
449
|
+
fetchedRequests.forEach((request) => this.#addRequest(request));
|
|
454
450
|
const fetchedCount = fetchedRequests.length;
|
|
455
451
|
const importedCount = this.requests.length - originalLength;
|
|
456
452
|
this.#log.info('Fetched and loaded Requests from a remote resource.', {
|
|
@@ -462,7 +458,7 @@ export class RequestList {
|
|
|
462
458
|
sample: JSON.stringify(fetchedRequests.slice(0, 5)),
|
|
463
459
|
});
|
|
464
460
|
}
|
|
465
|
-
async getPersistedState(key) {
|
|
461
|
+
async #getPersistedState(key) {
|
|
466
462
|
this.#store ??= await KeyValueStore.open();
|
|
467
463
|
const state = await this.#store.getValue(key);
|
|
468
464
|
return state;
|
|
@@ -470,12 +466,12 @@ export class RequestList {
|
|
|
470
466
|
/**
|
|
471
467
|
* Fetches URLs from requestsFromUrl and returns them in format of list of requests
|
|
472
468
|
*/
|
|
473
|
-
async fetchRequestsFromUrl(source) {
|
|
469
|
+
async #fetchRequestsFromUrl(source) {
|
|
474
470
|
const { requestsFromUrl, regex, ...sharedOpts } = source;
|
|
475
471
|
// Download remote resource and parse URLs.
|
|
476
472
|
let urlsArr;
|
|
477
473
|
try {
|
|
478
|
-
urlsArr = await this
|
|
474
|
+
urlsArr = await this.#downloadListOfUrls({
|
|
479
475
|
url: requestsFromUrl,
|
|
480
476
|
urlRegExp: regex,
|
|
481
477
|
proxyUrl: (await this.#proxyConfiguration?.newProxyInfo())?.url,
|
|
@@ -496,7 +492,7 @@ export class RequestList {
|
|
|
496
492
|
* If the `source` parameter is a string or plain object and not an instance
|
|
497
493
|
* of a `Request`, then the function creates a `Request` instance.
|
|
498
494
|
*/
|
|
499
|
-
addRequest(source) {
|
|
495
|
+
#addRequest(source) {
|
|
500
496
|
let request;
|
|
501
497
|
const type = typeof source;
|
|
502
498
|
if (type === 'string') {
|
|
@@ -518,7 +514,7 @@ export class RequestList {
|
|
|
518
514
|
request.uniqueKey += `-${this.requests.length}`;
|
|
519
515
|
}
|
|
520
516
|
const { uniqueKey } = request;
|
|
521
|
-
this
|
|
517
|
+
this.#ensureUniqueKeyValid(uniqueKey);
|
|
522
518
|
// Skip requests with duplicate uniqueKey
|
|
523
519
|
if (!Object.hasOwn(this.#uniqueKeyToIndex, uniqueKey)) {
|
|
524
520
|
this.#uniqueKeyToIndex[uniqueKey] = this.requests.length;
|
|
@@ -532,7 +528,7 @@ export class RequestList {
|
|
|
532
528
|
* Helper function that validates unique key.
|
|
533
529
|
* Throws an error if uniqueKey is not a non-empty string.
|
|
534
530
|
*/
|
|
535
|
-
ensureUniqueKeyValid(uniqueKey) {
|
|
531
|
+
#ensureUniqueKeyValid(uniqueKey) {
|
|
536
532
|
if (typeof uniqueKey !== 'string' || !uniqueKey) {
|
|
537
533
|
throw new Error("Request object's uniqueKey must be a non-empty string");
|
|
538
534
|
}
|
|
@@ -540,7 +536,7 @@ export class RequestList {
|
|
|
540
536
|
/**
|
|
541
537
|
* Checks that a request is currently being processed and throws an error if not.
|
|
542
538
|
*/
|
|
543
|
-
ensureInProgress(uniqueKey) {
|
|
539
|
+
#ensureInProgress(uniqueKey) {
|
|
544
540
|
if (!this.inProgress.has(uniqueKey)) {
|
|
545
541
|
throw new Error(`The request is not being processed (uniqueKey: ${uniqueKey})`);
|
|
546
542
|
}
|
|
@@ -548,7 +544,7 @@ export class RequestList {
|
|
|
548
544
|
/**
|
|
549
545
|
* Throws an error if request list wasn't initialized.
|
|
550
546
|
*/
|
|
551
|
-
ensureIsInitialized() {
|
|
547
|
+
#ensureIsInitialized() {
|
|
552
548
|
if (!this.#isInitialized) {
|
|
553
549
|
throw new Error('RequestList is not initialized; you must call "await requestList.initialize()" before using it!');
|
|
554
550
|
}
|
|
@@ -557,15 +553,15 @@ export class RequestList {
|
|
|
557
553
|
* Returns the total number of unique requests present in the `RequestList`.
|
|
558
554
|
*/
|
|
559
555
|
async getTotalCount() {
|
|
560
|
-
this
|
|
556
|
+
this.#ensureIsInitialized();
|
|
561
557
|
return this.requests.length;
|
|
562
558
|
}
|
|
563
559
|
/**
|
|
564
560
|
* Returns the number of pending requests in the `RequestList`.
|
|
565
561
|
*/
|
|
566
562
|
async getPendingCount() {
|
|
567
|
-
this
|
|
568
|
-
return this.requests.length - (this.#nextIndex - this.inProgress.size);
|
|
563
|
+
this.#ensureIsInitialized();
|
|
564
|
+
return this.requests.length - (this.#state.currentValue.nextIndex - this.inProgress.size);
|
|
569
565
|
}
|
|
570
566
|
/**
|
|
571
567
|
* Combines this list with a request manager (a {@link RequestQueue} by default) into a
|
|
@@ -582,8 +578,8 @@ export class RequestList {
|
|
|
582
578
|
* @inheritDoc
|
|
583
579
|
*/
|
|
584
580
|
async getHandledCount() {
|
|
585
|
-
this
|
|
586
|
-
return this.#nextIndex - this.inProgress.size;
|
|
581
|
+
this.#ensureIsInitialized();
|
|
582
|
+
return this.#state.currentValue.nextIndex - this.inProgress.size;
|
|
587
583
|
}
|
|
588
584
|
/**
|
|
589
585
|
* Opens a request list and returns a promise resolving to an instance
|
|
@@ -642,7 +638,7 @@ export class RequestList {
|
|
|
642
638
|
if (listNameOrOptions != null && typeof listNameOrOptions === 'object') {
|
|
643
639
|
options = { ...listNameOrOptions, ...options };
|
|
644
640
|
const rl = new RequestList(options);
|
|
645
|
-
await rl
|
|
641
|
+
await rl.#initialize();
|
|
646
642
|
return rl;
|
|
647
643
|
}
|
|
648
644
|
const listName = listNameOrOptions;
|
|
@@ -655,13 +651,13 @@ export class RequestList {
|
|
|
655
651
|
persistRequestsKey: listName ? `${listName}-${REQUESTS_PERSISTENCE_KEY}` : options.persistRequestsKey,
|
|
656
652
|
sources: sources ?? options.sources,
|
|
657
653
|
});
|
|
658
|
-
await rl
|
|
654
|
+
await rl.#initialize();
|
|
659
655
|
return rl;
|
|
660
656
|
}
|
|
661
657
|
/**
|
|
662
658
|
* @internal wraps public utility for mocking purposes
|
|
663
659
|
*/
|
|
664
|
-
async downloadListOfUrls(options) {
|
|
660
|
+
async #downloadListOfUrls(options) {
|
|
665
661
|
return downloadListOfUrls({
|
|
666
662
|
...options,
|
|
667
663
|
httpClient: this.#httpClient,
|