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