@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.
- 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/error_snapshotter.d.ts +1 -1
- package/crawlers/error_snapshotter.js +4 -2
- 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 +48 -32
- package/serialization.js +6 -4
- package/service_locator.d.ts +18 -0
- package/service_locator.js +9 -0
- 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,26 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { z } from 'zod';
|
|
2
2
|
import { addTimeoutToPromise } from '@apify/timeout';
|
|
3
3
|
import { betterClearInterval, betterSetInterval } from '@apify/utilities';
|
|
4
4
|
import { CriticalError } from '../errors.js';
|
|
5
5
|
import { serviceLocator } from '../service_locator.js';
|
|
6
|
+
import { parseArgument, schemas, validators } from '../validators.js';
|
|
7
|
+
// `schemas.anyObject` and `objectWithKeys`-based validators pass values through by reference
|
|
8
|
+
// (object schemas return a pruned plain copy), so class instances like loggers and the
|
|
9
|
+
// concurrency system keep their prototype.
|
|
10
|
+
const autoscaledPoolOptionsSchema = z.strictObject({
|
|
11
|
+
runTaskFunction: schemas.anyFunction,
|
|
12
|
+
isFinishedFunction: schemas.anyFunction,
|
|
13
|
+
isTaskReadyFunction: schemas.anyFunction,
|
|
14
|
+
maybeRunIntervalSecs: schemas.anyNumber
|
|
15
|
+
.refine((value) => value > 0, 'Expected a number greater than 0')
|
|
16
|
+
.default(0.5),
|
|
17
|
+
taskTimeoutSecs: schemas.anyNumber
|
|
18
|
+
.refine((value) => value >= 0, 'Expected a number greater than or equal to 0')
|
|
19
|
+
.default(0),
|
|
20
|
+
log: validators.logger.default(() => serviceLocator.getLogger()),
|
|
21
|
+
concurrencySystem: schemas.anyObject,
|
|
22
|
+
consumer: schemas.anyObject.refine((value) => typeof value.id === 'string' && value.id.length > 0, "Expected an object with a non-empty string 'id'"),
|
|
23
|
+
});
|
|
6
24
|
/**
|
|
7
25
|
* Manages a pool of asynchronous resource-intensive tasks that are executed in parallel.
|
|
8
26
|
* The pool only starts new tasks while its {@link IConcurrencySystem|concurrency system} reports free capacity —
|
|
@@ -78,17 +96,7 @@ export class AutoscaledPool {
|
|
|
78
96
|
*/
|
|
79
97
|
#ownConcurrency = 0;
|
|
80
98
|
constructor(options) {
|
|
81
|
-
|
|
82
|
-
runTaskFunction: ow.function,
|
|
83
|
-
isFinishedFunction: ow.function,
|
|
84
|
-
isTaskReadyFunction: ow.function,
|
|
85
|
-
maybeRunIntervalSecs: ow.optional.number.greaterThan(0),
|
|
86
|
-
taskTimeoutSecs: ow.optional.number.greaterThanOrEqual(0),
|
|
87
|
-
log: ow.optional.object,
|
|
88
|
-
concurrencySystem: ow.object,
|
|
89
|
-
consumer: ow.object.partialShape({ id: ow.string.nonEmpty }),
|
|
90
|
-
}));
|
|
91
|
-
const { runTaskFunction, isFinishedFunction, isTaskReadyFunction, maybeRunIntervalSecs = 0.5, taskTimeoutSecs = 0, log = serviceLocator.getLogger(), concurrencySystem, consumer, } = options;
|
|
99
|
+
const { runTaskFunction, isFinishedFunction, isTaskReadyFunction, maybeRunIntervalSecs, taskTimeoutSecs, log, concurrencySystem, consumer, } = parseArgument(options, autoscaledPoolOptionsSchema, 'AutoscaledPoolOptions');
|
|
92
100
|
this.#log = log.child({ prefix: 'AutoscaledPool' });
|
|
93
101
|
// Configurable properties.
|
|
94
102
|
this.#maybeRunIntervalMillis = maybeRunIntervalSecs * 1000;
|
|
@@ -1,8 +1,34 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { z } from 'zod';
|
|
2
2
|
import { betterClearInterval, betterSetInterval } from '@apify/utilities';
|
|
3
3
|
import { serviceLocator } from '../service_locator.js';
|
|
4
|
+
import { parseArgument, schemas, validators } from '../validators.js';
|
|
4
5
|
import { Snapshotter } from './snapshotter.js';
|
|
5
6
|
import { SystemStatus } from './system_status.js';
|
|
7
|
+
const concurrencySchema = z.number().int().gte(1).optional();
|
|
8
|
+
// `schemas.anyObject` passes values through by reference, so the load signal instances inside
|
|
9
|
+
// `loadSignals` and class instances like loggers keep their prototype.
|
|
10
|
+
const concurrencySystemOptionsSchema = z.strictObject({
|
|
11
|
+
maxConcurrency: z.number().int().gte(1).default(200),
|
|
12
|
+
minConcurrency: z.number().int().gte(1).default(1),
|
|
13
|
+
desiredConcurrency: z.number().int().gte(1).optional(),
|
|
14
|
+
desiredConcurrencyRatio: z.number().gt(0).lt(1).default(0.9),
|
|
15
|
+
scaleUpStepRatio: z.number().gt(0).lt(1).default(0.05),
|
|
16
|
+
scaleDownStepRatio: z.number().gt(0).lt(1).default(0.05),
|
|
17
|
+
loggingIntervalSecs: schemas.anyNumber
|
|
18
|
+
.refine((value) => value > 0, 'Expected a number greater than 0')
|
|
19
|
+
.nullish()
|
|
20
|
+
.default(60),
|
|
21
|
+
autoscaleIntervalSecs: schemas.anyNumber
|
|
22
|
+
.refine((value) => value > 0, 'Expected a number greater than 0')
|
|
23
|
+
.default(10),
|
|
24
|
+
loadSignals: schemas.anyObject.default(() => ({})),
|
|
25
|
+
snapshotHistorySecs: schemas.anyNumber.refine((value) => value > 0, 'Expected a number greater than 0').optional(),
|
|
26
|
+
currentHistorySecs: schemas.anyNumber.refine((value) => value > 0, 'Expected a number greater than 0').optional(),
|
|
27
|
+
log: validators.logger.default(() => serviceLocator.getLogger()),
|
|
28
|
+
maxTasksPerMinute: z
|
|
29
|
+
.union([z.number().int().gte(1), z.literal(Number.POSITIVE_INFINITY)])
|
|
30
|
+
.default(Number.POSITIVE_INFINITY),
|
|
31
|
+
});
|
|
6
32
|
/**
|
|
7
33
|
* The shareable "governor" behind an {@link AutoscaledPool}: it decides whether there is free compute for one more
|
|
8
34
|
* task by combining live system load (via an internal {@link Snapshotter}) with a concurrency budget it autoscales
|
|
@@ -44,22 +70,7 @@ export class ConcurrencySystem {
|
|
|
44
70
|
/** Set once per session, so a pool outliving `stop()` is reported once rather than every half second. */
|
|
45
71
|
#warnedAboutQueryWhileStopped = false;
|
|
46
72
|
constructor(options = {}) {
|
|
47
|
-
|
|
48
|
-
maxConcurrency: ow.optional.number.integer.greaterThanOrEqual(1),
|
|
49
|
-
minConcurrency: ow.optional.number.integer.greaterThanOrEqual(1),
|
|
50
|
-
desiredConcurrency: ow.optional.number.integer.greaterThanOrEqual(1),
|
|
51
|
-
desiredConcurrencyRatio: ow.optional.number.greaterThan(0).lessThan(1),
|
|
52
|
-
scaleUpStepRatio: ow.optional.number.greaterThan(0).lessThan(1),
|
|
53
|
-
scaleDownStepRatio: ow.optional.number.greaterThan(0).lessThan(1),
|
|
54
|
-
loggingIntervalSecs: ow.any(ow.number.greaterThan(0), ow.nullOrUndefined),
|
|
55
|
-
autoscaleIntervalSecs: ow.optional.number.greaterThan(0),
|
|
56
|
-
loadSignals: ow.optional.object,
|
|
57
|
-
snapshotHistorySecs: ow.optional.number.greaterThan(0),
|
|
58
|
-
currentHistorySecs: ow.optional.number.greaterThan(0),
|
|
59
|
-
log: ow.optional.object,
|
|
60
|
-
maxTasksPerMinute: ow.optional.number.integerOrInfinite.greaterThanOrEqual(1),
|
|
61
|
-
}));
|
|
62
|
-
const { maxConcurrency = 200, minConcurrency = 1, desiredConcurrency, desiredConcurrencyRatio = 0.9, scaleUpStepRatio = 0.05, scaleDownStepRatio = 0.05, loggingIntervalSecs = 60, autoscaleIntervalSecs = 10, loadSignals = {}, snapshotHistorySecs, currentHistorySecs, log = serviceLocator.getLogger(), maxTasksPerMinute = Infinity, } = options;
|
|
73
|
+
const { maxConcurrency, minConcurrency, desiredConcurrency, desiredConcurrencyRatio, scaleUpStepRatio, scaleDownStepRatio, loggingIntervalSecs, autoscaleIntervalSecs, loadSignals, snapshotHistorySecs, currentHistorySecs, log, maxTasksPerMinute, } = parseArgument(options, concurrencySystemOptionsSchema, 'ConcurrencySystemOptions');
|
|
63
74
|
this.log = log.child({ prefix: 'ConcurrencySystem' });
|
|
64
75
|
this.desiredConcurrencyRatio = desiredConcurrencyRatio;
|
|
65
76
|
this.scaleUpStepRatio = scaleUpStepRatio;
|
|
@@ -99,7 +110,7 @@ export class ConcurrencySystem {
|
|
|
99
110
|
* If you're not sure, just keep the default value and the concurrency will scale up automatically.
|
|
100
111
|
*/
|
|
101
112
|
set minConcurrency(value) {
|
|
102
|
-
|
|
113
|
+
parseArgument(value, concurrencySchema);
|
|
103
114
|
this.#minConcurrency = value;
|
|
104
115
|
this.clampDesiredConcurrency();
|
|
105
116
|
}
|
|
@@ -116,7 +127,7 @@ export class ConcurrencySystem {
|
|
|
116
127
|
* limit as they settle).
|
|
117
128
|
*/
|
|
118
129
|
set maxConcurrency(value) {
|
|
119
|
-
|
|
130
|
+
parseArgument(value, concurrencySchema);
|
|
120
131
|
this.#maxConcurrency = value;
|
|
121
132
|
this.clampDesiredConcurrency();
|
|
122
133
|
}
|
|
@@ -132,7 +143,7 @@ export class ConcurrencySystem {
|
|
|
132
143
|
* in parallel if there's large enough supply of tasks.
|
|
133
144
|
*/
|
|
134
145
|
set desiredConcurrency(value) {
|
|
135
|
-
|
|
146
|
+
parseArgument(value, concurrencySchema);
|
|
136
147
|
this.#desiredConcurrency = value;
|
|
137
148
|
this.clampDesiredConcurrency();
|
|
138
149
|
}
|
package/configuration.d.ts
CHANGED
|
@@ -6,47 +6,47 @@ export interface ConfigField<T extends z.ZodType = z.ZodType> {
|
|
|
6
6
|
}
|
|
7
7
|
export declare function field<T extends z.ZodType>(schema: T, envVar?: string | string[]): ConfigField<T>;
|
|
8
8
|
/** Zod preprocessor treating `'0'` and `'false'` as falsy. */
|
|
9
|
-
export declare const coerceBoolean: z.
|
|
10
|
-
export declare const coerceNumber: z.
|
|
9
|
+
export declare const coerceBoolean: z.ZodPreprocess<z.ZodBoolean>;
|
|
10
|
+
export declare const coerceNumber: z.ZodPreprocess<z.ZodNumber>;
|
|
11
11
|
export declare const crawleeConfigFields: {
|
|
12
12
|
/** @default 'default' */
|
|
13
13
|
defaultDatasetId: ConfigField<z.ZodDefault<z.ZodString>>;
|
|
14
14
|
/** @default true */
|
|
15
|
-
purgeOnStart: ConfigField<z.ZodDefault<z.
|
|
15
|
+
purgeOnStart: ConfigField<z.ZodDefault<z.ZodPreprocess<z.ZodBoolean>>>;
|
|
16
16
|
/** @default 'default' */
|
|
17
17
|
defaultKeyValueStoreId: ConfigField<z.ZodDefault<z.ZodString>>;
|
|
18
18
|
/** @default 'default' */
|
|
19
19
|
defaultRequestQueueId: ConfigField<z.ZodDefault<z.ZodString>>;
|
|
20
20
|
/** @default 0.95 */
|
|
21
|
-
maxUsedCpuRatio: ConfigField<z.ZodDefault<z.
|
|
21
|
+
maxUsedCpuRatio: ConfigField<z.ZodDefault<z.ZodPreprocess<z.ZodNumber>>>;
|
|
22
22
|
/** @default 0.25 */
|
|
23
|
-
availableMemoryRatio: ConfigField<z.ZodDefault<z.
|
|
24
|
-
memoryMbytes: ConfigField<z.ZodOptional<z.
|
|
23
|
+
availableMemoryRatio: ConfigField<z.ZodDefault<z.ZodPreprocess<z.ZodNumber>>>;
|
|
24
|
+
memoryMbytes: ConfigField<z.ZodOptional<z.ZodPreprocess<z.ZodNumber>>>;
|
|
25
25
|
/** @default 60_000 */
|
|
26
|
-
persistStateIntervalMillis: ConfigField<z.ZodDefault<z.
|
|
26
|
+
persistStateIntervalMillis: ConfigField<z.ZodDefault<z.ZodPreprocess<z.ZodNumber>>>;
|
|
27
27
|
/**
|
|
28
28
|
* Internal safety-net timeout for a single request, in milliseconds. When unset the crawler derives it from
|
|
29
29
|
* the request handler timeout (twice it, and never below 5 minutes).
|
|
30
30
|
*/
|
|
31
|
-
internalTimeoutMillis: ConfigField<z.ZodOptional<z.
|
|
31
|
+
internalTimeoutMillis: ConfigField<z.ZodOptional<z.ZodPreprocess<z.ZodNumber>>>;
|
|
32
32
|
/** @default 1_000 */
|
|
33
|
-
systemInfoIntervalMillis: ConfigField<z.ZodDefault<z.
|
|
33
|
+
systemInfoIntervalMillis: ConfigField<z.ZodDefault<z.ZodPreprocess<z.ZodNumber>>>;
|
|
34
34
|
/** @default 'INPUT' */
|
|
35
35
|
inputKey: ConfigField<z.ZodDefault<z.ZodString>>;
|
|
36
36
|
/** @default true */
|
|
37
|
-
headless: ConfigField<z.ZodDefault<z.
|
|
37
|
+
headless: ConfigField<z.ZodDefault<z.ZodPreprocess<z.ZodBoolean>>>;
|
|
38
38
|
/** @default false */
|
|
39
|
-
xvfb: ConfigField<z.ZodDefault<z.
|
|
39
|
+
xvfb: ConfigField<z.ZodDefault<z.ZodPreprocess<z.ZodBoolean>>>;
|
|
40
40
|
chromeExecutablePath: ConfigField<z.ZodOptional<z.ZodString>>;
|
|
41
41
|
defaultBrowserPath: ConfigField<z.ZodOptional<z.ZodString>>;
|
|
42
42
|
/** @default false */
|
|
43
|
-
disableBrowserSandbox: ConfigField<z.ZodDefault<z.
|
|
44
|
-
logLevel: ConfigField<z.ZodOptional<z.
|
|
43
|
+
disableBrowserSandbox: ConfigField<z.ZodDefault<z.ZodPreprocess<z.ZodBoolean>>>;
|
|
44
|
+
logLevel: ConfigField<z.ZodOptional<z.ZodPreprocess<z.ZodEnum<typeof LogLevel>>>>;
|
|
45
45
|
/** @default true */
|
|
46
|
-
persistStorage: ConfigField<z.ZodDefault<z.
|
|
46
|
+
persistStorage: ConfigField<z.ZodDefault<z.ZodPreprocess<z.ZodBoolean>>>;
|
|
47
47
|
/** @default './storage' */
|
|
48
48
|
storageDir: ConfigField<z.ZodDefault<z.ZodString>>;
|
|
49
|
-
containerized: ConfigField<z.ZodOptional<z.
|
|
49
|
+
containerized: ConfigField<z.ZodOptional<z.ZodPreprocess<z.ZodBoolean>>>;
|
|
50
50
|
};
|
|
51
51
|
export type FieldsInput<F extends Record<string, ConfigField>> = {
|
|
52
52
|
[K in keyof F]?: z.output<F[K]['schema']>;
|
package/configuration.js
CHANGED
|
@@ -36,7 +36,7 @@ const logLevelSchema = z.preprocess((val) => {
|
|
|
36
36
|
if (key in LogLevel)
|
|
37
37
|
return LogLevel[key];
|
|
38
38
|
return val;
|
|
39
|
-
}, z.
|
|
39
|
+
}, z.enum(LogLevel));
|
|
40
40
|
// --- Crawlee config field definitions ---
|
|
41
41
|
export const crawleeConfigFields = {
|
|
42
42
|
/** @default 'default' */
|
|
@@ -196,8 +196,8 @@ export class Configuration {
|
|
|
196
196
|
continue;
|
|
197
197
|
}
|
|
198
198
|
// 4. Schema default (by parsing undefined through the schema)
|
|
199
|
-
const
|
|
200
|
-
values[key] =
|
|
199
|
+
const parsed = fieldDef.schema.safeParse(undefined);
|
|
200
|
+
values[key] = parsed.success ? parsed.data : undefined;
|
|
201
201
|
}
|
|
202
202
|
return values;
|
|
203
203
|
}
|
|
@@ -46,7 +46,7 @@ export declare class ErrorSnapshotter {
|
|
|
46
46
|
*/
|
|
47
47
|
contextCaptureSnapshot(context: BrowserCrawlingContext, fileName: string): Promise<SnapshotResult | undefined>;
|
|
48
48
|
/**
|
|
49
|
-
* Save the HTML snapshot of the page, and return the
|
|
49
|
+
* Save the HTML snapshot of the page, and return the key it was stored under.
|
|
50
50
|
*/
|
|
51
51
|
saveHTMLSnapshot(html: string, keyValueStore: Pick<KeyValueStore, 'setValue'>, fileName: string): Promise<string | undefined>;
|
|
52
52
|
/**
|
|
@@ -77,12 +77,14 @@ export class ErrorSnapshotter {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
/**
|
|
80
|
-
* Save the HTML snapshot of the page, and return the
|
|
80
|
+
* Save the HTML snapshot of the page, and return the key it was stored under.
|
|
81
81
|
*/
|
|
82
82
|
async saveHTMLSnapshot(html, keyValueStore, fileName) {
|
|
83
83
|
try {
|
|
84
84
|
await keyValueStore.setValue(fileName, html, { contentType: 'text/html' });
|
|
85
|
-
|
|
85
|
+
// The record key is `fileName` - returning it with an `.html` suffix (as v3 did,
|
|
86
|
+
// where local storage put the extension in the key) would break `getPublicUrl`.
|
|
87
|
+
return fileName;
|
|
86
88
|
}
|
|
87
89
|
catch {
|
|
88
90
|
return undefined;
|
package/crawlers/statistics.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CrawleeLogger } from '../log.js';
|
|
2
|
-
import
|
|
2
|
+
import { KeyValueStore } from '../storages/key_value_store.js';
|
|
3
3
|
import { ErrorTracker } from './error_tracker.js';
|
|
4
4
|
/**
|
|
5
5
|
* Persistence-related options to control how and when crawler's data gets persisted.
|
package/crawlers/statistics.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import ow from 'ow';
|
|
2
1
|
import { z } from 'zod';
|
|
3
2
|
import { RecoverableState } from '../recoverable_state.js';
|
|
4
3
|
import { serviceLocator } from '../service_locator.js';
|
|
4
|
+
import { KeyValueStore } from '../storages/key_value_store.js';
|
|
5
|
+
import { parseArgument, schemas, validators } from '../validators.js';
|
|
5
6
|
import { ErrorTracker } from './error_tracker.js';
|
|
6
7
|
/**
|
|
7
8
|
* @ignore
|
|
@@ -17,6 +18,16 @@ class Job {
|
|
|
17
18
|
return this.#durationMillis;
|
|
18
19
|
}
|
|
19
20
|
}
|
|
21
|
+
const statisticsOptionsSchema = z.strictObject({
|
|
22
|
+
logIntervalSecs: schemas.anyNumber.default(60),
|
|
23
|
+
logMessage: z.string().default('Statistics'),
|
|
24
|
+
log: validators.logger.optional(),
|
|
25
|
+
keyValueStore: z.instanceof(KeyValueStore).optional(),
|
|
26
|
+
// `schemas.anyObject` passes values through by reference (object schemas return a pruned plain copy).
|
|
27
|
+
persistenceOptions: schemas.anyObject.default(() => ({ enable: true })),
|
|
28
|
+
saveErrorSnapshots: z.boolean().default(false),
|
|
29
|
+
id: z.union([schemas.anyNumber, z.string()]).optional(),
|
|
30
|
+
});
|
|
20
31
|
const errorTrackerConfig = {
|
|
21
32
|
showErrorCode: true,
|
|
22
33
|
showErrorName: true,
|
|
@@ -184,21 +195,10 @@ export class Statistics {
|
|
|
184
195
|
* persistence or error snapshots, share it across sequential runs, or subclass it to track extra fields.
|
|
185
196
|
*/
|
|
186
197
|
constructor(options = {}) {
|
|
187
|
-
|
|
188
|
-
logIntervalSecs: ow.optional.number,
|
|
189
|
-
logMessage: ow.optional.string,
|
|
190
|
-
log: ow.optional.object,
|
|
191
|
-
keyValueStore: ow.optional.object,
|
|
192
|
-
persistenceOptions: ow.optional.object,
|
|
193
|
-
saveErrorSnapshots: ow.optional.boolean,
|
|
194
|
-
id: ow.optional.any(ow.number, ow.string),
|
|
195
|
-
}));
|
|
196
|
-
const { logIntervalSecs = 60, logMessage = 'Statistics', keyValueStore, persistenceOptions = {
|
|
197
|
-
enable: true,
|
|
198
|
-
}, saveErrorSnapshots = false, id, } = options;
|
|
198
|
+
const { logIntervalSecs, logMessage, log, keyValueStore, persistenceOptions, saveErrorSnapshots, id } = parseArgument(options, statisticsOptionsSchema);
|
|
199
199
|
this.id = id ?? String(Statistics.id++);
|
|
200
200
|
this.persistStateKey = `CRAWLEE_CRAWLER_STATISTICS_${this.id}`;
|
|
201
|
-
this.log = (
|
|
201
|
+
this.log = (log ?? serviceLocator.getLogger()).child({ prefix: 'Statistics' });
|
|
202
202
|
this.errorTracker = new ErrorTracker({ ...errorTrackerConfig, saveErrorSnapshots });
|
|
203
203
|
this.errorTrackerRetry = new ErrorTracker({ ...errorTrackerConfig, saveErrorSnapshots });
|
|
204
204
|
this.#logIntervalMillis = logIntervalSecs * 1000;
|
package/debug.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { inspect } from 'node:util';
|
|
2
|
-
import
|
|
2
|
+
import { parseArgument, schemas } from './validators.js';
|
|
3
3
|
/**
|
|
4
4
|
* Creates a standardized debug info from request and response. This info is usually added to dataset under the hidden `#debug` field.
|
|
5
5
|
*
|
|
@@ -12,9 +12,9 @@ import ow from 'ow';
|
|
|
12
12
|
* @internal
|
|
13
13
|
*/
|
|
14
14
|
export function createRequestDebugInfo(request, response = {}, additionalFields = {}) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
parseArgument(request, schemas.anyObject);
|
|
16
|
+
parseArgument(response, schemas.anyObject);
|
|
17
|
+
parseArgument(additionalFields, schemas.anyObject);
|
|
18
18
|
return {
|
|
19
19
|
requestId: request.id,
|
|
20
20
|
url: request.url,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import ow from 'ow';
|
|
2
1
|
import { getDomain } from 'tldts';
|
|
2
|
+
import { z } from 'zod';
|
|
3
3
|
import { Request } from '../request.js';
|
|
4
|
-
import {
|
|
4
|
+
import { parseArgument, schemas } from '../validators.js';
|
|
5
|
+
import { applyRequestTransform, constructUrlPatternObjects, createRequestOptions, filterRequestOptionsByPatterns, urlPatternSchema, } from './shared.js';
|
|
5
6
|
/**
|
|
6
7
|
* The different enqueueing strategies available.
|
|
7
8
|
*
|
|
@@ -54,6 +55,28 @@ export var EnqueueStrategy;
|
|
|
54
55
|
*/
|
|
55
56
|
EnqueueStrategy["SameOrigin"] = "same-origin";
|
|
56
57
|
})(EnqueueStrategy || (EnqueueStrategy = {}));
|
|
58
|
+
// `schemas.anyObject` passes values through by reference (object schemas return a pruned plain
|
|
59
|
+
// copy), so `userData` keeps its identity for the enqueued requests.
|
|
60
|
+
const enqueueLinksOptionsSchema = z.strictObject({
|
|
61
|
+
urls: schemas.arrayOf(z.string(), 'strings'),
|
|
62
|
+
requestManager: schemas.objectWithKeys(['addRequestsBatched']),
|
|
63
|
+
robotsTxtFile: schemas.objectWithKeys(['isAllowed']).optional(),
|
|
64
|
+
respectRobotsTxtFile: z.union([z.boolean(), z.strictObject({ userAgent: z.string().optional() })]).optional(),
|
|
65
|
+
onSkippedRequest: schemas.anyFunction.optional(),
|
|
66
|
+
forefront: z.boolean().optional(),
|
|
67
|
+
skipNavigation: z.boolean().optional(),
|
|
68
|
+
sessionId: z.string().optional(),
|
|
69
|
+
limit: schemas.anyNumber.optional(),
|
|
70
|
+
selector: z.string().optional(),
|
|
71
|
+
baseUrl: z.string().optional(),
|
|
72
|
+
userData: schemas.anyObject.optional(),
|
|
73
|
+
label: z.string().optional(),
|
|
74
|
+
include: schemas.arrayOf(urlPatternSchema, 'URL patterns').min(1).optional(),
|
|
75
|
+
exclude: schemas.arrayOf(urlPatternSchema, 'URL patterns').optional(),
|
|
76
|
+
transformRequestFunction: schemas.anyFunction.optional(),
|
|
77
|
+
strategy: z.enum(EnqueueStrategy).optional(),
|
|
78
|
+
waitForAllRequestsToBeAdded: z.boolean().optional(),
|
|
79
|
+
});
|
|
57
80
|
/**
|
|
58
81
|
* This function enqueues the urls provided to the {@link RequestQueue} provided. If you want to automatically find and enqueue links,
|
|
59
82
|
* you should use the context-aware `enqueueLinks` function provided on the crawler contexts.
|
|
@@ -84,37 +107,17 @@ export async function enqueueLinks(options) {
|
|
|
84
107
|
'Check out our guide on how to use enqueueLinks() here: https://crawlee.dev/js/docs/examples/crawl-relative-links',
|
|
85
108
|
].join('\n'));
|
|
86
109
|
}
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
urls: ow.array.ofType(ow.string),
|
|
90
|
-
requestManager: ow.object.hasKeys('addRequestsBatched'),
|
|
91
|
-
robotsTxtFile: ow.optional.object.hasKeys('isAllowed'),
|
|
92
|
-
respectRobotsTxtFile: ow.optional.any(ow.boolean, ow.object.exactShape({ userAgent: ow.optional.string })),
|
|
93
|
-
onSkippedRequest: ow.optional.function,
|
|
94
|
-
forefront: ow.optional.boolean,
|
|
95
|
-
skipNavigation: ow.optional.boolean,
|
|
96
|
-
sessionId: ow.optional.string,
|
|
97
|
-
limit: ow.optional.number,
|
|
98
|
-
selector: ow.optional.string,
|
|
99
|
-
baseUrl: ow.optional.string,
|
|
100
|
-
userData: ow.optional.object,
|
|
101
|
-
label: ow.optional.string,
|
|
102
|
-
include: ow.optional.array.minLength(1).ofType(urlPatternValidator),
|
|
103
|
-
exclude: ow.optional.array.ofType(urlPatternValidator),
|
|
104
|
-
transformRequestFunction: ow.optional.function,
|
|
105
|
-
strategy: ow.optional.string.oneOf(Object.values(EnqueueStrategy)),
|
|
106
|
-
waitForAllRequestsToBeAdded: ow.optional.boolean,
|
|
107
|
-
}));
|
|
108
|
-
const { requestManager, limit, urls, include, exclude, transformRequestFunction, forefront, waitForAllRequestsToBeAdded, robotsTxtFile, onSkippedRequest, } = options;
|
|
110
|
+
const parsedOptions = parseArgument(options, enqueueLinksOptionsSchema, 'EnqueueLinksOptions');
|
|
111
|
+
const { requestManager, limit, urls, include, exclude, transformRequestFunction, forefront, waitForAllRequestsToBeAdded, robotsTxtFile, onSkippedRequest, } = parsedOptions;
|
|
109
112
|
const urlExcludePatternObjects = exclude?.length ? constructUrlPatternObjects(exclude) : [];
|
|
110
113
|
const urlPatternObjects = include?.length ? constructUrlPatternObjects(include) : [];
|
|
111
114
|
// The strategy always applies, even when `include` patterns are provided - the two are AND-ed together
|
|
112
115
|
// (a URL must match an `include` pattern *and* satisfy the strategy). This mirrors crawlee-python.
|
|
113
|
-
|
|
116
|
+
parsedOptions.strategy ??= EnqueueStrategy.SameHostname;
|
|
114
117
|
const enqueueStrategyPatterns = [];
|
|
115
|
-
if (
|
|
116
|
-
const url = new URL(
|
|
117
|
-
switch (
|
|
118
|
+
if (parsedOptions.baseUrl) {
|
|
119
|
+
const url = new URL(parsedOptions.baseUrl);
|
|
120
|
+
switch (parsedOptions.strategy) {
|
|
118
121
|
case EnqueueStrategy.SameHostname:
|
|
119
122
|
// We need to get the origin of the passed in domain in the event someone sets baseUrl
|
|
120
123
|
// to an url like https://example.com/deep/default/path and one of the found urls is an
|
|
@@ -157,9 +160,11 @@ export async function enqueueLinks(options) {
|
|
|
157
160
|
}));
|
|
158
161
|
}
|
|
159
162
|
}
|
|
160
|
-
let requestOptions = createRequestOptions(urls,
|
|
161
|
-
if (robotsTxtFile &&
|
|
162
|
-
const robotsUserAgent = typeof
|
|
163
|
+
let requestOptions = createRequestOptions(urls, parsedOptions);
|
|
164
|
+
if (robotsTxtFile && parsedOptions.respectRobotsTxtFile !== false) {
|
|
165
|
+
const robotsUserAgent = typeof parsedOptions.respectRobotsTxtFile === 'object'
|
|
166
|
+
? (parsedOptions.respectRobotsTxtFile.userAgent ?? '*')
|
|
167
|
+
: '*';
|
|
163
168
|
const skippedRequests = [];
|
|
164
169
|
requestOptions = requestOptions.filter((request) => {
|
|
165
170
|
if (robotsTxtFile.isAllowed(request.url, robotsUserAgent)) {
|
|
@@ -175,13 +180,13 @@ export async function enqueueLinks(options) {
|
|
|
175
180
|
// Step 1: Filter request options by exclude patterns, user include patterns, and strategy patterns.
|
|
176
181
|
let filteredOptions;
|
|
177
182
|
if (urlPatternObjects.length === 0) {
|
|
178
|
-
filteredOptions = filterRequestOptionsByPatterns(requestOptions, enqueueStrategyPatterns.length > 0 ? enqueueStrategyPatterns : undefined, urlExcludePatternObjects,
|
|
183
|
+
filteredOptions = filterRequestOptionsByPatterns(requestOptions, enqueueStrategyPatterns.length > 0 ? enqueueStrategyPatterns : undefined, urlExcludePatternObjects, parsedOptions.strategy, (url) => skippedRequests.push(url));
|
|
179
184
|
}
|
|
180
185
|
else {
|
|
181
186
|
// Filter by user patterns first (with exclude)
|
|
182
|
-
const afterUserPatterns = filterRequestOptionsByPatterns(requestOptions, urlPatternObjects, urlExcludePatternObjects,
|
|
187
|
+
const afterUserPatterns = filterRequestOptionsByPatterns(requestOptions, urlPatternObjects, urlExcludePatternObjects, parsedOptions.strategy, (url) => skippedRequests.push(url));
|
|
183
188
|
// ...then filter by the enqueue links strategy (making this an AND check)
|
|
184
|
-
filteredOptions = filterRequestOptionsByPatterns(afterUserPatterns, enqueueStrategyPatterns.length > 0 ? enqueueStrategyPatterns : undefined, [],
|
|
189
|
+
filteredOptions = filterRequestOptionsByPatterns(afterUserPatterns, enqueueStrategyPatterns.length > 0 ? enqueueStrategyPatterns : undefined, [], parsedOptions.strategy, (url) => skippedRequests.push(url));
|
|
185
190
|
}
|
|
186
191
|
await reportSkippedRequests(skippedRequests.map((url) => ({ url })), 'filters');
|
|
187
192
|
// Step 2: Apply transformRequestFunction on request options - it has the highest priority
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Awaitable } from '@crawlee/types';
|
|
2
|
+
import { z } from 'zod';
|
|
2
3
|
import type { RequestOptions } from '../request.js';
|
|
3
4
|
import type { EnqueueLinksOptions } from './enqueue_links.js';
|
|
4
5
|
export { tryAbsoluteURL } from '@crawlee/utils/internal';
|
|
@@ -16,6 +17,11 @@ export interface RegExpObject {
|
|
|
16
17
|
export type RegExpInput = RegExp | RegExpObject;
|
|
17
18
|
/** Unified URL pattern input — accepts glob strings, glob objects, RegExp instances, or regexp objects. */
|
|
18
19
|
export type UrlPatternInput = GlobInput | RegExpInput;
|
|
20
|
+
/**
|
|
21
|
+
* Accepts one {@link UrlPatternInput} — a glob string, a RegExp instance, or a `{ glob }` / `{ regexp }` object.
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
export declare const urlPatternSchema: z.ZodType<UrlPatternInput>;
|
|
19
25
|
export type SkippedRequestReason = 'robotsTxt' | 'limit' | 'enqueueLimit' | 'filters' | 'transform' | 'redirect' | 'depth';
|
|
20
26
|
export type SkippedRequestCallback = (args: {
|
|
21
27
|
url: string;
|
package/enqueue_links/shared.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { URL } from 'node:url';
|
|
2
2
|
import { Minimatch } from 'minimatch';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { schemas } from '../validators.js';
|
|
3
5
|
export { tryAbsoluteURL } from '@crawlee/utils/internal';
|
|
4
6
|
const MAX_ENQUEUE_LINKS_CACHE_SIZE = 1000;
|
|
5
7
|
/**
|
|
@@ -8,6 +10,16 @@ const MAX_ENQUEUE_LINKS_CACHE_SIZE = 1000;
|
|
|
8
10
|
* @ignore
|
|
9
11
|
*/
|
|
10
12
|
const enqueueLinksPatternCache = new Map();
|
|
13
|
+
/**
|
|
14
|
+
* Accepts one {@link UrlPatternInput} — a glob string, a RegExp instance, or a `{ glob }` / `{ regexp }` object.
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
export const urlPatternSchema = z.union([
|
|
18
|
+
z.string(),
|
|
19
|
+
z.instanceof(RegExp),
|
|
20
|
+
schemas.objectWithKeys(['glob']),
|
|
21
|
+
schemas.objectWithKeys(['regexp']),
|
|
22
|
+
]);
|
|
11
23
|
/**
|
|
12
24
|
* @ignore
|
|
13
25
|
*/
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { parseArgument, schemas } from '@crawlee/utils/internal';
|
|
3
3
|
import { BaseClient } from './common/base-client.js';
|
|
4
4
|
/**
|
|
5
5
|
* This is what API returns in the x-apify-pagination-limit
|
|
@@ -49,13 +49,7 @@ export class DatasetBackend extends BaseClient {
|
|
|
49
49
|
this.updateTimestamps(true);
|
|
50
50
|
}
|
|
51
51
|
getData(options = {}) {
|
|
52
|
-
const { desc, limit, offset } =
|
|
53
|
-
.object({
|
|
54
|
-
desc: s.boolean().optional(),
|
|
55
|
-
limit: s.number().int().optional(),
|
|
56
|
-
offset: s.number().int().optional(),
|
|
57
|
-
})
|
|
58
|
-
.parse(options);
|
|
52
|
+
const { desc, limit, offset } = parseArgument(options, schemas.datasetListItemsOptions);
|
|
59
53
|
return this.getDataPage({
|
|
60
54
|
desc,
|
|
61
55
|
offset: offset ?? 0,
|
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { parseArgument, schemas } from '@crawlee/utils/internal';
|
|
3
|
+
import { z } from 'zod';
|
|
3
4
|
import { isStream, toBuffer } from '../utils.js';
|
|
4
5
|
import { BaseClient } from './common/base-client.js';
|
|
5
6
|
import mime from 'mime-types';
|
|
6
7
|
const DEFAULT_LOCAL_FILE_EXTENSION = 'bin';
|
|
8
|
+
const keySchema = z.string();
|
|
9
|
+
const inputRecordSchema = z.object({
|
|
10
|
+
key: z.string().min(1),
|
|
11
|
+
value: z.union([
|
|
12
|
+
z.null(),
|
|
13
|
+
z.string(),
|
|
14
|
+
z.number(),
|
|
15
|
+
z.instanceof(Buffer),
|
|
16
|
+
z.instanceof(ArrayBuffer),
|
|
17
|
+
schemas.typedArray,
|
|
18
|
+
// only checks the value is an actual object, not null, nor array
|
|
19
|
+
schemas.plainObject,
|
|
20
|
+
]),
|
|
21
|
+
contentType: z.string().min(1).optional(),
|
|
22
|
+
});
|
|
7
23
|
/**
|
|
8
24
|
* Key under which a run's input is stored in the default key-value store. Matches Crawlee's default
|
|
9
25
|
* `inputKey` (`CRAWLEE_INPUT_KEY`) and the `INPUT` files `FileSystemStorageBackend` preserves on purge.
|
|
@@ -58,13 +74,7 @@ export class KeyValueStoreBackend extends BaseClient {
|
|
|
58
74
|
this.updateTimestamps(true);
|
|
59
75
|
}
|
|
60
76
|
async listKeys(options = {}) {
|
|
61
|
-
const { prefix, exclusiveStartKey, limit } =
|
|
62
|
-
.object({
|
|
63
|
-
prefix: s.string().optional(),
|
|
64
|
-
exclusiveStartKey: s.string().optional(),
|
|
65
|
-
limit: s.number().int().greaterThan(0).optional(),
|
|
66
|
-
})
|
|
67
|
-
.parse(options);
|
|
77
|
+
const { prefix, exclusiveStartKey, limit } = parseArgument(options, schemas.keyValueStoreListKeysOptions);
|
|
68
78
|
const items = [];
|
|
69
79
|
for (const record of this.#keyValueEntries.values()) {
|
|
70
80
|
const size = Buffer.byteLength(record.value);
|
|
@@ -104,7 +114,7 @@ export class KeyValueStoreBackend extends BaseClient {
|
|
|
104
114
|
* @param key The key of the record to generate the public URL for.
|
|
105
115
|
*/
|
|
106
116
|
async getPublicUrl(key) {
|
|
107
|
-
|
|
117
|
+
parseArgument(key, keySchema);
|
|
108
118
|
return undefined;
|
|
109
119
|
}
|
|
110
120
|
/**
|
|
@@ -114,11 +124,11 @@ export class KeyValueStoreBackend extends BaseClient {
|
|
|
114
124
|
* @returns `true` if the record exists, `false` if it does not.
|
|
115
125
|
*/
|
|
116
126
|
async recordExists(key) {
|
|
117
|
-
|
|
127
|
+
parseArgument(key, keySchema);
|
|
118
128
|
return this.#keyValueEntries.has(key);
|
|
119
129
|
}
|
|
120
130
|
async getValue(key) {
|
|
121
|
-
|
|
131
|
+
parseArgument(key, keySchema);
|
|
122
132
|
const entry = this.#keyValueEntries.get(key);
|
|
123
133
|
if (!entry) {
|
|
124
134
|
return undefined;
|
|
@@ -136,20 +146,7 @@ export class KeyValueStoreBackend extends BaseClient {
|
|
|
136
146
|
return record;
|
|
137
147
|
}
|
|
138
148
|
async setValue(record) {
|
|
139
|
-
|
|
140
|
-
key: s.string().lengthGreaterThan(0),
|
|
141
|
-
value: s.union([
|
|
142
|
-
s.null(),
|
|
143
|
-
s.string(),
|
|
144
|
-
s.number(),
|
|
145
|
-
s.instance(Buffer),
|
|
146
|
-
s.instance(ArrayBuffer),
|
|
147
|
-
s.typedArray(),
|
|
148
|
-
// disabling validation will make shapeshift only check the object given is an actual object, not null, nor array
|
|
149
|
-
s.object({}).setValidationEnabled(false),
|
|
150
|
-
]),
|
|
151
|
-
contentType: s.string().lengthGreaterThan(0).optional(),
|
|
152
|
-
}).parse(record);
|
|
149
|
+
parseArgument(record, inputRecordSchema);
|
|
153
150
|
const { key } = record;
|
|
154
151
|
let { value } = record;
|
|
155
152
|
// The frontend (KeyValueStore codec) serializes the value and resolves its content type
|
|
@@ -180,7 +177,7 @@ export class KeyValueStoreBackend extends BaseClient {
|
|
|
180
177
|
this.updateTimestamps(true);
|
|
181
178
|
}
|
|
182
179
|
async deleteValue(key) {
|
|
183
|
-
|
|
180
|
+
parseArgument(key, keySchema);
|
|
184
181
|
if (this.#keyValueEntries.has(key)) {
|
|
185
182
|
this.#keyValueEntries.delete(key);
|
|
186
183
|
this.updateTimestamps(true);
|