@crawlee/core 4.0.0-beta.122 → 4.0.0-beta.123
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/autoscaling/concurrency_system.d.ts +2 -2
- package/autoscaling/index.d.ts +1 -1
- package/autoscaling/index.js +1 -1
- package/autoscaling/load_signal.d.ts +7 -6
- package/autoscaling/load_signal.js +2 -1
- package/autoscaling/snapshotter.d.ts +6 -6
- package/autoscaling/snapshotter.js +9 -9
- package/autoscaling/{client_load_signal.d.ts → storage_backend_load_signal.d.ts} +13 -12
- package/autoscaling/{client_load_signal.js → storage_backend_load_signal.js} +11 -11
- package/autoscaling/system_status.d.ts +8 -8
- package/autoscaling/system_status.js +2 -2
- package/crawlers/crawler_commons.d.ts +8 -54
- package/enqueue_links/enqueue_links.d.ts +33 -61
- package/enqueue_links/enqueue_links.js +35 -157
- package/enqueue_links/shared.d.ts +11 -4
- package/enqueue_links/shared.js +16 -1
- package/package.json +6 -6
- package/request.d.ts +2 -2
- package/router.d.ts +5 -5
|
@@ -53,8 +53,8 @@ export interface ConcurrencySystemOptions {
|
|
|
53
53
|
autoscaleIntervalSecs?: number;
|
|
54
54
|
/**
|
|
55
55
|
* The signals that tell the system whether the machine is overloaded: per-resource tuning for the built-in four
|
|
56
|
-
* (memory, event loop, CPU,
|
|
57
|
-
* your own. See {@link LoadSignalsOptions}.
|
|
56
|
+
* (memory, event loop, CPU, storage backend) plus any {@link LoadSignalsOptions.custom|`custom`}
|
|
57
|
+
* implementations of your own. See {@link LoadSignalsOptions}.
|
|
58
58
|
*/
|
|
59
59
|
loadSignals?: LoadSignalsOptions;
|
|
60
60
|
/**
|
package/autoscaling/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export * from './autoscaled_pool.js';
|
|
2
2
|
export * from './concurrency_system.js';
|
|
3
|
-
export * from './client_load_signal.js';
|
|
4
3
|
export * from './cpu_load_signal.js';
|
|
5
4
|
export * from './event_loop_load_signal.js';
|
|
6
5
|
export * from './load_signal.js';
|
|
7
6
|
export * from './memory_load_signal.js';
|
|
8
7
|
export * from './snapshotter.js';
|
|
8
|
+
export * from './storage_backend_load_signal.js';
|
|
9
9
|
export * from './system_status.js';
|
package/autoscaling/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export * from './autoscaled_pool.js';
|
|
2
2
|
export * from './concurrency_system.js';
|
|
3
|
-
export * from './client_load_signal.js';
|
|
4
3
|
export * from './cpu_load_signal.js';
|
|
5
4
|
export * from './event_loop_load_signal.js';
|
|
6
5
|
export * from './load_signal.js';
|
|
7
6
|
export * from './memory_load_signal.js';
|
|
8
7
|
export * from './snapshotter.js';
|
|
8
|
+
export * from './storage_backend_load_signal.js';
|
|
9
9
|
export * from './system_status.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { LoadSignalInfo } from './system_status.js';
|
|
2
2
|
/**
|
|
3
3
|
* A snapshot of a resource's overload state at a point in time.
|
|
4
4
|
*/
|
|
@@ -22,7 +22,7 @@ export interface LoadSignalStartContext {
|
|
|
22
22
|
* A signal that reports whether a particular resource is overloaded. The {@link ConcurrencySystem} aggregates
|
|
23
23
|
* several of them — if any one reports overload, the system is overloaded.
|
|
24
24
|
*
|
|
25
|
-
* The built-in signals cover memory, CPU, event loop and storage
|
|
25
|
+
* The built-in signals cover memory, CPU, event loop and storage backend rate limits. Implement this interface to add
|
|
26
26
|
* your own (navigation timeouts, proxy health, …) and pass them via
|
|
27
27
|
* {@link LoadSignalsOptions.custom|`loadSignals.custom`}; {@link SnapshotStore} does the time-windowed
|
|
28
28
|
* bookkeeping if you want it. Each built-in is also a public class, so one can be *wrapped* rather than reimplemented
|
|
@@ -32,8 +32,8 @@ export interface LoadSignal {
|
|
|
32
32
|
/**
|
|
33
33
|
* This signal's key in the reported {@link SystemInfo}, also used in logging — so it must be unique among the
|
|
34
34
|
* signals of one {@link ConcurrencySystem}, which throws on a duplicate. The four built-in names (`memInfo`,
|
|
35
|
-
* `eventLoopInfo`, `cpuInfo`, `
|
|
36
|
-
* `loadSignalInfo` bag; taking one over means switching that built-in off.
|
|
35
|
+
* `eventLoopInfo`, `cpuInfo`, `storageBackendInfo`) land in the correspondingly named `SystemInfo` fields rather
|
|
36
|
+
* than the `loadSignalInfo` bag; taking one over means switching that built-in off.
|
|
37
37
|
*/
|
|
38
38
|
readonly name: string;
|
|
39
39
|
/**
|
|
@@ -79,7 +79,8 @@ export declare class SnapshotStore<T extends LoadSnapshot = LoadSnapshot> {
|
|
|
79
79
|
getSample(sampleDurationMillis?: number): T[];
|
|
80
80
|
/**
|
|
81
81
|
* Direct, unwindowed access to the underlying array — used by signals whose handler needs the previous snapshot
|
|
82
|
-
* to compute a delta (e.g. the event loop and
|
|
82
|
+
* to compute a delta (e.g. the event loop and storage backend signals read the last entry to measure change since
|
|
83
|
+
* it).
|
|
83
84
|
*/
|
|
84
85
|
getAll(): T[];
|
|
85
86
|
/**
|
|
@@ -96,4 +97,4 @@ export declare class SnapshotStore<T extends LoadSnapshot = LoadSnapshot> {
|
|
|
96
97
|
* evaluation logic used by `SystemStatus` for all signal types.
|
|
97
98
|
* @internal
|
|
98
99
|
*/
|
|
99
|
-
export declare function evaluateLoadSignalSample(sample: LoadSnapshot[], overloadedRatio: number):
|
|
100
|
+
export declare function evaluateLoadSignalSample(sample: LoadSnapshot[], overloadedRatio: number): LoadSignalInfo;
|
|
@@ -57,7 +57,8 @@ export class SnapshotStore {
|
|
|
57
57
|
}
|
|
58
58
|
/**
|
|
59
59
|
* Direct, unwindowed access to the underlying array — used by signals whose handler needs the previous snapshot
|
|
60
|
-
* to compute a delta (e.g. the event loop and
|
|
60
|
+
* to compute a delta (e.g. the event loop and storage backend signals read the last entry to measure change since
|
|
61
|
+
* it).
|
|
61
62
|
*/
|
|
62
63
|
getAll() {
|
|
63
64
|
return this.#snapshots;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { ClientLoadSignalOptions } from './client_load_signal.js';
|
|
2
1
|
import type { CpuLoadSignalOptions } from './cpu_load_signal.js';
|
|
3
2
|
import type { EventLoopLoadSignalOptions } from './event_loop_load_signal.js';
|
|
4
3
|
import type { LoadSignal, LoadSignalStartContext } from './load_signal.js';
|
|
5
4
|
import type { MemoryLoadSignalOptions } from './memory_load_signal.js';
|
|
5
|
+
import type { StorageBackendLoadSignalOptions } from './storage_backend_load_signal.js';
|
|
6
6
|
/**
|
|
7
7
|
* The load signals a {@link ConcurrencySystem} watches to decide whether the machine is overloaded.
|
|
8
8
|
*
|
|
@@ -31,11 +31,11 @@ export interface LoadSignalsOptions {
|
|
|
31
31
|
*/
|
|
32
32
|
cpu?: CpuLoadSignalOptions | false;
|
|
33
33
|
/**
|
|
34
|
-
* Tuning for the built-in {@link
|
|
35
|
-
* `false` to switch it off — worth doing when the storage backend reports no rate-limit statistics, since the
|
|
34
|
+
* Tuning for the built-in {@link StorageBackendLoadSignal} (snapshot interval + error limit + overload ratio),
|
|
35
|
+
* or `false` to switch it off — worth doing when the storage backend reports no rate-limit statistics, since the
|
|
36
36
|
* signal otherwise polls it every second to no purpose.
|
|
37
37
|
*/
|
|
38
|
-
|
|
38
|
+
storageBackend?: StorageBackendLoadSignalOptions | false;
|
|
39
39
|
/**
|
|
40
40
|
* Additional {@link LoadSignal} implementations — e.g. navigation timeouts or proxy health — evaluated
|
|
41
41
|
* alongside the built-in four. If any signal reports overload, the system counts as overloaded. Their lifecycle
|
|
@@ -53,8 +53,8 @@ export interface LoadSignalsOptions {
|
|
|
53
53
|
export type SnapshotterOptions = Omit<LoadSignalsOptions, 'custom'>;
|
|
54
54
|
/**
|
|
55
55
|
* Owns the four built-in {@link LoadSignal} instances — {@link MemoryLoadSignal},
|
|
56
|
-
* {@link EventLoopLoadSignal}, {@link CpuLoadSignal} and {@link
|
|
57
|
-
* that were not switched off and driving their shared lifecycle.
|
|
56
|
+
* {@link EventLoopLoadSignal}, {@link CpuLoadSignal} and {@link StorageBackendLoadSignal} — constructing
|
|
57
|
+
* the ones that were not switched off and driving their shared lifecycle.
|
|
58
58
|
*
|
|
59
59
|
* Configured indirectly through {@link ConcurrencySystemOptions.loadSignals|`loadSignals`}, whose per-signal bags
|
|
60
60
|
* are simply forwarded to the corresponding constructor.
|
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import { ClientLoadSignal } from './client_load_signal.js';
|
|
2
1
|
import { CpuLoadSignal } from './cpu_load_signal.js';
|
|
3
2
|
import { EventLoopLoadSignal } from './event_loop_load_signal.js';
|
|
4
3
|
import { MemoryLoadSignal } from './memory_load_signal.js';
|
|
4
|
+
import { StorageBackendLoadSignal } from './storage_backend_load_signal.js';
|
|
5
5
|
/**
|
|
6
6
|
* Owns the four built-in {@link LoadSignal} instances — {@link MemoryLoadSignal},
|
|
7
|
-
* {@link EventLoopLoadSignal}, {@link CpuLoadSignal} and {@link
|
|
8
|
-
* that were not switched off and driving their shared lifecycle.
|
|
7
|
+
* {@link EventLoopLoadSignal}, {@link CpuLoadSignal} and {@link StorageBackendLoadSignal} — constructing
|
|
8
|
+
* the ones that were not switched off and driving their shared lifecycle.
|
|
9
9
|
*
|
|
10
10
|
* Configured indirectly through {@link ConcurrencySystemOptions.loadSignals|`loadSignals`}, whose per-signal bags
|
|
11
11
|
* are simply forwarded to the corresponding constructor.
|
|
12
12
|
* @internal
|
|
13
13
|
*/
|
|
14
14
|
export class Snapshotter {
|
|
15
|
-
// Absent when switched off through the corresponding option (e.g. `
|
|
15
|
+
// Absent when switched off through the corresponding option (e.g. `storageBackend: false`).
|
|
16
16
|
#memorySignal;
|
|
17
17
|
#eventLoopSignal;
|
|
18
18
|
#cpuSignal;
|
|
19
|
-
#
|
|
19
|
+
#storageBackendSignal;
|
|
20
20
|
/**
|
|
21
21
|
* Returns the enabled built-in signals, so `SystemStatus` can iterate them alongside any custom `LoadSignal`
|
|
22
22
|
* instances. Signals switched off through the options are simply absent — the system status reports them as
|
|
@@ -27,7 +27,7 @@ export class Snapshotter {
|
|
|
27
27
|
this.#memorySignal,
|
|
28
28
|
this.#eventLoopSignal,
|
|
29
29
|
this.#cpuSignal,
|
|
30
|
-
this.#
|
|
30
|
+
this.#storageBackendSignal,
|
|
31
31
|
];
|
|
32
32
|
return builtin.filter((signal) => signal !== undefined);
|
|
33
33
|
}
|
|
@@ -35,7 +35,7 @@ export class Snapshotter {
|
|
|
35
35
|
* @param [options] All `Snapshotter` configuration options.
|
|
36
36
|
*/
|
|
37
37
|
constructor(options = {}) {
|
|
38
|
-
const { memory = {}, eventLoop = {}, cpu = {},
|
|
38
|
+
const { memory = {}, eventLoop = {}, cpu = {}, storageBackend = {} } = options;
|
|
39
39
|
// Each signal resolves its own ambient dependencies when started, and is told the window it will be sampled
|
|
40
40
|
// over then too - so there is nothing to thread in here beyond the caller's tuning.
|
|
41
41
|
if (memory !== false)
|
|
@@ -44,8 +44,8 @@ export class Snapshotter {
|
|
|
44
44
|
this.#eventLoopSignal = new EventLoopLoadSignal(eventLoop);
|
|
45
45
|
if (cpu !== false)
|
|
46
46
|
this.#cpuSignal = new CpuLoadSignal(cpu);
|
|
47
|
-
if (
|
|
48
|
-
this.#
|
|
47
|
+
if (storageBackend !== false)
|
|
48
|
+
this.#storageBackendSignal = new StorageBackendLoadSignal(storageBackend);
|
|
49
49
|
}
|
|
50
50
|
/**
|
|
51
51
|
* Starts capturing snapshots at configured intervals. The `context` carries the sample window the signals will
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import type { LoadSignal, LoadSignalStartContext, LoadSnapshot } from './load_signal.js';
|
|
2
2
|
/**
|
|
3
|
-
* A snapshot produced by the built-in
|
|
3
|
+
* A snapshot produced by the built-in storage backend (rate-limit) signal.
|
|
4
4
|
* @internal
|
|
5
5
|
*/
|
|
6
|
-
export interface
|
|
6
|
+
export interface StorageBackendSnapshot extends LoadSnapshot {
|
|
7
7
|
rateLimitErrorCount: number;
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
|
-
* Tuning for the built-in **
|
|
11
|
-
*
|
|
10
|
+
* Tuning for the built-in **storage backend** (rate-limit) load signal, as accepted both by
|
|
11
|
+
* {@link StorageBackendLoadSignal} and by the
|
|
12
|
+
* {@link LoadSignalsOptions.storageBackend|`storageBackend`} shorthand on {@link LoadSignalsOptions}.
|
|
12
13
|
*/
|
|
13
|
-
export interface
|
|
14
|
+
export interface StorageBackendLoadSignalOptions {
|
|
14
15
|
/**
|
|
15
|
-
* Defines the interval of checking the current state of the
|
|
16
|
+
* Defines the interval of checking the current state of the storage backend, in seconds.
|
|
16
17
|
* @default 1
|
|
17
18
|
*/
|
|
18
19
|
snapshotIntervalSecs?: number;
|
|
@@ -22,7 +23,7 @@ export interface ClientLoadSignalOptions {
|
|
|
22
23
|
*/
|
|
23
24
|
maxErrors?: number;
|
|
24
25
|
/**
|
|
25
|
-
* Maximum ratio of overloaded snapshots in a sample before the
|
|
26
|
+
* Maximum ratio of overloaded snapshots in a sample before the storage backend counts as overloaded.
|
|
26
27
|
* @default 0.3
|
|
27
28
|
*/
|
|
28
29
|
overloadedRatio?: number;
|
|
@@ -33,16 +34,16 @@ export interface ClientLoadSignalOptions {
|
|
|
33
34
|
*
|
|
34
35
|
* Built by default; construct one yourself only to wrap or adapt it — see {@link LoadSignal}.
|
|
35
36
|
*
|
|
36
|
-
* Switch it off entirely ({@link LoadSignalsOptions.
|
|
37
|
-
* rate-limit statistics, since it otherwise polls it every second to no purpose.
|
|
37
|
+
* Switch it off entirely ({@link LoadSignalsOptions.storageBackend|`storageBackend: false`}) if the storage backend
|
|
38
|
+
* reports no rate-limit statistics, since it otherwise polls it every second to no purpose.
|
|
38
39
|
*
|
|
39
40
|
* @category Scaling
|
|
40
41
|
*/
|
|
41
|
-
export declare class
|
|
42
|
+
export declare class StorageBackendLoadSignal implements LoadSignal {
|
|
42
43
|
#private;
|
|
43
|
-
readonly name = "
|
|
44
|
+
readonly name = "storageBackendInfo";
|
|
44
45
|
readonly overloadedRatio: number;
|
|
45
|
-
constructor(options?:
|
|
46
|
+
constructor(options?: StorageBackendLoadSignalOptions);
|
|
46
47
|
start(context: LoadSignalStartContext): Promise<void>;
|
|
47
48
|
stop(): Promise<void>;
|
|
48
49
|
getSample(sampleDurationMillis?: number): LoadSnapshot[];
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
import { betterClearInterval, betterSetInterval } from '@apify/utilities';
|
|
2
2
|
import { serviceLocator } from '../service_locator.js';
|
|
3
3
|
import { SnapshotStore } from './load_signal.js';
|
|
4
|
-
const
|
|
4
|
+
const RATE_LIMIT_ERROR_RETRY_COUNT = 2;
|
|
5
5
|
/**
|
|
6
6
|
* Periodically checks the storage backend for rate-limit errors (HTTP 429) and reports overload when the error delta
|
|
7
7
|
* exceeds a threshold.
|
|
8
8
|
*
|
|
9
9
|
* Built by default; construct one yourself only to wrap or adapt it — see {@link LoadSignal}.
|
|
10
10
|
*
|
|
11
|
-
* Switch it off entirely ({@link LoadSignalsOptions.
|
|
12
|
-
* rate-limit statistics, since it otherwise polls it every second to no purpose.
|
|
11
|
+
* Switch it off entirely ({@link LoadSignalsOptions.storageBackend|`storageBackend: false`}) if the storage backend
|
|
12
|
+
* reports no rate-limit statistics, since it otherwise polls it every second to no purpose.
|
|
13
13
|
*
|
|
14
14
|
* @category Scaling
|
|
15
15
|
*/
|
|
16
|
-
export class
|
|
17
|
-
name = '
|
|
16
|
+
export class StorageBackendLoadSignal {
|
|
17
|
+
name = 'storageBackendInfo';
|
|
18
18
|
overloadedRatio;
|
|
19
19
|
#store = new SnapshotStore();
|
|
20
20
|
#intervalMillis;
|
|
21
21
|
#maxErrors;
|
|
22
22
|
#interval;
|
|
23
|
-
#
|
|
23
|
+
#storageBackend;
|
|
24
24
|
constructor(options = {}) {
|
|
25
25
|
this.overloadedRatio = options.overloadedRatio ?? 0.3;
|
|
26
26
|
this.#intervalMillis = (options.snapshotIntervalSecs ?? 1) * 1000;
|
|
@@ -30,18 +30,18 @@ export class ClientLoadSignal {
|
|
|
30
30
|
async start(context) {
|
|
31
31
|
this.#store.useSampleWindow(context.maxSampleWindowMillis);
|
|
32
32
|
// A new session starts from a clean slate, or its first measurement diffs the error count against the previous
|
|
33
|
-
// session's — possibly against a different backend, since
|
|
33
|
+
// session's — possibly against a different backend, since it is resolved afresh just below.
|
|
34
34
|
this.#store.clear();
|
|
35
35
|
// Resolved here rather than in the constructor, where asking for the backend would instantiate a default one
|
|
36
36
|
// as a side effect - long before the crawler that owns the run has had a chance to register its own.
|
|
37
|
-
this.#
|
|
37
|
+
this.#storageBackend = serviceLocator.getStorageBackend();
|
|
38
38
|
this.#interval = betterSetInterval(this.handle, this.#intervalMillis);
|
|
39
39
|
}
|
|
40
40
|
async stop() {
|
|
41
41
|
if (this.#interval)
|
|
42
42
|
betterClearInterval(this.#interval);
|
|
43
43
|
this.#interval = undefined;
|
|
44
|
-
this.#
|
|
44
|
+
this.#storageBackend = undefined;
|
|
45
45
|
}
|
|
46
46
|
getSample(sampleDurationMillis) {
|
|
47
47
|
return this.#store.getSample(sampleDurationMillis);
|
|
@@ -53,8 +53,8 @@ export class ClientLoadSignal {
|
|
|
53
53
|
*/
|
|
54
54
|
handle(intervalCallback) {
|
|
55
55
|
const now = new Date();
|
|
56
|
-
const allErrorCounts = this.#
|
|
57
|
-
const currentErrCount = allErrorCounts[
|
|
56
|
+
const allErrorCounts = this.#storageBackend?.stats?.rateLimitErrors ?? [];
|
|
57
|
+
const currentErrCount = allErrorCounts[RATE_LIMIT_ERROR_RETRY_COUNT] || 0;
|
|
58
58
|
const snapshot = {
|
|
59
59
|
createdAt: now,
|
|
60
60
|
isOverloaded: false,
|
|
@@ -6,10 +6,10 @@ import type { Snapshotter } from './snapshotter.js';
|
|
|
6
6
|
export interface SystemInfo {
|
|
7
7
|
/** If false, system is being overloaded. */
|
|
8
8
|
isSystemIdle: boolean;
|
|
9
|
-
memInfo:
|
|
10
|
-
eventLoopInfo:
|
|
11
|
-
cpuInfo:
|
|
12
|
-
|
|
9
|
+
memInfo: LoadSignalInfo;
|
|
10
|
+
eventLoopInfo: LoadSignalInfo;
|
|
11
|
+
cpuInfo: LoadSignalInfo;
|
|
12
|
+
storageBackendInfo: LoadSignalInfo;
|
|
13
13
|
memTotalBytes?: number;
|
|
14
14
|
memCurrentBytes?: number;
|
|
15
15
|
/**
|
|
@@ -31,7 +31,7 @@ export interface SystemInfo {
|
|
|
31
31
|
* Status of additional load signals beyond the built-in four.
|
|
32
32
|
* Keys are `LoadSignal.name` values, values are overload info.
|
|
33
33
|
*/
|
|
34
|
-
loadSignalInfo?: Record<string,
|
|
34
|
+
loadSignalInfo?: Record<string, LoadSignalInfo>;
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
37
|
* How far back the *current* system status looks by default — the window that gates task dispatch.
|
|
@@ -69,12 +69,12 @@ export interface SystemStatusOptions {
|
|
|
69
69
|
/**
|
|
70
70
|
* Additional load signals to include in the system status evaluation.
|
|
71
71
|
* These are evaluated alongside the built-in memory, CPU, event loop,
|
|
72
|
-
* and
|
|
73
|
-
* considered overloaded. Each signal carries its own overload ratio.
|
|
72
|
+
* and storage backend signals. If any signal reports overload, the system
|
|
73
|
+
* is considered overloaded. Each signal carries its own overload ratio.
|
|
74
74
|
*/
|
|
75
75
|
loadSignals?: LoadSignal[];
|
|
76
76
|
}
|
|
77
|
-
export interface
|
|
77
|
+
export interface LoadSignalInfo {
|
|
78
78
|
isOverloaded: boolean;
|
|
79
79
|
limitRatio: number;
|
|
80
80
|
actualRatio: number;
|
|
@@ -15,7 +15,7 @@ const BUILTIN_SIGNAL_OPTION_KEYS = {
|
|
|
15
15
|
memInfo: 'memory',
|
|
16
16
|
eventLoopInfo: 'eventLoop',
|
|
17
17
|
cpuInfo: 'cpu',
|
|
18
|
-
|
|
18
|
+
storageBackendInfo: 'storageBackend',
|
|
19
19
|
};
|
|
20
20
|
const BUILTIN_SIGNAL_NAMES = new Set(Object.keys(BUILTIN_SIGNAL_OPTION_KEYS));
|
|
21
21
|
/**
|
|
@@ -114,7 +114,7 @@ export class SystemStatus {
|
|
|
114
114
|
memInfo: { isOverloaded: false, limitRatio: 0, actualRatio: 0 },
|
|
115
115
|
eventLoopInfo: { isOverloaded: false, limitRatio: 0, actualRatio: 0 },
|
|
116
116
|
cpuInfo: { isOverloaded: false, limitRatio: 0, actualRatio: 0 },
|
|
117
|
-
|
|
117
|
+
storageBackendInfo: { isOverloaded: false, limitRatio: 0, actualRatio: 0 },
|
|
118
118
|
};
|
|
119
119
|
let loadSignalInfo;
|
|
120
120
|
for (const signal of this.#signals) {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { Dictionary, HttpRequestOptions, ISession, ProxyInfo, SendRequestOptions } from '@crawlee/types';
|
|
2
|
-
import type { ReadonlyDeep
|
|
3
|
-
import type {
|
|
2
|
+
import type { ReadonlyDeep } from 'type-fest';
|
|
3
|
+
import type { EnqueueUrlsOptions } from '../enqueue_links/enqueue_links.js';
|
|
4
4
|
import type { CrawleeLogger } from '../log.js';
|
|
5
5
|
import type { Request, RequestOptions, Source } from '../request.js';
|
|
6
6
|
import type { StorageIdentifier } from '../storages/storage_instance_manager.js';
|
|
7
7
|
import type { Dataset } from '../storages/dataset.js';
|
|
8
8
|
import type { KeyValueStore } from '../storages/key_value_store.js';
|
|
9
|
-
import type {
|
|
9
|
+
import type { AddRequestsBatchedResult } from '../storages/request_queue.js';
|
|
10
10
|
/** @internal */
|
|
11
11
|
export type IsAny<T> = 0 extends 1 & T ? true : false;
|
|
12
12
|
/**
|
|
@@ -37,7 +37,7 @@ export type TypedRequestsLike<Routes extends Record<keyof Routes, Dictionary>> =
|
|
|
37
37
|
* The label-aware `addRequests` method signature exposed on a request handler's context when the crawler is
|
|
38
38
|
* bound to a typed router. Mirrors {@link RestrictedCrawlingContext.addRequests} with typed sources.
|
|
39
39
|
*/
|
|
40
|
-
export type TypedContextAddRequests<Routes extends Record<keyof Routes, Dictionary>> = (requestsLike: ReadonlyDeep<LabeledSource<Routes>[]>, options?: ReadonlyDeep<
|
|
40
|
+
export type TypedContextAddRequests<Routes extends Record<keyof Routes, Dictionary>> = (requestsLike: ReadonlyDeep<LabeledSource<Routes>[]>, options?: ReadonlyDeep<EnqueueUrlsOptions>) => Promise<AddRequestsBatchedResult>;
|
|
41
41
|
/**
|
|
42
42
|
* An `enqueueLinks`-options object with its `label`/`userData` retyped according to a router's route map: a
|
|
43
43
|
* declared `label` requires the matching `userData` shape (unknown labels are rejected), while unlabeled
|
|
@@ -87,36 +87,15 @@ export interface RestrictedCrawlingContext<UserData extends Dictionary = Diction
|
|
|
87
87
|
*/
|
|
88
88
|
pushData(data: ReadonlyDeep<Parameters<Dataset['pushData']>[0]>, datasetIdentifier?: string | StorageIdentifier): Promise<void>;
|
|
89
89
|
/**
|
|
90
|
-
*
|
|
91
|
-
* currently used by the crawler.
|
|
90
|
+
* Add requests directly to the request queue currently used by the crawler.
|
|
92
91
|
*
|
|
93
|
-
* Optionally, the function allows you to filter the target
|
|
94
|
-
*
|
|
95
|
-
* Check out the [Crawl a website with relative links](https://crawlee.dev/js/docs/examples/crawl-relative-links) example
|
|
96
|
-
* for more details regarding its usage.
|
|
97
|
-
*
|
|
98
|
-
* **Example usage**
|
|
99
|
-
*
|
|
100
|
-
* ```ts
|
|
101
|
-
* async requestHandler({ enqueueLinks }) {
|
|
102
|
-
* await enqueueLinks({
|
|
103
|
-
* include: [
|
|
104
|
-
* 'https://www.example.com/handbags/*',
|
|
105
|
-
* ],
|
|
106
|
-
* });
|
|
107
|
-
* },
|
|
108
|
-
* ```
|
|
109
|
-
*
|
|
110
|
-
* @param [options] All `enqueueLinks()` parameters are passed via an options object.
|
|
111
|
-
*/
|
|
112
|
-
enqueueLinks: (options: ReadonlyDeep<Omit<SetRequired<EnqueueLinksOptions, 'urls'>, 'requestManager' | 'robotsTxtFile'>>) => Promise<unknown>;
|
|
113
|
-
/**
|
|
114
|
-
* Add requests directly to the request queue.
|
|
92
|
+
* Optionally, the function allows you to filter the target URLs using an array of glob or regexp patterns,
|
|
93
|
+
* the same way {@link CrawlingContext.enqueueLinks|`enqueueLinks`} does for extracted links.
|
|
115
94
|
*
|
|
116
95
|
* @param requests The requests to add
|
|
117
96
|
* @param options Options for the request queue
|
|
118
97
|
*/
|
|
119
|
-
addRequests: (requestsLike: ReadonlyDeep<(string | Source)[]>, options?: ReadonlyDeep<
|
|
98
|
+
addRequests: (requestsLike: ReadonlyDeep<(string | Source)[]>, options?: ReadonlyDeep<EnqueueUrlsOptions>) => Promise<AddRequestsBatchedResult>;
|
|
120
99
|
/**
|
|
121
100
|
* Returns the state - a piece of mutable persistent data shared across all the request handler runs.
|
|
122
101
|
*/
|
|
@@ -131,31 +110,6 @@ export interface RestrictedCrawlingContext<UserData extends Dictionary = Diction
|
|
|
131
110
|
log: CrawleeLogger;
|
|
132
111
|
}
|
|
133
112
|
export interface CrawlingContext<UserData extends Dictionary = Dictionary> extends RestrictedCrawlingContext<UserData> {
|
|
134
|
-
/**
|
|
135
|
-
* This function automatically finds and enqueues links from the current page, adding them to the {@link RequestQueue}
|
|
136
|
-
* currently used by the crawler.
|
|
137
|
-
*
|
|
138
|
-
* Optionally, the function allows you to filter the target links' URLs using an array of glob or regexp patterns.
|
|
139
|
-
*
|
|
140
|
-
* Check out the [Crawl a website with relative links](https://crawlee.dev/js/docs/examples/crawl-relative-links) example
|
|
141
|
-
* for more details regarding its usage.
|
|
142
|
-
*
|
|
143
|
-
* **Example usage**
|
|
144
|
-
*
|
|
145
|
-
* ```ts
|
|
146
|
-
* async requestHandler({ enqueueLinks }) {
|
|
147
|
-
* await enqueueLinks({
|
|
148
|
-
* include: [
|
|
149
|
-
* 'https://www.example.com/handbags/*',
|
|
150
|
-
* ],
|
|
151
|
-
* });
|
|
152
|
-
* },
|
|
153
|
-
* ```
|
|
154
|
-
*
|
|
155
|
-
* @param [options] All `enqueueLinks()` parameters are passed via an options object.
|
|
156
|
-
* @returns Promise that resolves to {@link BatchAddRequestsResult} object.
|
|
157
|
-
*/
|
|
158
|
-
enqueueLinks(options: ReadonlyDeep<Omit<SetRequired<EnqueueLinksOptions, 'urls'>, 'requestManager' | 'robotsTxtFile'>> & Pick<EnqueueLinksOptions, 'requestManager' | 'robotsTxtFile'>): Promise<unknown>;
|
|
159
113
|
/**
|
|
160
114
|
* Fires HTTP request via the internal HTTP client, allowing to override the request options on the fly.
|
|
161
115
|
*
|
|
@@ -1,19 +1,24 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import {
|
|
3
|
-
import type {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import type { Dictionary } from '@crawlee/types';
|
|
2
|
+
import type { RequestQueueOperationOptions } from '../storages/request_queue.js';
|
|
3
|
+
import type { RequestTransform, SkippedRequestCallback, UrlPatternInput, UrlPatternObject } from './shared.js';
|
|
4
|
+
/**
|
|
5
|
+
* Options shared by the `extractLinks()` context helper across crawler types.
|
|
6
|
+
*/
|
|
7
|
+
export interface ExtractLinksOptions {
|
|
8
|
+
/** A CSS selector matching links to be extracted. */
|
|
9
|
+
selector?: string;
|
|
10
|
+
/**
|
|
11
|
+
* A base URL that will be used to resolve relative URLs when using Cheerio. Ignored when using Puppeteer,
|
|
12
|
+
* since the relative URL resolution is done inside the browser automatically.
|
|
13
|
+
*/
|
|
14
|
+
baseUrl?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Options accepted by the `enqueueUrls()` context helper exposed by `BasicCrawler`.
|
|
18
|
+
*/
|
|
19
|
+
export interface EnqueueUrlsOptions extends RequestQueueOperationOptions {
|
|
9
20
|
/** Limit the amount of actually enqueued URLs to this number. Useful for testing across the entire crawling scope. */
|
|
10
21
|
limit?: number;
|
|
11
|
-
/** An array of URLs to enqueue. */
|
|
12
|
-
urls?: readonly string[];
|
|
13
|
-
/** A request manager to which the URLs will be enqueued. */
|
|
14
|
-
requestManager?: IRequestManager;
|
|
15
|
-
/** A CSS selector matching links to be enqueued. */
|
|
16
|
-
selector?: string;
|
|
17
22
|
/** Sets {@link Request.userData} for newly enqueued requests. */
|
|
18
23
|
userData?: Dictionary;
|
|
19
24
|
/**
|
|
@@ -30,8 +35,7 @@ export interface EnqueueLinksOptions extends RequestQueueOperationOptions {
|
|
|
30
35
|
*/
|
|
31
36
|
skipNavigation?: boolean;
|
|
32
37
|
/**
|
|
33
|
-
* A base URL that will be used to resolve relative URLs
|
|
34
|
-
* since the relative URL resolution is done inside the browser automatically.
|
|
38
|
+
* A base URL that will be used to resolve relative URLs.
|
|
35
39
|
*/
|
|
36
40
|
baseUrl?: string;
|
|
37
41
|
/**
|
|
@@ -42,11 +46,11 @@ export interface EnqueueLinksOptions extends RequestQueueOperationOptions {
|
|
|
42
46
|
* Glob matching is always case-insensitive.
|
|
43
47
|
* If you need case-sensitive matching, use a `RegExp`.
|
|
44
48
|
*
|
|
45
|
-
* The patterns are combined with the {@link
|
|
49
|
+
* The patterns are combined with the {@link EnqueueUrlsOptions.strategy|`strategy`} using AND logic - a URL
|
|
46
50
|
* must match at least one `include` pattern **and** satisfy the strategy to be enqueued. To match URLs across
|
|
47
51
|
* hostnames, pass an explicit {@link EnqueueStrategy.All} strategy.
|
|
48
52
|
*
|
|
49
|
-
* If `undefined`, the links are enqueued based on the {@link
|
|
53
|
+
* If `undefined`, the links are enqueued based on the {@link EnqueueUrlsOptions.strategy|`strategy`} alone.
|
|
50
54
|
* Passing an empty array is not allowed.
|
|
51
55
|
*/
|
|
52
56
|
include?: readonly UrlPatternInput[];
|
|
@@ -106,25 +110,12 @@ export interface EnqueueLinksOptions extends RequestQueueOperationOptions {
|
|
|
106
110
|
*
|
|
107
111
|
* @default EnqueueStrategy.SameHostname
|
|
108
112
|
*/
|
|
109
|
-
strategy?:
|
|
113
|
+
strategy?: EnqueueStrategyOption;
|
|
110
114
|
/**
|
|
111
115
|
* By default, only the first batch (1000) of found requests will be added to the queue before resolving the call.
|
|
112
116
|
* You can use this option to wait for adding all of them.
|
|
113
117
|
*/
|
|
114
118
|
waitForAllRequestsToBeAdded?: boolean;
|
|
115
|
-
/**
|
|
116
|
-
* RobotsTxtFile instance for the current request that triggered the `enqueueLinks`.
|
|
117
|
-
* If provided, disallowed URLs will be ignored.
|
|
118
|
-
*/
|
|
119
|
-
robotsTxtFile?: Pick<RobotsTxtFile, 'isAllowed'>;
|
|
120
|
-
/**
|
|
121
|
-
* Mirrors {@link BasicCrawlerOptions.respectRobotsTxtFile}: pass `false` to disable filtering or
|
|
122
|
-
* `{ userAgent }` to evaluate rules for a specific user-agent. Defaults to `*` when
|
|
123
|
-
* {@link EnqueueLinksOptions.robotsTxtFile|`robotsTxtFile`} is provided.
|
|
124
|
-
*/
|
|
125
|
-
respectRobotsTxtFile?: boolean | {
|
|
126
|
-
userAgent?: string;
|
|
127
|
-
};
|
|
128
119
|
/**
|
|
129
120
|
* When a request is skipped for some reason, you can use this callback to act on it.
|
|
130
121
|
* This is currently fired for requests skipped
|
|
@@ -134,6 +125,8 @@ export interface EnqueueLinksOptions extends RequestQueueOperationOptions {
|
|
|
134
125
|
*/
|
|
135
126
|
onSkippedRequest?: SkippedRequestCallback;
|
|
136
127
|
}
|
|
128
|
+
/** The combined options accepted by a crawler context's `enqueueLinks()` helper: `extractLinks()` + `enqueueUrls()`. */
|
|
129
|
+
export type EnqueueLinksOptions = ExtractLinksOptions & EnqueueUrlsOptions;
|
|
137
130
|
/**
|
|
138
131
|
* The different enqueueing strategies available.
|
|
139
132
|
*
|
|
@@ -185,34 +178,8 @@ export declare enum EnqueueStrategy {
|
|
|
185
178
|
*/
|
|
186
179
|
SameOrigin = "same-origin"
|
|
187
180
|
}
|
|
188
|
-
/**
|
|
189
|
-
|
|
190
|
-
* you should use the context-aware `enqueueLinks` function provided on the crawler contexts.
|
|
191
|
-
*
|
|
192
|
-
* Optionally, the function allows you to filter the target links' URLs using an array of glob or regexp patterns.
|
|
193
|
-
*
|
|
194
|
-
* **Example usage**
|
|
195
|
-
*
|
|
196
|
-
* ```javascript
|
|
197
|
-
* await enqueueLinks({
|
|
198
|
-
* urls: aListOfFoundUrls,
|
|
199
|
-
* requestManager,
|
|
200
|
-
* selector: 'a.product-detail',
|
|
201
|
-
* include: [
|
|
202
|
-
* 'https://www.example.com/handbags/*',
|
|
203
|
-
* 'https://www.example.com/purses/*'
|
|
204
|
-
* ],
|
|
205
|
-
* });
|
|
206
|
-
* ```
|
|
207
|
-
*
|
|
208
|
-
* @param options All `enqueueLinks()` parameters are passed via an options object.
|
|
209
|
-
* @returns Promise that resolves to {@link BatchAddRequestsResult} object.
|
|
210
|
-
*/
|
|
211
|
-
export declare function enqueueLinks(options: SetRequired<Omit<EnqueueLinksOptions, 'requestManager'>, 'urls'> & {
|
|
212
|
-
requestManager: {
|
|
213
|
-
addRequestsBatched: (requests: Request<Dictionary>[], options: AddRequestsBatchedOptions) => Promise<AddRequestsBatchedResult>;
|
|
214
|
-
};
|
|
215
|
-
}): Promise<BatchAddRequestsResult>;
|
|
181
|
+
/** The `strategy` option accepted by {@link ExtractLinksOptions} and {@link EnqueueUrlsOptions}. */
|
|
182
|
+
export type EnqueueStrategyOption = EnqueueStrategy | 'all' | 'same-domain' | 'same-hostname' | 'same-origin';
|
|
216
183
|
/**
|
|
217
184
|
* @internal
|
|
218
185
|
* This method helps resolve the baseUrl that will be used for filtering in {@link enqueueLinks}.
|
|
@@ -227,7 +194,12 @@ export declare function resolveBaseUrlForEnqueueLinksFiltering({ enqueueStrategy
|
|
|
227
194
|
*/
|
|
228
195
|
export interface ResolveBaseUrl {
|
|
229
196
|
userProvidedBaseUrl?: string;
|
|
230
|
-
enqueueStrategy?:
|
|
197
|
+
enqueueStrategy?: EnqueueStrategyOption;
|
|
231
198
|
originalRequestUrl: string;
|
|
232
199
|
finalRequestUrl?: string;
|
|
233
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* @internal
|
|
203
|
+
* Builds the glob patterns a URL must match to satisfy the given enqueue `strategy`, anchored at `baseUrl`.
|
|
204
|
+
*/
|
|
205
|
+
export declare function buildEnqueueStrategyPatterns(baseUrl: string, strategy: EnqueueStrategyOption): UrlPatternObject[];
|
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
import { getDomain } from 'tldts';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
import { Request } from '../request.js';
|
|
4
|
-
import { parseArgument, schemas } from '../validators.js';
|
|
5
|
-
import { applyRequestTransform, constructUrlPatternObjects, createRequestOptions, filterRequestOptionsByPatterns, urlPatternSchema, } from './shared.js';
|
|
6
2
|
/**
|
|
7
3
|
* The different enqueueing strategies available.
|
|
8
4
|
*
|
|
@@ -55,159 +51,6 @@ export var EnqueueStrategy;
|
|
|
55
51
|
*/
|
|
56
52
|
EnqueueStrategy["SameOrigin"] = "same-origin";
|
|
57
53
|
})(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
|
-
});
|
|
80
|
-
/**
|
|
81
|
-
* This function enqueues the urls provided to the {@link RequestQueue} provided. If you want to automatically find and enqueue links,
|
|
82
|
-
* you should use the context-aware `enqueueLinks` function provided on the crawler contexts.
|
|
83
|
-
*
|
|
84
|
-
* Optionally, the function allows you to filter the target links' URLs using an array of glob or regexp patterns.
|
|
85
|
-
*
|
|
86
|
-
* **Example usage**
|
|
87
|
-
*
|
|
88
|
-
* ```javascript
|
|
89
|
-
* await enqueueLinks({
|
|
90
|
-
* urls: aListOfFoundUrls,
|
|
91
|
-
* requestManager,
|
|
92
|
-
* selector: 'a.product-detail',
|
|
93
|
-
* include: [
|
|
94
|
-
* 'https://www.example.com/handbags/*',
|
|
95
|
-
* 'https://www.example.com/purses/*'
|
|
96
|
-
* ],
|
|
97
|
-
* });
|
|
98
|
-
* ```
|
|
99
|
-
*
|
|
100
|
-
* @param options All `enqueueLinks()` parameters are passed via an options object.
|
|
101
|
-
* @returns Promise that resolves to {@link BatchAddRequestsResult} object.
|
|
102
|
-
*/
|
|
103
|
-
export async function enqueueLinks(options) {
|
|
104
|
-
if (!options || Object.keys(options).length === 0) {
|
|
105
|
-
throw new RangeError([
|
|
106
|
-
'enqueueLinks() was called without the required options. You can only do that when you use the `crawlingContext.enqueueLinks()` method in request handlers.',
|
|
107
|
-
'Check out our guide on how to use enqueueLinks() here: https://crawlee.dev/js/docs/examples/crawl-relative-links',
|
|
108
|
-
].join('\n'));
|
|
109
|
-
}
|
|
110
|
-
const parsedOptions = parseArgument(options, enqueueLinksOptionsSchema, 'EnqueueLinksOptions');
|
|
111
|
-
const { requestManager, limit, urls, include, exclude, transformRequestFunction, forefront, waitForAllRequestsToBeAdded, robotsTxtFile, onSkippedRequest, } = parsedOptions;
|
|
112
|
-
const urlExcludePatternObjects = exclude?.length ? constructUrlPatternObjects(exclude) : [];
|
|
113
|
-
const urlPatternObjects = include?.length ? constructUrlPatternObjects(include) : [];
|
|
114
|
-
// The strategy always applies, even when `include` patterns are provided - the two are AND-ed together
|
|
115
|
-
// (a URL must match an `include` pattern *and* satisfy the strategy). This mirrors crawlee-python.
|
|
116
|
-
parsedOptions.strategy ??= EnqueueStrategy.SameHostname;
|
|
117
|
-
const enqueueStrategyPatterns = [];
|
|
118
|
-
if (parsedOptions.baseUrl) {
|
|
119
|
-
const url = new URL(parsedOptions.baseUrl);
|
|
120
|
-
switch (parsedOptions.strategy) {
|
|
121
|
-
case EnqueueStrategy.SameHostname:
|
|
122
|
-
// We need to get the origin of the passed in domain in the event someone sets baseUrl
|
|
123
|
-
// to an url like https://example.com/deep/default/path and one of the found urls is an
|
|
124
|
-
// absolute relative path (/path/to/page)
|
|
125
|
-
enqueueStrategyPatterns.push({ glob: ignoreHttpSchema(`${url.origin}/**`) });
|
|
126
|
-
break;
|
|
127
|
-
case EnqueueStrategy.SameDomain: {
|
|
128
|
-
// Get the actual hostname from the base url
|
|
129
|
-
const baseUrlHostname = getDomain(url.hostname, { mixedInputs: false });
|
|
130
|
-
if (baseUrlHostname) {
|
|
131
|
-
// We have a hostname, so we can use it to match all links on the page that point to it and any subdomains of it
|
|
132
|
-
url.hostname = baseUrlHostname;
|
|
133
|
-
enqueueStrategyPatterns.push({ glob: ignoreHttpSchema(`${url.origin.replace(baseUrlHostname, `*.${baseUrlHostname}`)}/**`) }, { glob: ignoreHttpSchema(`${url.origin}/**`) });
|
|
134
|
-
}
|
|
135
|
-
else {
|
|
136
|
-
// We don't have a hostname (can happen for ips for instance), so reproduce the same behavior
|
|
137
|
-
// as SameDomainAndSubdomain
|
|
138
|
-
enqueueStrategyPatterns.push({ glob: ignoreHttpSchema(`${url.origin}/**`) });
|
|
139
|
-
}
|
|
140
|
-
break;
|
|
141
|
-
}
|
|
142
|
-
case EnqueueStrategy.SameOrigin: {
|
|
143
|
-
// The same behavior as SameHostname, but respecting the protocol of the URL
|
|
144
|
-
enqueueStrategyPatterns.push({ glob: `${url.origin}/**` });
|
|
145
|
-
break;
|
|
146
|
-
}
|
|
147
|
-
case EnqueueStrategy.All:
|
|
148
|
-
default:
|
|
149
|
-
enqueueStrategyPatterns.push({ glob: `http{s,}://**` });
|
|
150
|
-
break;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
async function reportSkippedRequests(skippedRequests, reason) {
|
|
154
|
-
if (onSkippedRequest && skippedRequests.length > 0) {
|
|
155
|
-
await Promise.all(skippedRequests.map((request) => {
|
|
156
|
-
return onSkippedRequest({
|
|
157
|
-
url: request.url,
|
|
158
|
-
reason: request.skippedReason ?? reason,
|
|
159
|
-
});
|
|
160
|
-
}));
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
let requestOptions = createRequestOptions(urls, parsedOptions);
|
|
164
|
-
if (robotsTxtFile && parsedOptions.respectRobotsTxtFile !== false) {
|
|
165
|
-
const robotsUserAgent = typeof parsedOptions.respectRobotsTxtFile === 'object'
|
|
166
|
-
? (parsedOptions.respectRobotsTxtFile.userAgent ?? '*')
|
|
167
|
-
: '*';
|
|
168
|
-
const skippedRequests = [];
|
|
169
|
-
requestOptions = requestOptions.filter((request) => {
|
|
170
|
-
if (robotsTxtFile.isAllowed(request.url, robotsUserAgent)) {
|
|
171
|
-
return true;
|
|
172
|
-
}
|
|
173
|
-
skippedRequests.push(request);
|
|
174
|
-
return false;
|
|
175
|
-
});
|
|
176
|
-
await reportSkippedRequests(skippedRequests, 'robotsTxt');
|
|
177
|
-
}
|
|
178
|
-
async function createFilteredRequests() {
|
|
179
|
-
const skippedRequests = [];
|
|
180
|
-
// Step 1: Filter request options by exclude patterns, user include patterns, and strategy patterns.
|
|
181
|
-
let filteredOptions;
|
|
182
|
-
if (urlPatternObjects.length === 0) {
|
|
183
|
-
filteredOptions = filterRequestOptionsByPatterns(requestOptions, enqueueStrategyPatterns.length > 0 ? enqueueStrategyPatterns : undefined, urlExcludePatternObjects, parsedOptions.strategy, (url) => skippedRequests.push(url));
|
|
184
|
-
}
|
|
185
|
-
else {
|
|
186
|
-
// Filter by user patterns first (with exclude)
|
|
187
|
-
const afterUserPatterns = filterRequestOptionsByPatterns(requestOptions, urlPatternObjects, urlExcludePatternObjects, parsedOptions.strategy, (url) => skippedRequests.push(url));
|
|
188
|
-
// ...then filter by the enqueue links strategy (making this an AND check)
|
|
189
|
-
filteredOptions = filterRequestOptionsByPatterns(afterUserPatterns, enqueueStrategyPatterns.length > 0 ? enqueueStrategyPatterns : undefined, [], parsedOptions.strategy, (url) => skippedRequests.push(url));
|
|
190
|
-
}
|
|
191
|
-
await reportSkippedRequests(skippedRequests.map((url) => ({ url })), 'filters');
|
|
192
|
-
// Step 2: Apply transformRequestFunction on request options - it has the highest priority
|
|
193
|
-
if (transformRequestFunction) {
|
|
194
|
-
const skippedByTransform = [];
|
|
195
|
-
filteredOptions = applyRequestTransform(filteredOptions, transformRequestFunction, (r) => skippedByTransform.push(r));
|
|
196
|
-
await reportSkippedRequests(skippedByTransform, 'transform');
|
|
197
|
-
}
|
|
198
|
-
// Step 3: Create Request instances from the final request options
|
|
199
|
-
return filteredOptions.map((opts) => new Request(opts));
|
|
200
|
-
}
|
|
201
|
-
const { addedRequests, requestsOverLimit } = await requestManager.addRequestsBatched(await createFilteredRequests(), {
|
|
202
|
-
forefront,
|
|
203
|
-
waitForAllRequestsToBeAdded,
|
|
204
|
-
maxNewRequests: limit,
|
|
205
|
-
});
|
|
206
|
-
if (requestsOverLimit?.length !== undefined && requestsOverLimit.length > 0) {
|
|
207
|
-
await reportSkippedRequests(requestsOverLimit.map((r) => ({ url: typeof r === 'string' ? r : r.url })), 'enqueueLimit');
|
|
208
|
-
}
|
|
209
|
-
return { processedRequests: addedRequests, unprocessedRequests: [] };
|
|
210
|
-
}
|
|
211
54
|
/**
|
|
212
55
|
* @internal
|
|
213
56
|
* This method helps resolve the baseUrl that will be used for filtering in {@link enqueueLinks}.
|
|
@@ -242,6 +85,41 @@ export function resolveBaseUrlForEnqueueLinksFiltering({ enqueueStrategy, finalR
|
|
|
242
85
|
// before actually finding the urls
|
|
243
86
|
return originalUrlOrigin;
|
|
244
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* @internal
|
|
90
|
+
* Builds the glob patterns a URL must match to satisfy the given enqueue `strategy`, anchored at `baseUrl`.
|
|
91
|
+
*/
|
|
92
|
+
export function buildEnqueueStrategyPatterns(baseUrl, strategy) {
|
|
93
|
+
const url = new URL(baseUrl);
|
|
94
|
+
switch (strategy) {
|
|
95
|
+
case EnqueueStrategy.SameHostname:
|
|
96
|
+
// We need to get the origin of the passed in domain in the event someone sets baseUrl
|
|
97
|
+
// to an url like https://example.com/deep/default/path and one of the found urls is an
|
|
98
|
+
// absolute relative path (/path/to/page)
|
|
99
|
+
return [{ glob: ignoreHttpSchema(`${url.origin}/**`) }];
|
|
100
|
+
case EnqueueStrategy.SameDomain: {
|
|
101
|
+
// Get the actual hostname from the base url
|
|
102
|
+
const baseUrlHostname = getDomain(url.hostname, { mixedInputs: false });
|
|
103
|
+
if (baseUrlHostname) {
|
|
104
|
+
// We have a hostname, so we can use it to match all links on the page that point to it and any subdomains of it
|
|
105
|
+
url.hostname = baseUrlHostname;
|
|
106
|
+
return [
|
|
107
|
+
{ glob: ignoreHttpSchema(`${url.origin.replace(baseUrlHostname, `*.${baseUrlHostname}`)}/**`) },
|
|
108
|
+
{ glob: ignoreHttpSchema(`${url.origin}/**`) },
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
// We don't have a hostname (can happen for ips for instance), so reproduce the same behavior
|
|
112
|
+
// as SameDomainAndSubdomain
|
|
113
|
+
return [{ glob: ignoreHttpSchema(`${url.origin}/**`) }];
|
|
114
|
+
}
|
|
115
|
+
case EnqueueStrategy.SameOrigin:
|
|
116
|
+
// The same behavior as SameHostname, but respecting the protocol of the URL
|
|
117
|
+
return [{ glob: `${url.origin}/**` }];
|
|
118
|
+
case EnqueueStrategy.All:
|
|
119
|
+
default:
|
|
120
|
+
return [{ glob: `http{s,}://**` }];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
245
123
|
/**
|
|
246
124
|
* Internal function that changes the enqueue glob patterns to match both http and https
|
|
247
125
|
*/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { Awaitable } from '@crawlee/types';
|
|
1
|
+
import type { Awaitable, Dictionary } from '@crawlee/types';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import type { RequestOptions } from '../request.js';
|
|
4
|
-
import type {
|
|
4
|
+
import type { EnqueueStrategyOption } from './enqueue_links.js';
|
|
5
5
|
export { tryAbsoluteURL } from '@crawlee/utils/internal';
|
|
6
6
|
export interface UrlPatternObject {
|
|
7
7
|
glob?: string;
|
|
@@ -59,11 +59,18 @@ export declare function constructUrlPatternObjects(patterns: readonly UrlPattern
|
|
|
59
59
|
* When `includePatterns` is empty/undefined, all options pass through (only exclude filtering applies).
|
|
60
60
|
* @ignore
|
|
61
61
|
*/
|
|
62
|
-
export declare function filterRequestOptionsByPatterns(requestOptions: RequestOptions[], includePatterns: UrlPatternObject[] | undefined, excludePatterns?: UrlPatternObject[], strategy?:
|
|
62
|
+
export declare function filterRequestOptionsByPatterns(requestOptions: RequestOptions[], includePatterns: UrlPatternObject[] | undefined, excludePatterns?: UrlPatternObject[], strategy?: EnqueueStrategyOption, onSkippedUrl?: (url: string) => void): RequestOptions[];
|
|
63
63
|
/**
|
|
64
64
|
* @ignore
|
|
65
65
|
*/
|
|
66
|
-
export declare function createRequestOptions(sources: readonly (string | Record<string, unknown>)[], options?:
|
|
66
|
+
export declare function createRequestOptions(sources: readonly (string | Record<string, unknown>)[], options?: {
|
|
67
|
+
label?: string;
|
|
68
|
+
userData?: Dictionary;
|
|
69
|
+
baseUrl?: string;
|
|
70
|
+
skipNavigation?: boolean;
|
|
71
|
+
sessionId?: string;
|
|
72
|
+
strategy?: EnqueueStrategyOption;
|
|
73
|
+
}): RequestOptions[];
|
|
67
74
|
/**
|
|
68
75
|
* Takes a {@link RequestOptions} object and changes its attributes in a desired way. This user-function is used
|
|
69
76
|
* by {@link enqueueLinks} to modify request options before they are converted to {@link Request} instances.
|
package/enqueue_links/shared.js
CHANGED
|
@@ -144,6 +144,16 @@ export function filterRequestOptionsByPatterns(requestOptions, includePatterns,
|
|
|
144
144
|
})
|
|
145
145
|
.filter((opts) => opts !== null);
|
|
146
146
|
}
|
|
147
|
+
function isAbsoluteUrl(url) {
|
|
148
|
+
try {
|
|
149
|
+
// eslint-disable-next-line no-new
|
|
150
|
+
new URL(url);
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
147
157
|
/**
|
|
148
158
|
* @ignore
|
|
149
159
|
*/
|
|
@@ -161,7 +171,12 @@ export function createRequestOptions(sources, options = {}) {
|
|
|
161
171
|
}
|
|
162
172
|
})
|
|
163
173
|
.map((requestOptions) => {
|
|
164
|
-
|
|
174
|
+
// Leave already-absolute URLs untouched - re-deriving them via `new URL()` would normalize them
|
|
175
|
+
// (e.g. adding a trailing slash to a bare domain), which is surprising for URLs that didn't need
|
|
176
|
+
// resolving against `baseUrl` in the first place.
|
|
177
|
+
if (!isAbsoluteUrl(requestOptions.url)) {
|
|
178
|
+
requestOptions.url = new URL(requestOptions.url, options.baseUrl).href;
|
|
179
|
+
}
|
|
165
180
|
requestOptions.userData ??= options.userData ?? {};
|
|
166
181
|
if (typeof options.label === 'string') {
|
|
167
182
|
requestOptions.userData = {
|
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.123",
|
|
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,10 +52,10 @@
|
|
|
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/http-client": "4.0.0-beta.
|
|
57
|
-
"@crawlee/types": "4.0.0-beta.
|
|
58
|
-
"@crawlee/utils": "4.0.0-beta.
|
|
55
|
+
"@crawlee/fs-storage": "4.0.0-beta.123",
|
|
56
|
+
"@crawlee/http-client": "4.0.0-beta.123",
|
|
57
|
+
"@crawlee/types": "4.0.0-beta.123",
|
|
58
|
+
"@crawlee/utils": "4.0.0-beta.123",
|
|
59
59
|
"@sapphire/async-queue": "^1.5.5",
|
|
60
60
|
"@vladfrangu/async_event_emitter": "^2.4.6",
|
|
61
61
|
"content-type": "^1.0.5",
|
|
@@ -77,5 +77,5 @@
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
},
|
|
80
|
-
"gitHead": "
|
|
80
|
+
"gitHead": "f77648095c6a3f5ed8815c7620ea765db430ae44"
|
|
81
81
|
}
|
package/request.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { BinaryLike } from 'node:crypto';
|
|
2
2
|
import type { AllowedHttpMethods, Dictionary } from '@crawlee/types';
|
|
3
|
-
import type {
|
|
3
|
+
import type { EnqueueStrategyOption } from './enqueue_links/enqueue_links.js';
|
|
4
4
|
import type { SkippedRequestReason } from './enqueue_links/shared.js';
|
|
5
5
|
export declare enum RequestState {
|
|
6
6
|
UNPROCESSED = 0,
|
|
@@ -281,7 +281,7 @@ export interface RequestOptions<UserData extends Dictionary = Dictionary> {
|
|
|
281
281
|
/** @internal */
|
|
282
282
|
lockExpiresAt?: Date;
|
|
283
283
|
/** @internal */
|
|
284
|
-
enqueueStrategy?:
|
|
284
|
+
enqueueStrategy?: EnqueueStrategyOption;
|
|
285
285
|
}
|
|
286
286
|
export interface PushErrorMessageOptions {
|
|
287
287
|
/**
|
package/router.d.ts
CHANGED
|
@@ -65,7 +65,7 @@ export declare function validateUserData(label: string | symbol, schema: Standar
|
|
|
65
65
|
* `Record<string, ...>`), any string or symbol label is accepted, preserving the original behaviour.
|
|
66
66
|
*/
|
|
67
67
|
export type RouterLabel<Routes extends Record<keyof Routes, Dictionary>> = string extends keyof Routes ? string | symbol : (keyof Routes & string) | symbol;
|
|
68
|
-
export interface RouterHandler<Context extends
|
|
68
|
+
export interface RouterHandler<Context extends RestrictedCrawlingContext = CrawlingContext, Routes extends Record<keyof Routes, Dictionary> = Record<string, GetUserDataFromRequest<Context['request']>>> extends Router<Context, Routes> {
|
|
69
69
|
(ctx: Context): Awaitable<void>;
|
|
70
70
|
}
|
|
71
71
|
export type GetUserDataFromRequest<T> = T extends Request<infer Y> ? Y : never;
|
|
@@ -214,7 +214,7 @@ export type RouterRoutes<Context, Routes extends Record<keyof Routes, Dictionary
|
|
|
214
214
|
* });
|
|
215
215
|
* ```
|
|
216
216
|
*/
|
|
217
|
-
export declare class Router<Context extends
|
|
217
|
+
export declare class Router<Context extends RestrictedCrawlingContext, Routes extends Record<keyof Routes, Dictionary> = Record<string, GetUserDataFromRequest<Context['request']>>> {
|
|
218
218
|
#private;
|
|
219
219
|
/**
|
|
220
220
|
* use Router.create() instead!
|
|
@@ -299,8 +299,8 @@ export declare class Router<Context extends Omit<RestrictedCrawlingContext, 'enq
|
|
|
299
299
|
* await crawler.run();
|
|
300
300
|
* ```
|
|
301
301
|
*/
|
|
302
|
-
static create<Context extends
|
|
303
|
-
static create<Context extends
|
|
304
|
-
static create<Context extends
|
|
302
|
+
static create<Context extends RestrictedCrawlingContext = CrawlingContext, Routes extends Record<keyof Routes, Dictionary> = Record<string, GetUserDataFromRequest<Context['request']>>>(routes?: RouterRoutes<Context, Routes>): RouterHandler<Context, Routes>;
|
|
303
|
+
static create<Context extends RestrictedCrawlingContext = CrawlingContext, UserData extends Dictionary = GetUserDataFromRequest<Context['request']>>(routes?: RouterRoutes<Context, Record<string, UserData>>): RouterHandler<Context, Record<string, UserData>>;
|
|
304
|
+
static create<Context extends RestrictedCrawlingContext = CrawlingContext, const Schemas extends RouteSchemas = RouteSchemas>(schemas: Schemas): RouterHandler<Context, RoutesFromSchemas<Schemas>>;
|
|
305
305
|
}
|
|
306
306
|
export {};
|