@crawlee/core 4.0.0-beta.121 → 4.0.0-beta.122
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.js +31 -20
- package/configuration.d.ts +15 -15
- package/configuration.js +3 -3
- package/crawlers/statistics.d.ts +1 -1
- package/crawlers/statistics.js +14 -14
- package/debug.js +4 -4
- package/enqueue_links/enqueue_links.js +39 -34
- package/enqueue_links/shared.d.ts +6 -0
- package/enqueue_links/shared.js +12 -0
- 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.js +44 -31
- 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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crawlee/core",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.122",
|
|
4
4
|
"description": "The scalable web crawling and scraping library for JavaScript/Node.js. Enables development of data extraction and web automation jobs (not only) with headless Chrome and Puppeteer.",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22.0.0"
|
|
@@ -52,24 +52,23 @@
|
|
|
52
52
|
"@apify/log": "^2.5.18",
|
|
53
53
|
"@apify/timeout": "^0.4.4",
|
|
54
54
|
"@apify/utilities": "^2.15.5",
|
|
55
|
-
"@crawlee/fs-storage": "4.0.0-beta.
|
|
56
|
-
"@crawlee/
|
|
57
|
-
"@crawlee/
|
|
55
|
+
"@crawlee/fs-storage": "4.0.0-beta.122",
|
|
56
|
+
"@crawlee/http-client": "4.0.0-beta.122",
|
|
57
|
+
"@crawlee/types": "4.0.0-beta.122",
|
|
58
|
+
"@crawlee/utils": "4.0.0-beta.122",
|
|
58
59
|
"@sapphire/async-queue": "^1.5.5",
|
|
59
|
-
"@sapphire/shapeshift": "^4.0.0",
|
|
60
60
|
"@vladfrangu/async_event_emitter": "^2.4.6",
|
|
61
61
|
"content-type": "^1.0.5",
|
|
62
62
|
"csv-stringify": "^6.5.2",
|
|
63
63
|
"json5": "^2.2.3",
|
|
64
64
|
"mime-types": "^3.0.1",
|
|
65
65
|
"minimatch": "^10.0.1",
|
|
66
|
-
"ow": "^2.0.0",
|
|
67
66
|
"stream-json": "^1.9.1",
|
|
68
67
|
"tldts": "^7.0.6",
|
|
69
68
|
"tough-cookie": "^6.0.0",
|
|
70
69
|
"tslib": "^2.8.1",
|
|
71
70
|
"type-fest": "^4.41.0",
|
|
72
|
-
"zod": "^4.
|
|
71
|
+
"zod": "^4.4.3"
|
|
73
72
|
},
|
|
74
73
|
"lerna": {
|
|
75
74
|
"command": {
|
|
@@ -78,5 +77,5 @@
|
|
|
78
77
|
}
|
|
79
78
|
}
|
|
80
79
|
},
|
|
81
|
-
"gitHead": "
|
|
80
|
+
"gitHead": "2c3e87fefdb9e1fca4c144f03167d8524cfdc2e5"
|
|
82
81
|
}
|
package/proxy_configuration.js
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { parseArgument, schemas } from './validators.js';
|
|
3
|
+
const proxyConfigurationOptionsSchema = z.strictObject({
|
|
4
|
+
proxyUrls: z
|
|
5
|
+
.array(z.union([z.url(), z.null()]))
|
|
6
|
+
.nonempty()
|
|
7
|
+
.optional(),
|
|
8
|
+
newUrlFunction: schemas.anyFunction.optional(),
|
|
9
|
+
});
|
|
2
10
|
/**
|
|
3
11
|
* Configures connection to a proxy server with the provided options. Proxy servers are used to prevent target websites from blocking
|
|
4
12
|
* your crawlers based on IP address rate limits or blacklists. Setting proxy configuration in your crawlers automatically configures
|
|
@@ -58,11 +66,7 @@ export class ProxyConfiguration {
|
|
|
58
66
|
throw new Error('The `tieredProxyUrls` option has been removed in Crawlee v4. ' +
|
|
59
67
|
'See the v4 upgrading guide for the recommended migration to named sessions.');
|
|
60
68
|
}
|
|
61
|
-
|
|
62
|
-
proxyUrls: ow.optional.array.nonEmpty.ofType(ow.any(ow.string.url, ow.null)),
|
|
63
|
-
newUrlFunction: ow.optional.function,
|
|
64
|
-
}));
|
|
65
|
-
const { proxyUrls, newUrlFunction } = options;
|
|
69
|
+
const { proxyUrls, newUrlFunction } = parseArgument(rest, proxyConfigurationOptionsSchema);
|
|
66
70
|
if (proxyUrls && newUrlFunction)
|
|
67
71
|
this.throwCannotCombineCustomMethods();
|
|
68
72
|
if (!proxyUrls && !newUrlFunction && validateRequired)
|
package/request.js
CHANGED
|
@@ -1,32 +1,13 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
2
|
import util from 'node:util';
|
|
3
|
-
import
|
|
3
|
+
import { z } from 'zod';
|
|
4
4
|
import { cryptoRandomObjectId, normalizeUrl } from '@apify/utilities';
|
|
5
5
|
import { serviceLocator } from './service_locator.js';
|
|
6
6
|
import { keys } from './typedefs.js';
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
uniqueKey: ow.optional.string,
|
|
12
|
-
method: ow.optional.string,
|
|
13
|
-
payload: ow.optional.any(ow.string, ow.uint8Array),
|
|
14
|
-
noRetry: ow.optional.boolean,
|
|
15
|
-
retryCount: ow.optional.number,
|
|
16
|
-
sessionId: ow.optional.string,
|
|
17
|
-
maxRetries: ow.optional.number,
|
|
18
|
-
errorMessages: ow.optional.array.ofType(ow.string),
|
|
19
|
-
headers: ow.optional.object,
|
|
20
|
-
userData: ow.optional.object,
|
|
21
|
-
label: ow.optional.string,
|
|
22
|
-
handledAt: ow.optional.any(ow.string.date, ow.date),
|
|
23
|
-
keepUrlFragment: ow.optional.boolean,
|
|
24
|
-
useExtendedUniqueKey: ow.optional.boolean,
|
|
25
|
-
alwaysEnqueue: ow.optional.boolean,
|
|
26
|
-
skipNavigation: ow.optional.boolean,
|
|
27
|
-
crawlDepth: ow.optional.number.greaterThanOrEqual(0),
|
|
28
|
-
state: ow.optional.number.greaterThanOrEqual(0).lessThanOrEqual(6),
|
|
29
|
-
};
|
|
7
|
+
import { parseArgument, schemas } from './validators.js';
|
|
8
|
+
const dateString = z.string().refine((value) => !Number.isNaN(Date.parse(value)), {
|
|
9
|
+
message: 'Invalid input: expected a date string',
|
|
10
|
+
});
|
|
30
11
|
export var RequestState;
|
|
31
12
|
(function (RequestState) {
|
|
32
13
|
RequestState[RequestState["UNPROCESSED"] = 0] = "UNPROCESSED";
|
|
@@ -38,6 +19,34 @@ export var RequestState;
|
|
|
38
19
|
RequestState[RequestState["ERROR"] = 6] = "ERROR";
|
|
39
20
|
RequestState[RequestState["SKIPPED"] = 7] = "SKIPPED";
|
|
40
21
|
})(RequestState || (RequestState = {}));
|
|
22
|
+
const requestUrlSchema = z.object({ url: z.string() });
|
|
23
|
+
// new properties on the Request object breaks serialization
|
|
24
|
+
const requestOptionalSchemaShapes = {
|
|
25
|
+
id: z.string().optional(),
|
|
26
|
+
loadedUrl: z.url().optional(),
|
|
27
|
+
uniqueKey: z.string().optional(),
|
|
28
|
+
method: z.string().optional(),
|
|
29
|
+
payload: z.union([z.string(), z.instanceof(Uint8Array)]).optional(),
|
|
30
|
+
noRetry: z.boolean().optional(),
|
|
31
|
+
retryCount: schemas.anyNumber.optional(),
|
|
32
|
+
sessionId: z.string().optional(),
|
|
33
|
+
maxRetries: schemas.anyNumber.optional(),
|
|
34
|
+
errorMessages: schemas.arrayOf(z.string(), 'strings').optional(),
|
|
35
|
+
headers: z.looseObject({}).optional(),
|
|
36
|
+
userData: z.looseObject({}).optional(),
|
|
37
|
+
label: z.string().optional(),
|
|
38
|
+
handledAt: z.union([dateString, z.date()]).optional(),
|
|
39
|
+
keepUrlFragment: z.boolean().optional(),
|
|
40
|
+
useExtendedUniqueKey: z.boolean().optional(),
|
|
41
|
+
alwaysEnqueue: z.boolean().optional(),
|
|
42
|
+
skipNavigation: z.boolean().optional(),
|
|
43
|
+
crawlDepth: schemas.anyNumber
|
|
44
|
+
.refine((value) => value >= 0, 'Expected a number greater than or equal to 0')
|
|
45
|
+
.optional(),
|
|
46
|
+
state: z.enum(RequestState).optional(),
|
|
47
|
+
};
|
|
48
|
+
// Each schema is wrapped in a single-key object so validation errors carry the property name.
|
|
49
|
+
const requestOptionalSchemas = Object.fromEntries(Object.entries(requestOptionalSchemaShapes).map(([key, schema]) => [key, z.object({ [key]: schema })]));
|
|
41
50
|
/**
|
|
42
51
|
* Represents a URL to be crawled, optionally including HTTP method, headers, payload and other metadata.
|
|
43
52
|
* The `Request` object also stores information about errors that occurred during processing of the request.
|
|
@@ -118,22 +127,26 @@ class CrawleeRequest {
|
|
|
118
127
|
* `Request` parameters including the URL, HTTP method and headers, and others.
|
|
119
128
|
*/
|
|
120
129
|
constructor(options) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
130
|
+
// A bare URL is a common slip — point at the object form instead of a generic type error.
|
|
131
|
+
if (typeof options === 'string') {
|
|
132
|
+
throw new TypeError(`\`Request\` options must be an object, got the string '${options}'. ` +
|
|
133
|
+
'Did you mean `new Request({ url })`?');
|
|
134
|
+
}
|
|
135
|
+
parseArgument(options, schemas.anyObject, 'RequestOptions');
|
|
136
|
+
parseArgument(options, requestUrlSchema, 'RequestOptions');
|
|
137
|
+
// Full-shape validation is slow, because it checks all predicates
|
|
124
138
|
// even if the validated object has only 1 property.
|
|
125
139
|
// This custom validation loop iterates only over existing
|
|
126
140
|
// properties and speeds up the validation cca 3-fold.
|
|
127
|
-
// See https://github.com/sindresorhus/ow/issues/193
|
|
128
141
|
keys(options).forEach((prop) => {
|
|
129
142
|
// skip url, because it is validated above
|
|
130
143
|
if (prop === 'url') {
|
|
131
144
|
return;
|
|
132
145
|
}
|
|
133
|
-
const
|
|
146
|
+
const schema = requestOptionalSchemas[prop];
|
|
134
147
|
const value = options[prop];
|
|
135
|
-
if (
|
|
136
|
-
|
|
148
|
+
if (schema) {
|
|
149
|
+
parseArgument({ [prop]: value }, schema, 'RequestOptions');
|
|
137
150
|
}
|
|
138
151
|
});
|
|
139
152
|
const { id, url, loadedUrl, uniqueKey, payload, noRetry = false, retryCount = 0, sessionId, maxRetries, errorMessages = [], headers = {}, userData = {}, label, handledAt, keepUrlFragment = false, useExtendedUniqueKey = false, alwaysEnqueue = false, skipNavigation, enqueueStrategy, crawlDepth, } = options;
|
package/serialization.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { pipeline as streamPipeline, Readable, Writable } from 'node:stream';
|
|
2
2
|
import util from 'node:util';
|
|
3
3
|
import zlib from 'node:zlib';
|
|
4
|
-
import ow from 'ow';
|
|
5
4
|
import StreamArray from 'stream-json/streamers/StreamArray.js';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { parseArgument, schemas } from './validators.js';
|
|
6
7
|
const pipeline = util.promisify(streamPipeline);
|
|
8
|
+
const uint8ArraySchema = z.instanceof(Uint8Array);
|
|
7
9
|
/**
|
|
8
10
|
* Transforms an array of items to a JSON in a streaming
|
|
9
11
|
* fashion to save memory. It operates in batches to speed
|
|
@@ -57,7 +59,7 @@ class ArrayToJson extends Readable {
|
|
|
57
59
|
* @internal
|
|
58
60
|
*/
|
|
59
61
|
export async function serializeArray(data) {
|
|
60
|
-
|
|
62
|
+
parseArgument(data, schemas.anyArray);
|
|
61
63
|
const { chunks, collector } = createChunkCollector();
|
|
62
64
|
await pipeline(new ArrayToJson(data), zlib.createGzip(), collector);
|
|
63
65
|
return Buffer.concat(chunks);
|
|
@@ -72,7 +74,7 @@ export async function serializeArray(data) {
|
|
|
72
74
|
* @internal
|
|
73
75
|
*/
|
|
74
76
|
export async function deserializeArray(compressedData) {
|
|
75
|
-
|
|
77
|
+
parseArgument(compressedData, uint8ArraySchema);
|
|
76
78
|
const { chunks, collector } = createChunkCollector({ fromValuesStream: true });
|
|
77
79
|
await pipeline(Readable.from([compressedData]), zlib.createGunzip(), StreamArray.withParser(), collector);
|
|
78
80
|
return chunks;
|
|
@@ -87,7 +89,7 @@ export async function deserializeArray(compressedData) {
|
|
|
87
89
|
* @internal
|
|
88
90
|
*/
|
|
89
91
|
export function createDeserialize(compressedData) {
|
|
90
|
-
|
|
92
|
+
parseArgument(compressedData, uint8ArraySchema);
|
|
91
93
|
const streamArray = StreamArray.withParser();
|
|
92
94
|
const destination = pluckValue(streamArray);
|
|
93
95
|
streamPipeline(Readable.from([compressedData]), zlib.createGunzip(), destination, (err) => destination.emit(err));
|
package/session_pool/session.js
CHANGED
|
@@ -1,8 +1,28 @@
|
|
|
1
|
-
import ow from 'ow';
|
|
2
1
|
import { CookieJar } from 'tough-cookie';
|
|
2
|
+
import { z } from 'zod';
|
|
3
3
|
import { cryptoRandomObjectId } from '@apify/utilities';
|
|
4
4
|
import { getDefaultCookieExpirationDate } from '../cookie_utils.js';
|
|
5
5
|
import { serviceLocator } from '../service_locator.js';
|
|
6
|
+
import { parseArgument, schemas, validators } from '../validators.js';
|
|
7
|
+
// `schemas.anyObject` passes values through by reference (object schemas return a pruned plain
|
|
8
|
+
// copy), so class instances like cookie jars and loggers keep their prototype.
|
|
9
|
+
const sessionOptionsSchema = z.strictObject({
|
|
10
|
+
id: z.string().default(() => `session_${cryptoRandomObjectId(10)}`),
|
|
11
|
+
cookieJar: schemas.anyObject.default(() => new CookieJar()),
|
|
12
|
+
proxyInfo: schemas.anyObject.optional(),
|
|
13
|
+
maxAgeSecs: schemas.anyNumber.default(3000),
|
|
14
|
+
userData: schemas.anyObject.default(() => ({})),
|
|
15
|
+
maxErrorScore: schemas.anyNumber.default(3),
|
|
16
|
+
errorScoreDecrement: schemas.anyNumber.default(0.5),
|
|
17
|
+
createdAt: z.date().default(() => new Date()),
|
|
18
|
+
expiresAt: z.date().optional(),
|
|
19
|
+
usageCount: schemas.anyNumber.default(0),
|
|
20
|
+
errorScore: schemas.anyNumber.default(0),
|
|
21
|
+
maxUsageCount: schemas.anyNumber.default(50),
|
|
22
|
+
retired: z.boolean().default(false),
|
|
23
|
+
log: validators.logger.default(() => serviceLocator.getLogger()),
|
|
24
|
+
fingerprint: schemas.anyObject.optional(),
|
|
25
|
+
});
|
|
6
26
|
/**
|
|
7
27
|
* Sessions are used to store information such as cookies and can be used for generating fingerprints and proxy sessions.
|
|
8
28
|
* You can imagine each session as a specific user, with its own cookies, IP (via proxy) and potentially a unique browser fingerprint.
|
|
@@ -68,25 +88,7 @@ export class Session {
|
|
|
68
88
|
* Session configuration.
|
|
69
89
|
*/
|
|
70
90
|
constructor(options = {}) {
|
|
71
|
-
|
|
72
|
-
id: ow.optional.string,
|
|
73
|
-
cookieJar: ow.optional.object,
|
|
74
|
-
proxyInfo: ow.optional.object,
|
|
75
|
-
maxAgeSecs: ow.optional.number,
|
|
76
|
-
userData: ow.optional.object,
|
|
77
|
-
maxErrorScore: ow.optional.number,
|
|
78
|
-
errorScoreDecrement: ow.optional.number,
|
|
79
|
-
createdAt: ow.optional.date,
|
|
80
|
-
expiresAt: ow.optional.date,
|
|
81
|
-
usageCount: ow.optional.number,
|
|
82
|
-
errorScore: ow.optional.number,
|
|
83
|
-
maxUsageCount: ow.optional.number,
|
|
84
|
-
retired: ow.optional.boolean,
|
|
85
|
-
log: ow.optional.object,
|
|
86
|
-
fingerprint: ow.optional.object,
|
|
87
|
-
}));
|
|
88
|
-
const { id = `session_${cryptoRandomObjectId(10)}`, cookieJar = new CookieJar(), proxyInfo = undefined, maxAgeSecs = 3000, userData = {}, maxErrorScore = 3, errorScoreDecrement = 0.5, createdAt = new Date(), usageCount = 0, errorScore = 0, maxUsageCount = 50, retired = false, log = serviceLocator.getLogger(), fingerprint, } = options;
|
|
89
|
-
const { expiresAt = getDefaultCookieExpirationDate(maxAgeSecs) } = options;
|
|
91
|
+
const { id, cookieJar, proxyInfo, maxAgeSecs, userData, maxErrorScore, errorScoreDecrement, createdAt, usageCount, errorScore, maxUsageCount, retired, log, fingerprint, expiresAt = getDefaultCookieExpirationDate(maxAgeSecs), } = parseArgument(options, sessionOptionsSchema);
|
|
90
92
|
this.#log = log.child({ prefix: 'Session' });
|
|
91
93
|
this.#cookieJar = cookieJar.setCookie ? cookieJar : CookieJar.fromJSON(JSON.stringify(cookieJar));
|
|
92
94
|
this.#proxyInfo = proxyInfo;
|
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
import { AsyncQueue } from '@sapphire/async-queue';
|
|
2
|
-
import
|
|
2
|
+
import { z } from 'zod';
|
|
3
3
|
import { EventType } from '../events/event_manager.js';
|
|
4
4
|
import { serviceLocator } from '../service_locator.js';
|
|
5
5
|
import { KeyValueStore } from '../storages/key_value_store.js';
|
|
6
|
+
import { parseArgument, schemas, validators } from '../validators.js';
|
|
6
7
|
import { MAX_POOL_SIZE, PERSIST_STATE_KEY } from './consts.js';
|
|
7
8
|
import { createDefaultSessionFingerprint } from './fingerprint.js';
|
|
8
9
|
import { Session } from './session.js';
|
|
9
10
|
const SESSION_REUSE_STRATEGIES = ['random', 'round-robin', 'use-until-failure'];
|
|
11
|
+
// `schemas.anyObject` passes values through by reference (object schemas return a pruned plain
|
|
12
|
+
// copy), so class instances like loggers keep their prototype.
|
|
13
|
+
const sessionPoolOptionsSchema = z.strictObject({
|
|
14
|
+
id: z.union([schemas.anyNumber, z.string()]).optional(),
|
|
15
|
+
maxPoolSize: schemas.anyNumber.default(MAX_POOL_SIZE),
|
|
16
|
+
persistStateKeyValueStoreId: z.string().optional(),
|
|
17
|
+
persistStateKey: z.string().optional(),
|
|
18
|
+
createSessionFunction: schemas.anyFunction.optional(),
|
|
19
|
+
sessionOptions: schemas.anyObject.default(() => ({})),
|
|
20
|
+
log: validators.logger.default(() => serviceLocator.getLogger()),
|
|
21
|
+
persistenceOptions: schemas.anyObject.default(() => ({ enable: true })),
|
|
22
|
+
sessionReuseStrategy: z.enum(SESSION_REUSE_STRATEGIES).default('random'),
|
|
23
|
+
});
|
|
24
|
+
const createSessionOptionsSchema = z.strictObject({
|
|
25
|
+
sessionOptions: schemas.anyObject.default(() => ({})),
|
|
26
|
+
});
|
|
10
27
|
/**
|
|
11
28
|
* Handles the rotation, creation and persistence of user-like sessions.
|
|
12
29
|
* Creates a pool of {@link Session} instances, that are randomly rotated.
|
|
@@ -81,20 +98,7 @@ export class SessionPool {
|
|
|
81
98
|
#queue = new AsyncQueue();
|
|
82
99
|
#roundRobinIndex = 0;
|
|
83
100
|
constructor(options = {}) {
|
|
84
|
-
|
|
85
|
-
id: ow.optional.any(ow.number, ow.string),
|
|
86
|
-
maxPoolSize: ow.optional.number,
|
|
87
|
-
persistStateKeyValueStoreId: ow.optional.string,
|
|
88
|
-
persistStateKey: ow.optional.string,
|
|
89
|
-
createSessionFunction: ow.optional.function,
|
|
90
|
-
sessionOptions: ow.optional.object,
|
|
91
|
-
log: ow.optional.object,
|
|
92
|
-
persistenceOptions: ow.optional.object,
|
|
93
|
-
sessionReuseStrategy: ow.optional.string.oneOf([...SESSION_REUSE_STRATEGIES]),
|
|
94
|
-
}));
|
|
95
|
-
const { id, maxPoolSize = MAX_POOL_SIZE, persistStateKeyValueStoreId, persistStateKey, createSessionFunction, sessionOptions = {}, log = serviceLocator.getLogger(), persistenceOptions = {
|
|
96
|
-
enable: true,
|
|
97
|
-
}, sessionReuseStrategy = 'random', } = options;
|
|
101
|
+
const { id, maxPoolSize, persistStateKeyValueStoreId, persistStateKey, createSessionFunction, sessionOptions, log, persistenceOptions, sessionReuseStrategy, } = parseArgument(options, sessionPoolOptionsSchema);
|
|
98
102
|
this.id = id != null ? String(id) : String(SessionPool.#nextId++);
|
|
99
103
|
this.#sessionReuseStrategy = sessionReuseStrategy;
|
|
100
104
|
this.#events = serviceLocator.getEventManager();
|
|
@@ -303,8 +307,7 @@ export class SessionPool {
|
|
|
303
307
|
* @returns New session.
|
|
304
308
|
*/
|
|
305
309
|
async defaultCreateSessionFunction(options = {}) {
|
|
306
|
-
|
|
307
|
-
const { sessionOptions = {} } = options;
|
|
310
|
+
const { sessionOptions } = parseArgument(options, createSessionOptionsSchema);
|
|
308
311
|
return new Session(sessionOptions);
|
|
309
312
|
}
|
|
310
313
|
/**
|
package/storages/dataset.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { z } from 'zod';
|
|
2
2
|
import { tryCancel } from '@apify/timeout';
|
|
3
3
|
import { Configuration } from '../configuration.js';
|
|
4
4
|
import { serviceLocator } from '../service_locator.js';
|
|
5
|
+
import { parseArgument, schemas, validators } from '../validators.js';
|
|
5
6
|
import { activeStorageTransaction, rejectOperationInTransaction, snapshotValue } from './transaction.js';
|
|
6
7
|
import { KeyValueStore } from './key_value_store.js';
|
|
7
8
|
import { StorageStatsTracker } from './storage_stats.js';
|
|
8
9
|
import { resolveStorageIdentifier } from './storage_instance_manager.js';
|
|
9
10
|
import { createDualIterable, purgeDefaultStorages } from './utils.js';
|
|
11
|
+
const openOptionsSchema = z.strictObject({
|
|
12
|
+
configuration: z.instanceof(Configuration).optional(),
|
|
13
|
+
storageBackend: validators.storageBackend.optional(),
|
|
14
|
+
});
|
|
10
15
|
/** @internal */
|
|
11
16
|
export const DATASET_ITERATORS_DEFAULT_LIMIT = 10000;
|
|
12
17
|
/**
|
|
@@ -122,7 +127,7 @@ export class Dataset {
|
|
|
122
127
|
*/
|
|
123
128
|
async pushData(data) {
|
|
124
129
|
const transaction = activeStorageTransaction();
|
|
125
|
-
|
|
130
|
+
parseArgument(data, schemas.anyObject);
|
|
126
131
|
// Normalize to array and validate each item
|
|
127
132
|
const items = Array.isArray(data) ? data : [data];
|
|
128
133
|
for (let i = 0; i < items.length; i++) {
|
|
@@ -553,13 +558,10 @@ export class Dataset {
|
|
|
553
558
|
*/
|
|
554
559
|
static async open(identifier, options = {}) {
|
|
555
560
|
tryCancel();
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
})
|
|
560
|
-
options.configuration ??= Configuration.getGlobalConfiguration();
|
|
561
|
-
const storageBackend = options.storageBackend ?? serviceLocator.getStorageBackend();
|
|
562
|
-
await purgeDefaultStorages({ onlyPurgeOnce: true, storageBackend, configuration: options.configuration });
|
|
561
|
+
const parsedOptions = parseArgument(options, openOptionsSchema);
|
|
562
|
+
const configuration = parsedOptions.configuration ?? Configuration.getGlobalConfiguration();
|
|
563
|
+
const storageBackend = parsedOptions.storageBackend ?? serviceLocator.getStorageBackend();
|
|
564
|
+
await purgeDefaultStorages({ onlyPurgeOnce: true, storageBackend, configuration });
|
|
563
565
|
const resolved = await resolveStorageIdentifier(identifier, storageBackend, 'Dataset');
|
|
564
566
|
return serviceLocator.getStorageInstanceManager().openStorage(this, {
|
|
565
567
|
...resolved,
|
|
@@ -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,
|