@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.
@@ -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, client) plus any {@link LoadSignalsOptions.custom|`custom`} implementations of
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
  /**
@@ -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,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 { ClientInfo } from './system_status.js';
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-client rate limits. Implement this interface to add
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`, `clientInfo`) land in the correspondingly named `SystemInfo` fields rather than the
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 client signals read the last entry to measure change since it).
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): ClientInfo;
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 client signals read the last entry to measure change since it).
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 ClientLoadSignal} (snapshot interval + error limit + overload ratio), or
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
- client?: ClientLoadSignalOptions | false;
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 ClientLoadSignal} — constructing the ones
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 ClientLoadSignal} — constructing the ones
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. `client: false`).
15
+ // Absent when switched off through the corresponding option (e.g. `storageBackend: false`).
16
16
  #memorySignal;
17
17
  #eventLoopSignal;
18
18
  #cpuSignal;
19
- #clientSignal;
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.#clientSignal,
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 = {}, client = {} } = options;
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 (client !== false)
48
- this.#clientSignal = new ClientLoadSignal(client);
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 client (rate-limit) signal.
3
+ * A snapshot produced by the built-in storage backend (rate-limit) signal.
4
4
  * @internal
5
5
  */
6
- export interface ClientSnapshot extends LoadSnapshot {
6
+ export interface StorageBackendSnapshot extends LoadSnapshot {
7
7
  rateLimitErrorCount: number;
8
8
  }
9
9
  /**
10
- * Tuning for the built-in **client** (rate-limit) load signal, as accepted both by {@link ClientLoadSignal} and by
11
- * the {@link LoadSignalsOptions.client|`client`} shorthand on {@link LoadSignalsOptions}.
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 ClientLoadSignalOptions {
14
+ export interface StorageBackendLoadSignalOptions {
14
15
  /**
15
- * Defines the interval of checking the current state of the remote API client, in seconds.
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 client counts as overloaded.
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.client|`client: false`}) if the storage backend reports no
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 ClientLoadSignal implements LoadSignal {
42
+ export declare class StorageBackendLoadSignal implements LoadSignal {
42
43
  #private;
43
- readonly name = "clientInfo";
44
+ readonly name = "storageBackendInfo";
44
45
  readonly overloadedRatio: number;
45
- constructor(options?: ClientLoadSignalOptions);
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 CLIENT_RATE_LIMIT_ERROR_RETRY_COUNT = 2;
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.client|`client: false`}) if the storage backend reports no
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 ClientLoadSignal {
17
- name = 'clientInfo';
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
- #client;
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 the client is resolved afresh just below.
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.#client = serviceLocator.getStorageBackend();
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.#client = undefined;
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.#client?.stats?.rateLimitErrors ?? [];
57
- const currentErrCount = allErrorCounts[CLIENT_RATE_LIMIT_ERROR_RETRY_COUNT] || 0;
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: ClientInfo;
10
- eventLoopInfo: ClientInfo;
11
- cpuInfo: ClientInfo;
12
- clientInfo: ClientInfo;
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, ClientInfo>;
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 client signals. If any signal reports overload, the system is
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 ClientInfo {
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
- clientInfo: 'client',
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
- clientInfo: { isOverloaded: false, limitRatio: 0, actualRatio: 0 },
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, SetRequired } from 'type-fest';
3
- import type { EnqueueLinksOptions } from '../enqueue_links/enqueue_links.js';
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 { RequestQueueOperationOptions } from '../storages/request_queue.js';
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<RequestQueueOperationOptions>) => Promise<void>;
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
- * This function automatically finds and enqueues links from the current page, adding them to the {@link RequestQueue}
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 links' URLs using an array of glob or regexp patterns.
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<RequestQueueOperationOptions>) => Promise<void>;
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 { BatchAddRequestsResult, Dictionary } from '@crawlee/types';
2
- import { type RobotsTxtFile } from '@crawlee/utils';
3
- import type { SetRequired } from 'type-fest';
4
- import { Request } from '../request.js';
5
- import type { IRequestManager } from '../storages/request_manager.js';
6
- import type { AddRequestsBatchedOptions, AddRequestsBatchedResult, RequestQueueOperationOptions } from '../storages/request_queue.js';
7
- import type { RequestTransform, SkippedRequestCallback, UrlPatternInput } from './shared.js';
8
- export interface EnqueueLinksOptions extends RequestQueueOperationOptions {
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 when using Cheerio. Ignored when using Puppeteer,
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 EnqueueLinksOptions.strategy|`strategy`} using AND logic - a URL
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 EnqueueLinksOptions.strategy|`strategy`} alone.
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?: EnqueueStrategy | 'all' | 'same-domain' | 'same-hostname' | 'same-origin';
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
- * This function enqueues the urls provided to the {@link RequestQueue} provided. If you want to automatically find and enqueue links,
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?: EnqueueLinksOptions['strategy'];
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 { EnqueueLinksOptions } from './enqueue_links.js';
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?: EnqueueLinksOptions['strategy'], onSkippedUrl?: (url: string) => void): RequestOptions[];
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?: Pick<EnqueueLinksOptions, 'label' | 'userData' | 'baseUrl' | 'skipNavigation' | 'sessionId' | 'strategy'>): RequestOptions[];
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.
@@ -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
- requestOptions.url = new URL(requestOptions.url, options.baseUrl).href;
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.122",
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.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",
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": "2c3e87fefdb9e1fca4c144f03167d8524cfdc2e5"
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 { EnqueueLinksOptions } from './enqueue_links/enqueue_links.js';
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?: EnqueueLinksOptions['strategy'];
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 Omit<RestrictedCrawlingContext, 'enqueueLinks'> = CrawlingContext, Routes extends Record<keyof Routes, Dictionary> = Record<string, GetUserDataFromRequest<Context['request']>>> extends Router<Context, Routes> {
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 Omit<RestrictedCrawlingContext, 'enqueueLinks'>, Routes extends Record<keyof Routes, Dictionary> = Record<string, GetUserDataFromRequest<Context['request']>>> {
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 Omit<RestrictedCrawlingContext, 'enqueueLinks'> = CrawlingContext, Routes extends Record<keyof Routes, Dictionary> = Record<string, GetUserDataFromRequest<Context['request']>>>(routes?: RouterRoutes<Context, Routes>): RouterHandler<Context, Routes>;
303
- static create<Context extends Omit<RestrictedCrawlingContext, 'enqueueLinks'> = CrawlingContext, UserData extends Dictionary = GetUserDataFromRequest<Context['request']>>(routes?: RouterRoutes<Context, Record<string, UserData>>): RouterHandler<Context, Record<string, UserData>>;
304
- static create<Context extends Omit<RestrictedCrawlingContext, 'enqueueLinks'> = CrawlingContext, const Schemas extends RouteSchemas = RouteSchemas>(schemas: Schemas): RouterHandler<Context, RoutesFromSchemas<Schemas>>;
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 {};