@crawlee/core 4.0.0-beta.121 → 4.0.0-beta.123
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.js +20 -12
- package/autoscaling/concurrency_system.d.ts +2 -2
- package/autoscaling/concurrency_system.js +31 -20
- package/autoscaling/index.d.ts +1 -1
- package/autoscaling/index.js +1 -1
- package/autoscaling/load_signal.d.ts +7 -6
- package/autoscaling/load_signal.js +2 -1
- package/autoscaling/snapshotter.d.ts +6 -6
- package/autoscaling/snapshotter.js +9 -9
- package/autoscaling/{client_load_signal.d.ts → storage_backend_load_signal.d.ts} +13 -12
- package/autoscaling/{client_load_signal.js → storage_backend_load_signal.js} +11 -11
- package/autoscaling/system_status.d.ts +8 -8
- package/autoscaling/system_status.js +2 -2
- package/configuration.d.ts +15 -15
- package/configuration.js +3 -3
- package/crawlers/crawler_commons.d.ts +8 -54
- package/crawlers/statistics.d.ts +1 -1
- package/crawlers/statistics.js +14 -14
- package/debug.js +4 -4
- package/enqueue_links/enqueue_links.d.ts +33 -61
- package/enqueue_links/enqueue_links.js +35 -152
- package/enqueue_links/shared.d.ts +17 -4
- package/enqueue_links/shared.js +28 -1
- package/memory-storage/resource-clients/dataset.js +2 -8
- package/memory-storage/resource-clients/key-value-store.js +23 -26
- package/memory-storage/resource-clients/request-queue.js +9 -22
- package/package.json +7 -8
- package/proxy_configuration.js +10 -6
- package/request.d.ts +2 -2
- package/request.js +44 -31
- package/router.d.ts +5 -5
- package/serialization.js +6 -4
- package/session_pool/session.js +22 -20
- package/session_pool/session_pool.js +20 -17
- package/storages/dataset.js +11 -9
- package/storages/key_value_store.js +30 -27
- package/storages/request_list.d.ts +2 -1
- package/storages/request_list.js +26 -21
- package/storages/request_queue.js +64 -59
- package/storages/sitemap_request_loader.d.ts +1 -1
- package/storages/sitemap_request_loader.js +22 -22
- package/storages/throttling_request_manager.js +11 -9
- package/storages/utils.d.ts +2 -1
- package/validators.d.ts +22 -25
- package/validators.js +13 -25
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { z } from 'zod';
|
|
2
2
|
import { KEY_VALUE_STORE_KEY_REGEX } from '@apify/consts';
|
|
3
3
|
import { tryCancel } from '@apify/timeout';
|
|
4
4
|
import { Configuration } from '../configuration.js';
|
|
5
5
|
import { serviceLocator } from '../service_locator.js';
|
|
6
|
+
import { parseArgument, schemas, validators } from '../validators.js';
|
|
6
7
|
import { activeStorageTransaction, operationRejectedInTransaction, rejectOperationInTransaction, snapshotValue, withDirectStorageAccess, } from './transaction.js';
|
|
7
8
|
import { parseValue, serializeValue } from './key_value_store_codec.js';
|
|
8
9
|
import { StorageStatsTracker } from './storage_stats.js';
|
|
@@ -11,6 +12,20 @@ import { createDualIterable, purgeDefaultStorages } from './utils.js';
|
|
|
11
12
|
import { isBuffer, isStream } from '../byte_utils.js';
|
|
12
13
|
/** @internal */
|
|
13
14
|
const KVS_KEYS_DEFAULT_LIMIT = 1000;
|
|
15
|
+
const keySchema = z.string().nonempty();
|
|
16
|
+
const setValueKeySchema = z.string().nonempty().regex(KEY_VALUE_STORE_KEY_REGEX, {
|
|
17
|
+
message: `The "key" argument must be at most 256 characters long and only contain the following characters: a-zA-Z0-9!-_.'()`,
|
|
18
|
+
});
|
|
19
|
+
const recordOptionsSchema = z.strictObject({
|
|
20
|
+
contentType: z.string().nonempty().optional(),
|
|
21
|
+
});
|
|
22
|
+
const iteratorOptionsSchema = z.strictObject({
|
|
23
|
+
prefix: z.string().optional(),
|
|
24
|
+
});
|
|
25
|
+
const openOptionsSchema = z.strictObject({
|
|
26
|
+
configuration: z.instanceof(Configuration).optional(),
|
|
27
|
+
storageBackend: validators.storageBackend.optional(),
|
|
28
|
+
});
|
|
14
29
|
/**
|
|
15
30
|
* The `KeyValueStore` class represents a key-value store, a simple data storage that is used
|
|
16
31
|
* for saving and reading data records or files. Each data record is
|
|
@@ -132,7 +147,7 @@ export class KeyValueStore {
|
|
|
132
147
|
*/
|
|
133
148
|
async getValue(key, defaultValue) {
|
|
134
149
|
tryCancel();
|
|
135
|
-
|
|
150
|
+
parseArgument(key, keySchema);
|
|
136
151
|
const record = await this.readRecord(key);
|
|
137
152
|
// A missing record falls back to the default; a record that parses to a falsy value (including
|
|
138
153
|
// a stored literal `null`) is returned verbatim, so callers can tell "stored null" from "absent".
|
|
@@ -216,7 +231,7 @@ export class KeyValueStore {
|
|
|
216
231
|
*/
|
|
217
232
|
async getRecord(key) {
|
|
218
233
|
tryCancel();
|
|
219
|
-
|
|
234
|
+
parseArgument(key, keySchema);
|
|
220
235
|
return this.readRecord(key);
|
|
221
236
|
}
|
|
222
237
|
/**
|
|
@@ -227,7 +242,7 @@ export class KeyValueStore {
|
|
|
227
242
|
*/
|
|
228
243
|
async recordExists(key) {
|
|
229
244
|
tryCancel();
|
|
230
|
-
|
|
245
|
+
parseArgument(key, keySchema);
|
|
231
246
|
const entry = this.bufferedJournalEntries()?.get(key);
|
|
232
247
|
if (entry) {
|
|
233
248
|
return entry.value !== null;
|
|
@@ -362,19 +377,12 @@ export class KeyValueStore {
|
|
|
362
377
|
*/
|
|
363
378
|
async setValue(key, value, options = {}) {
|
|
364
379
|
const transaction = activeStorageTransaction();
|
|
365
|
-
|
|
366
|
-
ow(key, ow.string.validate((k) => ({
|
|
367
|
-
validator: ow.isValid(k, ow.string.matches(KEY_VALUE_STORE_KEY_REGEX)),
|
|
368
|
-
message: `The "key" argument "${key}" must be at most 256 characters long and only contain the following characters: a-zA-Z0-9!-_.'()`,
|
|
369
|
-
})));
|
|
380
|
+
parseArgument(key, setValueKeySchema);
|
|
370
381
|
if (options.contentType && !(typeof value === 'string' || isBuffer(value) || isStream(value))) {
|
|
371
|
-
throw new
|
|
382
|
+
throw new Error('The "value" parameter must be a String, Buffer, ArrayBuffer, TypedArray, or Stream when "options.contentType" is specified.');
|
|
372
383
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
}));
|
|
376
|
-
// Make copy of options, don't update what user passed.
|
|
377
|
-
const optionsCopy = { ...options };
|
|
384
|
+
// The parse result is a fresh copy, so we never update what user passed.
|
|
385
|
+
const optionsCopy = parseArgument(options, recordOptionsSchema);
|
|
378
386
|
// The whole transaction branch sits *above* the auto-saved cache update below, so a buffered
|
|
379
387
|
// write touches nothing outside the journal. That cache is shared, process-lifetime frontend
|
|
380
388
|
// state, so mutating it here would survive a rollback and later be persisted by `persistState`.
|
|
@@ -480,12 +488,10 @@ export class KeyValueStore {
|
|
|
480
488
|
*/
|
|
481
489
|
async forEachKey(iteratee, options = {}) {
|
|
482
490
|
tryCancel();
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
prefix: ow.optional.string,
|
|
486
|
-
}));
|
|
491
|
+
parseArgument(iteratee, schemas.anyFunction);
|
|
492
|
+
const parsedOptions = parseArgument(options, iteratorOptionsSchema);
|
|
487
493
|
let index = 0;
|
|
488
|
-
for await (const page of this.fetchKeyPages(
|
|
494
|
+
for await (const page of this.fetchKeyPages(parsedOptions)) {
|
|
489
495
|
for (const item of page) {
|
|
490
496
|
await iteratee(item.key, index++, { size: item.size });
|
|
491
497
|
}
|
|
@@ -623,13 +629,10 @@ export class KeyValueStore {
|
|
|
623
629
|
*/
|
|
624
630
|
static async open(identifier, options = {}) {
|
|
625
631
|
tryCancel();
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
})
|
|
630
|
-
options.configuration ??= Configuration.getGlobalConfiguration();
|
|
631
|
-
const storageBackend = options.storageBackend ?? serviceLocator.getStorageBackend();
|
|
632
|
-
await purgeDefaultStorages({ onlyPurgeOnce: true, storageBackend, configuration: options.configuration });
|
|
632
|
+
const parsedOptions = parseArgument(options, openOptionsSchema);
|
|
633
|
+
const configuration = parsedOptions.configuration ?? Configuration.getGlobalConfiguration();
|
|
634
|
+
const storageBackend = parsedOptions.storageBackend ?? serviceLocator.getStorageBackend();
|
|
635
|
+
await purgeDefaultStorages({ onlyPurgeOnce: true, storageBackend, configuration });
|
|
633
636
|
const resolved = await resolveStorageIdentifier(identifier, storageBackend, 'KeyValueStore');
|
|
634
637
|
return serviceLocator.getStorageInstanceManager().openStorage(this, {
|
|
635
638
|
...resolved,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { BaseHttpClient
|
|
1
|
+
import type { BaseHttpClient } from '@crawlee/http-client';
|
|
2
|
+
import type { Dictionary } from '@crawlee/types';
|
|
2
3
|
import type { Configuration } from '../configuration.js';
|
|
3
4
|
import type { IProxyConfiguration } from '../proxy_configuration.js';
|
|
4
5
|
import { Request, type RequestOptions, type Source } from '../request.js';
|
package/storages/request_list.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { downloadListOfUrls } from '@crawlee/utils';
|
|
2
|
-
import
|
|
2
|
+
import { z } from 'zod';
|
|
3
3
|
import { EventType } from '../events/event_manager.js';
|
|
4
4
|
import { Request } from '../request.js';
|
|
5
5
|
import { createDeserialize, serializeArray } from '../serialization.js';
|
|
6
6
|
import { serviceLocator } from '../service_locator.js';
|
|
7
|
+
import { parseArgument, schemas, validators } from '../validators.js';
|
|
7
8
|
import { KeyValueStore } from './key_value_store.js';
|
|
8
9
|
import { purgeDefaultStorages } from './utils.js';
|
|
9
10
|
/** @internal */
|
|
@@ -11,6 +12,24 @@ export const STATE_PERSISTENCE_KEY = 'REQUEST_LIST_STATE';
|
|
|
11
12
|
/** @internal */
|
|
12
13
|
export const REQUESTS_PERSISTENCE_KEY = 'REQUEST_LIST_REQUESTS';
|
|
13
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({});
|
|
14
33
|
/**
|
|
15
34
|
* Represents a static list of URLs to crawl.
|
|
16
35
|
* The URLs can be provided either in code or parsed from a text file hosted on the web.
|
|
@@ -123,24 +142,10 @@ export class RequestList {
|
|
|
123
142
|
* @internal
|
|
124
143
|
*/
|
|
125
144
|
constructor(options = {}) {
|
|
126
|
-
const { sources, sourcesFunction, persistStateKey, persistRequestsKey, state, proxyConfiguration, keepDuplicateUrls
|
|
145
|
+
const { sources, sourcesFunction, persistStateKey, persistRequestsKey, state, proxyConfiguration, keepDuplicateUrls, httpClient, } = parseArgument(options, requestListOptionsSchema);
|
|
127
146
|
if (!(sources || sourcesFunction)) {
|
|
128
|
-
throw new
|
|
129
|
-
}
|
|
130
|
-
ow(options, ow.object.exactShape({
|
|
131
|
-
sources: ow.optional.array, // check only for array and not subtypes to avoid iteration over the whole thing
|
|
132
|
-
sourcesFunction: ow.optional.function,
|
|
133
|
-
persistStateKey: ow.optional.string,
|
|
134
|
-
persistRequestsKey: ow.optional.string,
|
|
135
|
-
state: ow.optional.object.exactShape({
|
|
136
|
-
nextIndex: ow.number,
|
|
137
|
-
nextUniqueKey: ow.string,
|
|
138
|
-
inProgress: ow.object,
|
|
139
|
-
}),
|
|
140
|
-
keepDuplicateUrls: ow.optional.boolean,
|
|
141
|
-
proxyConfiguration: ow.optional.object,
|
|
142
|
-
httpClient: ow.optional.object,
|
|
143
|
-
}));
|
|
147
|
+
throw new Error('At least one of "sources" or "sourcesFunction" must be provided.');
|
|
148
|
+
}
|
|
144
149
|
this.#persistStateKey = persistStateKey ? `CRAWLEE_${persistStateKey}` : persistStateKey;
|
|
145
150
|
this.#persistRequestsKey = persistRequestsKey ? `CRAWLEE_${persistRequestsKey}` : persistRequestsKey;
|
|
146
151
|
this.#initialState = state;
|
|
@@ -641,9 +646,9 @@ export class RequestList {
|
|
|
641
646
|
return rl;
|
|
642
647
|
}
|
|
643
648
|
const listName = listNameOrOptions;
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
649
|
+
parseArgument(listName, listNameSchema);
|
|
650
|
+
parseArgument(sources, schemas.anyArray);
|
|
651
|
+
parseArgument(options, openOptionsSchema);
|
|
647
652
|
const rl = new RequestList({
|
|
648
653
|
...options,
|
|
649
654
|
persistStateKey: listName ? `${listName}-${STATE_PERSISTENCE_KEY}` : options.persistStateKey,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { inspect } from 'node:util';
|
|
2
2
|
import { isAsyncIterable, isIterable } from '@crawlee/utils/internal';
|
|
3
3
|
import { downloadListOfUrls } from '@crawlee/utils';
|
|
4
|
-
import
|
|
4
|
+
import { z } from 'zod';
|
|
5
5
|
import { LruCache } from '@apify/datastructures';
|
|
6
6
|
import { tryCancel } from '@apify/timeout';
|
|
7
7
|
import { Configuration } from '../configuration.js';
|
|
@@ -9,6 +9,7 @@ import { getObjectType } from '../debug.js';
|
|
|
9
9
|
import { EventType } from '../events/event_manager.js';
|
|
10
10
|
import { Request } from '../request.js';
|
|
11
11
|
import { serviceLocator } from '../service_locator.js';
|
|
12
|
+
import { parseArgument, schemas, validators } from '../validators.js';
|
|
12
13
|
import { activeStorageTransaction, rejectOperationInTransaction } from './transaction.js';
|
|
13
14
|
import { drainRequestBatches } from './batched_adds.js';
|
|
14
15
|
import { StorageStatsTracker } from './storage_stats.js';
|
|
@@ -20,6 +21,43 @@ import { RequestDeduplicationCache } from './request_dedup_cache.js';
|
|
|
20
21
|
* @internal
|
|
21
22
|
*/
|
|
22
23
|
const MAX_CACHED_REQUESTS = 2_000_000;
|
|
24
|
+
const iterableSchema = z.custom((value) => isIterable(value) || isAsyncIterable(value), {
|
|
25
|
+
error: (issue) => `Expected an iterable or async iterable, got ${getObjectType(issue.input)}`,
|
|
26
|
+
});
|
|
27
|
+
const operationOptionsSchema = z.strictObject({
|
|
28
|
+
forefront: z.boolean().default(false),
|
|
29
|
+
});
|
|
30
|
+
const addRequestsOptionsSchema = z.strictObject({
|
|
31
|
+
forefront: z.boolean().default(false),
|
|
32
|
+
cache: z.boolean().default(true),
|
|
33
|
+
});
|
|
34
|
+
const addRequestsBatchedOptionsSchema = z.strictObject({
|
|
35
|
+
forefront: z.boolean().optional(),
|
|
36
|
+
waitForAllRequestsToBeAdded: z.boolean().default(false),
|
|
37
|
+
batchSize: schemas.anyNumber.default(1000),
|
|
38
|
+
waitBetweenBatchesMillis: schemas.anyNumber.default(1000),
|
|
39
|
+
maxNewRequests: schemas.anyNumber.optional(),
|
|
40
|
+
});
|
|
41
|
+
const newRequestLikeSchema = z.looseObject({
|
|
42
|
+
url: z.string(),
|
|
43
|
+
id: z.undefined().optional(),
|
|
44
|
+
});
|
|
45
|
+
const handledRequestSchema = z.looseObject({
|
|
46
|
+
id: z.string(),
|
|
47
|
+
uniqueKey: z.string(),
|
|
48
|
+
handledAt: z.string().optional(),
|
|
49
|
+
});
|
|
50
|
+
const reclaimedRequestSchema = z.looseObject({
|
|
51
|
+
id: z.string(),
|
|
52
|
+
uniqueKey: z.string(),
|
|
53
|
+
});
|
|
54
|
+
const uniqueKeySchema = z.string();
|
|
55
|
+
const openOptionsSchema = z.strictObject({
|
|
56
|
+
configuration: z.instanceof(Configuration).optional(),
|
|
57
|
+
storageBackend: validators.storageBackend.optional(),
|
|
58
|
+
proxyConfiguration: validators.proxyConfiguration.optional(),
|
|
59
|
+
httpClient: schemas.httpClient.optional(),
|
|
60
|
+
});
|
|
23
61
|
/**
|
|
24
62
|
* Represents a queue of URLs to crawl, which is used for deep crawling of websites
|
|
25
63
|
* where you start with several URLs and then recursively
|
|
@@ -140,20 +178,14 @@ export class RequestQueue {
|
|
|
140
178
|
*/
|
|
141
179
|
async addRequest(requestLike, options = {}) {
|
|
142
180
|
const transaction = activeStorageTransaction();
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
forefront: ow.optional.boolean,
|
|
146
|
-
}));
|
|
147
|
-
const { forefront = false } = options;
|
|
181
|
+
parseArgument(requestLike, schemas.anyObject);
|
|
182
|
+
const { forefront } = parseArgument(options, operationOptionsSchema);
|
|
148
183
|
if ('requestsFromUrl' in requestLike) {
|
|
149
184
|
const requests = await this.fetchRequestsFromUrl(requestLike);
|
|
150
185
|
const processedRequests = await this.addFetchedRequests(requestLike, requests, options);
|
|
151
186
|
return { ...processedRequests[0], forefront };
|
|
152
187
|
}
|
|
153
|
-
|
|
154
|
-
url: ow.string,
|
|
155
|
-
id: ow.undefined,
|
|
156
|
-
}));
|
|
188
|
+
parseArgument(requestLike, newRequestLikeSchema);
|
|
157
189
|
const request = requestLike instanceof Request ? requestLike : new Request(requestLike);
|
|
158
190
|
if (transaction?.policy.requestQueue === 'deferred') {
|
|
159
191
|
return this.addRequestDeferred(transaction, request, forefront);
|
|
@@ -346,14 +378,8 @@ export class RequestQueue {
|
|
|
346
378
|
*/
|
|
347
379
|
async addRequests(requestsLike, options = {}) {
|
|
348
380
|
const transaction = activeStorageTransaction();
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
.message((value) => `Expected an iterable or async iterable, got ${getObjectType(value)}`));
|
|
352
|
-
ow(options, ow.object.exactShape({
|
|
353
|
-
forefront: ow.optional.boolean,
|
|
354
|
-
cache: ow.optional.boolean,
|
|
355
|
-
}));
|
|
356
|
-
const { forefront = false, cache = true } = options;
|
|
381
|
+
parseArgument(requestsLike, iterableSchema);
|
|
382
|
+
const { forefront, cache } = parseArgument(options, addRequestsOptionsSchema);
|
|
357
383
|
const uniqueKeyToCacheKey = new Map();
|
|
358
384
|
const getCachedRequestId = (uniqueKey) => {
|
|
359
385
|
const cached = uniqueKeyToCacheKey.get(uniqueKey);
|
|
@@ -439,16 +465,8 @@ export class RequestQueue {
|
|
|
439
465
|
* @param options Options for the request queue
|
|
440
466
|
*/
|
|
441
467
|
async addRequestsBatched(requests, options = {}) {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
.message((value) => `Expected an iterable or async iterable, got ${getObjectType(value)}`));
|
|
445
|
-
ow(options, ow.object.exactShape({
|
|
446
|
-
forefront: ow.optional.boolean,
|
|
447
|
-
waitForAllRequestsToBeAdded: ow.optional.boolean,
|
|
448
|
-
batchSize: ow.optional.number,
|
|
449
|
-
waitBetweenBatchesMillis: ow.optional.number,
|
|
450
|
-
maxNewRequests: ow.optional.number,
|
|
451
|
-
}));
|
|
468
|
+
parseArgument(requests, iterableSchema);
|
|
469
|
+
const { forefront, waitForAllRequestsToBeAdded, batchSize, waitBetweenBatchesMillis, maxNewRequests } = parseArgument(options, addRequestsBatchedOptionsSchema);
|
|
452
470
|
const addRequest = this.addRequest.bind(this);
|
|
453
471
|
async function* generateRequests() {
|
|
454
472
|
for await (const opts of requests) {
|
|
@@ -467,7 +485,7 @@ export class RequestQueue {
|
|
|
467
485
|
}
|
|
468
486
|
if (opts && typeof opts === 'object' && 'requestsFromUrl' in opts) {
|
|
469
487
|
// Handle URL lists right away
|
|
470
|
-
await addRequest(opts, { forefront
|
|
488
|
+
await addRequest(opts, { forefront });
|
|
471
489
|
}
|
|
472
490
|
else {
|
|
473
491
|
// Yield valid requests
|
|
@@ -477,10 +495,10 @@ export class RequestQueue {
|
|
|
477
495
|
}
|
|
478
496
|
return drainRequestBatches({
|
|
479
497
|
items: generateRequests(),
|
|
480
|
-
batchSize
|
|
481
|
-
waitBetweenBatchesMillis
|
|
482
|
-
waitForAllRequestsToBeAdded
|
|
483
|
-
maxNewRequests
|
|
498
|
+
batchSize,
|
|
499
|
+
waitBetweenBatchesMillis,
|
|
500
|
+
waitForAllRequestsToBeAdded,
|
|
501
|
+
maxNewRequests,
|
|
484
502
|
/**
|
|
485
503
|
* Requests the backend reports as unprocessed are warned about and skipped rather than retried:
|
|
486
504
|
* `unprocessedRequests` is what remains after the backend's own transient-error handling - a
|
|
@@ -489,7 +507,7 @@ export class RequestQueue {
|
|
|
489
507
|
*/
|
|
490
508
|
processChunk: async (chunk, isInitial) => {
|
|
491
509
|
const { processedRequests, unprocessedRequests } = await this.addRequests(chunk, {
|
|
492
|
-
forefront
|
|
510
|
+
forefront,
|
|
493
511
|
cache: isInitial,
|
|
494
512
|
});
|
|
495
513
|
if (unprocessedRequests.length > 0) {
|
|
@@ -514,7 +532,7 @@ export class RequestQueue {
|
|
|
514
532
|
*/
|
|
515
533
|
async getRequest(uniqueKey) {
|
|
516
534
|
const transaction = activeStorageTransaction();
|
|
517
|
-
|
|
535
|
+
parseArgument(uniqueKey, uniqueKeySchema);
|
|
518
536
|
// Requests buffered by the active transaction (under the `deferred` write policy) are visible to it.
|
|
519
537
|
const buffered = transaction && this.bufferedRequests(transaction).get(uniqueKey);
|
|
520
538
|
if (buffered) {
|
|
@@ -561,11 +579,7 @@ export class RequestQueue {
|
|
|
561
579
|
*/
|
|
562
580
|
async markRequestAsHandled(request) {
|
|
563
581
|
rejectOperationInTransaction('RequestQueue.markRequestAsHandled()', 'it is part of the crawler request-processing bookkeeping, which a transaction must not affect.');
|
|
564
|
-
|
|
565
|
-
id: ow.string,
|
|
566
|
-
uniqueKey: ow.string,
|
|
567
|
-
handledAt: ow.optional.string,
|
|
568
|
-
}));
|
|
582
|
+
parseArgument(request, handledRequestSchema);
|
|
569
583
|
const forefront = this.requestCache.get(getRequestId(request.uniqueKey))?.forefront ?? false;
|
|
570
584
|
const handledAt = request.handledAt ?? new Date().toISOString();
|
|
571
585
|
this.#statsTracker.add('writeCount');
|
|
@@ -594,16 +608,12 @@ export class RequestQueue {
|
|
|
594
608
|
*/
|
|
595
609
|
async reclaimRequest(request, options = {}) {
|
|
596
610
|
rejectOperationInTransaction('RequestQueue.reclaimRequest()', 'it is part of the crawler request-processing bookkeeping, which a transaction must not affect.');
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
uniqueKey: ow.string,
|
|
600
|
-
}));
|
|
601
|
-
ow(options, ow.object.exactShape({
|
|
602
|
-
forefront: ow.optional.boolean,
|
|
603
|
-
}));
|
|
604
|
-
const { forefront = false } = options;
|
|
611
|
+
parseArgument(request, reclaimedRequestSchema);
|
|
612
|
+
const { forefront } = parseArgument(options, operationOptionsSchema);
|
|
605
613
|
this.#statsTracker.add('writeCount');
|
|
606
|
-
const processedRequest = await this.backend.reclaimRequest(request, {
|
|
614
|
+
const processedRequest = await this.backend.reclaimRequest(request, {
|
|
615
|
+
forefront,
|
|
616
|
+
});
|
|
607
617
|
// The request was not in progress — nothing to reclaim.
|
|
608
618
|
if (!processedRequest) {
|
|
609
619
|
return null;
|
|
@@ -837,14 +847,9 @@ export class RequestQueue {
|
|
|
837
847
|
*/
|
|
838
848
|
static async open(identifier, options = {}) {
|
|
839
849
|
tryCancel();
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
proxyConfiguration: ow.optional.object,
|
|
844
|
-
httpClient: ow.optional.object,
|
|
845
|
-
}));
|
|
846
|
-
const storageBackend = options.storageBackend ?? serviceLocator.getStorageBackend();
|
|
847
|
-
const configuration = options.configuration ?? serviceLocator.getConfiguration();
|
|
850
|
+
const parsedOptions = parseArgument(options, openOptionsSchema);
|
|
851
|
+
const storageBackend = parsedOptions.storageBackend ?? serviceLocator.getStorageBackend();
|
|
852
|
+
const configuration = parsedOptions.configuration ?? serviceLocator.getConfiguration();
|
|
848
853
|
await purgeDefaultStorages({ onlyPurgeOnce: true, storageBackend, configuration });
|
|
849
854
|
const resolved = await resolveStorageIdentifier(identifier, storageBackend, 'RequestQueue');
|
|
850
855
|
const queue = await serviceLocator
|
|
@@ -854,8 +859,8 @@ export class RequestQueue {
|
|
|
854
859
|
backendOpener: () => storageBackend.createRequestQueueBackend(resolved),
|
|
855
860
|
backendCacheKey: storageBackend.getStorageBackendCacheKey?.() ?? storageBackend.constructor.name,
|
|
856
861
|
});
|
|
857
|
-
queue.#proxyConfiguration =
|
|
858
|
-
queue.#httpClient =
|
|
862
|
+
queue.#proxyConfiguration = parsedOptions.proxyConfiguration;
|
|
863
|
+
queue.#httpClient = parsedOptions.httpClient;
|
|
859
864
|
return queue;
|
|
860
865
|
}
|
|
861
866
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BaseHttpClient } from '@crawlee/
|
|
1
|
+
import type { BaseHttpClient } from '@crawlee/http-client';
|
|
2
2
|
import { type ParseSitemapOptions } from '@crawlee/utils';
|
|
3
3
|
import type { UrlPatternInput } from '../enqueue_links/shared.js';
|
|
4
4
|
import { Request } from '../request.js';
|
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
import { Transform } from 'node:stream';
|
|
2
2
|
import { parseSitemap } from '@crawlee/utils';
|
|
3
3
|
import { minimatch } from 'minimatch';
|
|
4
|
-
import
|
|
5
|
-
import { constructUrlPatternObjects } from '../enqueue_links/shared.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { constructUrlPatternObjects, urlPatternSchema } from '../enqueue_links/shared.js';
|
|
6
6
|
import { EventType } from '../events/event_manager.js';
|
|
7
7
|
import { Request } from '../request.js';
|
|
8
8
|
import { serviceLocator } from '../service_locator.js';
|
|
9
|
+
import { parseArgument, schemas } from '../validators.js';
|
|
9
10
|
import { KeyValueStore } from './key_value_store.js';
|
|
10
11
|
import { purgeDefaultStorages } from './utils.js';
|
|
12
|
+
const sitemapRequestLoaderOptionsSchema = z.strictObject({
|
|
13
|
+
sitemapUrls: schemas.arrayOf(z.string(), 'strings'),
|
|
14
|
+
proxyUrl: z.string().optional(),
|
|
15
|
+
persistStateKey: z.string().optional(),
|
|
16
|
+
signal: z.unknown().optional(),
|
|
17
|
+
timeoutMillis: schemas.anyNumber.optional(),
|
|
18
|
+
maxBufferSize: schemas.anyNumber.default(200),
|
|
19
|
+
parseSitemapOptions: z.looseObject({}).optional(),
|
|
20
|
+
include: schemas.arrayOf(urlPatternSchema, 'URL patterns').optional(),
|
|
21
|
+
exclude: schemas.arrayOf(urlPatternSchema, 'URL patterns').optional(),
|
|
22
|
+
persistenceOptions: z.looseObject({}).optional(),
|
|
23
|
+
httpClient: schemas.httpClient.optional(),
|
|
24
|
+
});
|
|
11
25
|
/** @internal */
|
|
12
26
|
const STATE_PERSISTENCE_KEY = 'SITEMAP_REQUEST_LOADER_STATE';
|
|
13
27
|
/**
|
|
@@ -80,21 +94,7 @@ export class SitemapRequestLoader {
|
|
|
80
94
|
#persistenceOptions;
|
|
81
95
|
/** @internal */
|
|
82
96
|
constructor(options) {
|
|
83
|
-
const
|
|
84
|
-
ow(options, ow.object.exactShape({
|
|
85
|
-
sitemapUrls: ow.array.ofType(ow.string),
|
|
86
|
-
proxyUrl: ow.optional.string,
|
|
87
|
-
persistStateKey: ow.optional.string,
|
|
88
|
-
signal: ow.optional.any(),
|
|
89
|
-
timeoutMillis: ow.optional.number,
|
|
90
|
-
maxBufferSize: ow.optional.number,
|
|
91
|
-
parseSitemapOptions: ow.optional.object,
|
|
92
|
-
include: ow.optional.array.ofType(urlPatternValidator),
|
|
93
|
-
exclude: ow.optional.array.ofType(urlPatternValidator),
|
|
94
|
-
persistenceOptions: ow.optional.object,
|
|
95
|
-
httpClient: ow.optional.object,
|
|
96
|
-
}));
|
|
97
|
-
const { include, exclude } = options;
|
|
97
|
+
const { include, exclude, persistStateKey, persistenceOptions, proxyUrl, maxBufferSize, sitemapUrls } = parseArgument(options, sitemapRequestLoaderOptionsSchema, 'SitemapRequestLoaderOptions');
|
|
98
98
|
this.#log = serviceLocator.getLogger().child({ prefix: 'SitemapRequestLoader' });
|
|
99
99
|
if (exclude?.length) {
|
|
100
100
|
this.#urlExcludePatternObjects.push(...constructUrlPatternObjects(exclude));
|
|
@@ -102,11 +102,11 @@ export class SitemapRequestLoader {
|
|
|
102
102
|
if (include?.length) {
|
|
103
103
|
this.#urlPatternObjects.push(...constructUrlPatternObjects(include));
|
|
104
104
|
}
|
|
105
|
-
this.#persistStateKey =
|
|
106
|
-
this.#persistenceOptions = { enable: true, ...
|
|
107
|
-
this.#proxyUrl =
|
|
108
|
-
this.#urlQueueStream = this.createNewStream(
|
|
109
|
-
this.#sitemapParsingProgress.pendingSitemapUrls = new Set(
|
|
105
|
+
this.#persistStateKey = persistStateKey;
|
|
106
|
+
this.#persistenceOptions = { enable: true, ...persistenceOptions };
|
|
107
|
+
this.#proxyUrl = proxyUrl;
|
|
108
|
+
this.#urlQueueStream = this.createNewStream(maxBufferSize);
|
|
109
|
+
this.#sitemapParsingProgress.pendingSitemapUrls = new Set(sitemapUrls);
|
|
110
110
|
this.#events = serviceLocator.getEventManager();
|
|
111
111
|
this.persistState = this.persistState.bind(this);
|
|
112
112
|
}
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import { URL } from 'node:url';
|
|
2
|
-
import
|
|
2
|
+
import { z } from 'zod';
|
|
3
3
|
import { PersistentRateLimitError } from '../errors.js';
|
|
4
4
|
import { asyncifyIterable } from '../iterables.js';
|
|
5
5
|
import { serviceLocator } from '../service_locator.js';
|
|
6
6
|
import { normalizeHostname } from '../url.js';
|
|
7
|
+
import { parseArgument, schemas } from '../validators.js';
|
|
7
8
|
import { drainRequestBatches } from './batched_adds.js';
|
|
8
9
|
import { RequestQueue } from './request_queue.js';
|
|
10
|
+
const throttlingRequestManagerOptionsSchema = z.strictObject({
|
|
11
|
+
inner: schemas.anyObject,
|
|
12
|
+
domains: schemas.arrayOf(z.string().nonempty(), 'non-empty strings'),
|
|
13
|
+
requestManagerOpener: schemas.anyFunction.optional(),
|
|
14
|
+
baseDelaySecs: schemas.anyNumber.refine((value) => value > 0, 'Expected a number greater than 0').optional(),
|
|
15
|
+
maxDelaySecs: schemas.anyNumber.refine((value) => value > 0, 'Expected a number greater than 0').optional(),
|
|
16
|
+
maxDomainStallSecs: schemas.anyNumber.refine((value) => value > 0, 'Expected a number greater than 0').optional(),
|
|
17
|
+
});
|
|
9
18
|
/** Whether `manager` can pace requests per domain. */
|
|
10
19
|
export function supportsDomainThrottling(manager) {
|
|
11
20
|
const candidate = manager;
|
|
@@ -75,14 +84,7 @@ export class ThrottlingRequestManager {
|
|
|
75
84
|
}
|
|
76
85
|
constructor(options, config = serviceLocator.getConfiguration()) {
|
|
77
86
|
this.config = config;
|
|
78
|
-
|
|
79
|
-
inner: ow.object,
|
|
80
|
-
domains: ow.array.ofType(ow.string.nonEmpty),
|
|
81
|
-
requestManagerOpener: ow.optional.function,
|
|
82
|
-
baseDelaySecs: ow.optional.number.positive,
|
|
83
|
-
maxDelaySecs: ow.optional.number.positive,
|
|
84
|
-
maxDomainStallSecs: ow.optional.number.positive,
|
|
85
|
-
}));
|
|
87
|
+
parseArgument(options, throttlingRequestManagerOptionsSchema, 'ThrottlingRequestManagerOptions');
|
|
86
88
|
this.inner = options.inner;
|
|
87
89
|
this.requestManagerOpener =
|
|
88
90
|
options.requestManagerOpener ??
|
package/storages/utils.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { BaseHttpClient
|
|
1
|
+
import type { BaseHttpClient } from '@crawlee/http-client';
|
|
2
|
+
import type { Dictionary, StorageBackend } from '@crawlee/types';
|
|
2
3
|
import { Configuration } from '../configuration.js';
|
|
3
4
|
import type { IProxyConfiguration } from '../proxy_configuration.js';
|
|
4
5
|
/**
|
package/validators.d.ts
CHANGED
|
@@ -1,28 +1,25 @@
|
|
|
1
|
-
|
|
1
|
+
export { ArgumentValidationError, parseArgument } from '@crawlee/utils';
|
|
2
|
+
export { schemas } from '@crawlee/utils/internal';
|
|
2
3
|
/** @internal */
|
|
3
4
|
export declare const validators: {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
sessionPool: (value: Dictionary) => {
|
|
25
|
-
validator: boolean;
|
|
26
|
-
message: (label: string) => string;
|
|
27
|
-
};
|
|
5
|
+
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
6
|
+
browserPage: import("zod").ZodType<import("@crawlee/types").Dictionary, unknown, import("zod/v4/core").$ZodTypeInternals<import("@crawlee/types").Dictionary, unknown>>;
|
|
7
|
+
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
8
|
+
proxyConfiguration: import("zod").ZodType<import("@crawlee/types").Dictionary, unknown, import("zod/v4/core").$ZodTypeInternals<import("@crawlee/types").Dictionary, unknown>>;
|
|
9
|
+
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
10
|
+
requestList: import("zod").ZodType<import("@crawlee/types").Dictionary, unknown, import("zod/v4/core").$ZodTypeInternals<import("@crawlee/types").Dictionary, unknown>>;
|
|
11
|
+
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
12
|
+
requestQueue: import("zod").ZodType<import("@crawlee/types").Dictionary, unknown, import("zod/v4/core").$ZodTypeInternals<import("@crawlee/types").Dictionary, unknown>>;
|
|
13
|
+
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
14
|
+
browserPool: import("zod").ZodType<import("@crawlee/types").Dictionary, unknown, import("zod/v4/core").$ZodTypeInternals<import("@crawlee/types").Dictionary, unknown>>;
|
|
15
|
+
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
16
|
+
sessionPool: import("zod").ZodType<import("@crawlee/types").Dictionary, unknown, import("zod/v4/core").$ZodTypeInternals<import("@crawlee/types").Dictionary, unknown>>;
|
|
17
|
+
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
18
|
+
requestManager: import("zod").ZodType<import("@crawlee/types").Dictionary, unknown, import("zod/v4/core").$ZodTypeInternals<import("@crawlee/types").Dictionary, unknown>>;
|
|
19
|
+
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
20
|
+
storageBackend: import("zod").ZodType<import("@crawlee/types").Dictionary, unknown, import("zod/v4/core").$ZodTypeInternals<import("@crawlee/types").Dictionary, unknown>>;
|
|
21
|
+
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
22
|
+
logger: import("zod").ZodType<import("@crawlee/types").Dictionary, unknown, import("zod/v4/core").$ZodTypeInternals<import("@crawlee/types").Dictionary, unknown>>;
|
|
23
|
+
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
24
|
+
httpClient: import("zod").ZodCustom<import("@crawlee/http-client").BaseHttpClient, import("@crawlee/http-client").BaseHttpClient>;
|
|
28
25
|
};
|