@crawlee/core 4.0.0-beta.120 → 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.
@@ -1,23 +1,10 @@
1
1
  import { randomUUID } from 'node:crypto';
2
+ import { parseArgument, schemas } from '@crawlee/utils/internal';
2
3
  import { AsyncQueue } from '@sapphire/async-queue';
3
- import { s } from '@sapphire/shapeshift';
4
+ import { z } from 'zod';
4
5
  import { purgeNullsFromObject, uniqueKeyToRequestId } from '../utils.js';
5
6
  import { BaseClient } from './common/base-client.js';
6
- const requestShape = s
7
- .object({
8
- id: s.string(),
9
- url: s.string().url({ allowedProtocols: ['http:', 'https:'] }),
10
- uniqueKey: s.string(),
11
- method: s.string().optional(),
12
- retryCount: s.number().int().optional(),
13
- handledAt: s.union([s.string(), s.date().valid()]).optional(),
14
- })
15
- .passthrough();
16
- const requestShapeWithoutId = requestShape.omit(['id']);
17
- const batchRequestShapeWithoutId = requestShapeWithoutId.array();
18
- const requestOptionsShape = s.object({
19
- forefront: s.boolean().optional(),
20
- });
7
+ const uniqueKeySchema = z.string();
21
8
  export class RequestQueueBackend extends BaseClient {
22
9
  name;
23
10
  /**
@@ -183,8 +170,8 @@ export class RequestQueueBackend extends BaseClient {
183
170
  }
184
171
  }
185
172
  async addBatchOfRequests(requests, options = {}) {
186
- batchRequestShapeWithoutId.parse(requests);
187
- requestOptionsShape.parse(options);
173
+ parseArgument(requests, schemas.storageRequestBatch);
174
+ parseArgument(options, schemas.requestQueueOperationOptions);
188
175
  // Serialize against other mutators (and the head scans in `isEmpty`/`isFinished`) so that the
189
176
  // shared `requests` map, `forefrontRequestIds` array and request counts are not corrupted by a
190
177
  // concurrent operation interleaving at one of the `await` points below.
@@ -233,14 +220,14 @@ export class RequestQueueBackend extends BaseClient {
233
220
  }
234
221
  }
235
222
  async getRequest(uniqueKey) {
236
- s.string().parse(uniqueKey);
223
+ parseArgument(uniqueKey, uniqueKeySchema);
237
224
  this.updateTimestamps(false);
238
225
  const id = uniqueKeyToRequestId(uniqueKey);
239
226
  const json = this.#requests.get(id)?.json;
240
227
  return this.jsonToRequest(json);
241
228
  }
242
229
  async markRequestAsHandled(request) {
243
- requestShape.parse(request);
230
+ parseArgument(request, schemas.storageRequest);
244
231
  this.updateTimestamps(false);
245
232
  // Serialize against other mutators (and the head scans in `isEmpty`/`isFinished`) so the shared
246
233
  // `requests` map, `inProgressRequestIds` set and request counts stay consistent across the
@@ -278,8 +265,8 @@ export class RequestQueueBackend extends BaseClient {
278
265
  }
279
266
  }
280
267
  async reclaimRequest(request, options = {}) {
281
- requestShape.parse(request);
282
- requestOptionsShape.parse(options);
268
+ parseArgument(request, schemas.storageRequest);
269
+ parseArgument(options, schemas.requestQueueOperationOptions);
283
270
  this.updateTimestamps(false);
284
271
  // Serialize against other mutators (and the head scans in `isEmpty`/`isFinished`) so the shared
285
272
  // `requests` map, `forefrontRequestIds` array and `inProgressRequestIds` set stay consistent
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crawlee/core",
3
- "version": "4.0.0-beta.120",
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.120",
56
- "@crawlee/types": "4.0.0-beta.120",
57
- "@crawlee/utils": "4.0.0-beta.120",
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.1.0"
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": "da5d427c4c1c9dcaea95b151e9c4c7310100885d"
80
+ "gitHead": "2c3e87fefdb9e1fca4c144f03167d8524cfdc2e5"
82
81
  }
@@ -1,4 +1,12 @@
1
- import ow from 'ow';
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
- ow(rest, ow.object.exactShape({
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 ow from 'ow';
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
- // new properties on the Request object breaks serialization
8
- const requestOptionalPredicates = {
9
- id: ow.optional.string,
10
- loadedUrl: ow.optional.string.url,
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
- ow(options, 'RequestOptions', ow.object);
122
- ow(options.url, 'RequestOptions.url', ow.string);
123
- // 'ow' validation is slow, because it checks all predicates
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 predicate = requestOptionalPredicates[prop];
146
+ const schema = requestOptionalSchemas[prop];
134
147
  const value = options[prop];
135
- if (predicate) {
136
- ow(value, `RequestOptions.${prop}`, predicate);
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;
@@ -167,7 +180,10 @@ class CrawleeRequest {
167
180
  if (label) {
168
181
  userData.label = label;
169
182
  }
170
- this.#userData = { __crawlee: {}, ...userData };
183
+ // Read `__crawlee` explicitly - on a `userData` coming from another Request instance the
184
+ // bag is non-enumerable, so the spread alone would silently drop the internal state
185
+ // (e.g. `skipNavigation`) when a request is re-wrapped after a storage round trip.
186
+ this.#userData = { __crawlee: userData.__crawlee ?? {}, ...userData };
171
187
  // `userData` must stay an enumerable own accessor — serialization in the storages relies on it
172
188
  Object.defineProperties(this, {
173
189
  userData: {
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
- ow(data, ow.array);
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
- ow(compressedData, ow.uint8Array);
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
- ow(compressedData, ow.uint8Array);
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));
@@ -62,6 +62,17 @@ interface ServiceLocatorInterface {
62
62
  * Get the storage instance manager (shared across all storage types).
63
63
  */
64
64
  getStorageInstanceManager(): StorageInstanceManager;
65
+ /**
66
+ * Returns the currently set services without triggering the implicit creation of defaults.
67
+ * Used to inherit already-materialized services into crawler-scoped service locators.
68
+ * @internal
69
+ */
70
+ getServicesIfSet(): {
71
+ configuration?: Configuration;
72
+ eventManager?: EventManager;
73
+ storageBackend?: StorageBackend;
74
+ logger?: CrawleeLogger;
75
+ };
65
76
  /**
66
77
  * Resets the service locator to its initial state.
67
78
  * Used mainly for testing purposes.
@@ -111,6 +122,13 @@ export declare class ServiceLocator implements ServiceLocatorInterface {
111
122
  * @param logger Optional logger instance to use
112
123
  */
113
124
  constructor(configuration?: Configuration, eventManager?: EventManager, storageBackend?: StorageBackend, logger?: CrawleeLogger);
125
+ /** @internal */
126
+ getServicesIfSet(): {
127
+ configuration?: Configuration;
128
+ eventManager?: EventManager;
129
+ storageBackend?: StorageBackend;
130
+ logger?: CrawleeLogger;
131
+ };
114
132
  getConfiguration(): Configuration;
115
133
  setConfiguration(configuration: Configuration): void;
116
134
  getEventManager(): EventManager;
@@ -63,6 +63,15 @@ export class ServiceLocator {
63
63
  this.#storageBackend = storageBackend;
64
64
  this.#logger = logger;
65
65
  }
66
+ /** @internal */
67
+ getServicesIfSet() {
68
+ return {
69
+ configuration: this.#configuration,
70
+ eventManager: this.#eventManager,
71
+ storageBackend: this.#storageBackend,
72
+ logger: this.#logger,
73
+ };
74
+ }
66
75
  getConfiguration() {
67
76
  if (!this.#configuration) {
68
77
  this.getLogger().debug('No configuration set, implicitly creating and using default Configuration.');
@@ -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
- ow(options, ow.object.exactShape({
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 ow from 'ow';
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
- ow(options, ow.object.exactShape({
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
- ow(options, ow.object.exactShape({ sessionOptions: ow.optional.object }));
307
- const { sessionOptions = {} } = options;
310
+ const { sessionOptions } = parseArgument(options, createSessionOptionsSchema);
308
311
  return new Session(sessionOptions);
309
312
  }
310
313
  /**
@@ -1,12 +1,17 @@
1
- import ow from 'ow';
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
- ow(data, 'data', ow.object);
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
- ow(options, ow.object.exactShape({
557
- configuration: ow.optional.object.instanceOf(Configuration),
558
- storageBackend: ow.optional.object,
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 ow, { ArgumentError } from 'ow';
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
- ow(key, ow.string.nonEmpty);
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
- ow(key, ow.string.nonEmpty);
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
- ow(key, ow.string.nonEmpty);
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
- ow(key, 'key', ow.string.nonEmpty);
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 ArgumentError('The "value" parameter must be a String, Buffer, ArrayBuffer, TypedArray, or Stream when "options.contentType" is specified.', this.setValue);
382
+ throw new Error('The "value" parameter must be a String, Buffer, ArrayBuffer, TypedArray, or Stream when "options.contentType" is specified.');
372
383
  }
373
- ow(options, ow.object.exactShape({
374
- contentType: ow.optional.string.nonEmpty,
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
- ow(iteratee, ow.function);
484
- ow(options, ow.object.exactShape({
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(options)) {
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
- ow(options, ow.object.exactShape({
627
- configuration: ow.optional.object.instanceOf(Configuration),
628
- storageBackend: ow.optional.object,
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, Dictionary } from '@crawlee/types';
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';