@crawlee/core 4.0.0-beta.146 → 4.0.0-beta.147

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,8 @@
1
1
  import type { Dictionary } from '@crawlee/types';
2
2
  import type { Configuration } from '../configuration.js';
3
3
  import type { Request, Source } from '../request.js';
4
- import type { IRequestManager, RequestsLike } from './request_manager.js';
4
+ import type { RequestSourceStatus } from './request_loader.js';
5
+ import type { IRequestManager, PacingSignal, RequestsLike } from './request_manager.js';
5
6
  import type { AddRequestsBatchedOptions, AddRequestsBatchedResult, RequestQueueOperationInfo, RequestQueueOperationOptions } from './request_queue.js';
6
7
  import type { StorageIdentifier } from './storage_instance_manager.js';
7
8
  import type { StorageOpenOptions } from './utils.js';
@@ -12,30 +13,18 @@ import type { StorageOpenOptions } from './utils.js';
12
13
  * {@link ThrottlingRequestManager} calls this once per configured domain, so every per-domain queue shares the
13
14
  * concrete type and storage backend of the manager being wrapped.
14
15
  */
15
- export type RequestManagerOpener<T extends IRequestManager = IRequestManager> = (identifier: string | StorageIdentifier, options?: StorageOpenOptions) => Promise<T>;
16
- /**
17
- * A request manager that can pace requests per domain, as {@link ThrottlingRequestManager} does.
18
- *
19
- * The crawlers detect this structurally rather than by type, so a wrapper can opt in by forwarding these three
20
- * methods without {@link IRequestManager} having to know that throttling exists.
21
- */
22
- export interface SupportsDomainThrottling {
23
- /** @see {@link ThrottlingRequestManager.recordDomainDelay} */
24
- recordDomainDelay(url: string, retryAfterMs?: number | null): boolean;
25
- /** @see {@link ThrottlingRequestManager.setCrawlDelay} */
26
- setCrawlDelay(url: string, delaySeconds: number): boolean;
27
- /** @see {@link ThrottlingRequestManager.assertNoStalledDomains} */
28
- assertNoStalledDomains(): Promise<void>;
29
- }
30
- /** Whether `manager` can pace requests per domain. */
31
- export declare function supportsDomainThrottling(manager: unknown): manager is SupportsDomainThrottling;
16
+ export type RequestManagerOpener<T extends IRequestManager = IRequestManager> = (identifier?: string | StorageIdentifier | null, options?: StorageOpenOptions) => Promise<T>;
32
17
  /** Options for {@link ThrottlingRequestManager}. */
33
18
  export interface ThrottlingRequestManagerOptions<T extends IRequestManager = IRequestManager> {
34
19
  /**
35
20
  * The request manager to wrap, usually a {@link RequestQueue}. Requests for domains that are not throttled
36
- * are stored here.
21
+ * are stored here. May be a factory, so that the throttler can be constructed synchronously and the manager
22
+ * under it opened only on first use.
23
+ *
24
+ * Omitted, the default request queue is opened on first use through
25
+ * {@link ThrottlingRequestManagerOptions.requestManagerOpener|`requestManagerOpener`}.
37
26
  */
38
- inner: T;
27
+ inner?: T | (() => T | Promise<T>);
39
28
  /**
40
29
  * Which domains to throttle: a list of hostnames, or `'all'` for every domain the crawl encounters.
41
30
  *
@@ -54,7 +43,8 @@ export interface ThrottlingRequestManagerOptions<T extends IRequestManager = IRe
54
43
  /**
55
44
  * A floor under the crawl delay of every throttled domain, in seconds - the proactive clock described on
56
45
  * {@link ThrottlingRequestManager}. A domain whose robots.txt asks for a longer `Crawl-delay` gets the
57
- * longer one; this is a minimum, not an override.
46
+ * longer one; this is a minimum, not an override. A `minIntervalEverywhere` {@link PacingSignal} raises
47
+ * this floor at runtime, and never lowers it.
58
48
  * @default 0
59
49
  */
60
50
  minCrawlDelaySecs?: number;
@@ -125,8 +115,9 @@ export interface ThrottlingRequestManagerOptions<T extends IRequestManager = IRe
125
115
  *
126
116
  * {@link ThrottlingRequestManager.fetchNextRequest|`fetchNextRequest()`} serves the domain that has been waiting
127
117
  * longest and skips any that are backing off, falling back to the wrapped manager. It never blocks: while every
128
- * remaining request belongs to a throttled domain it returns `null` and {@link ThrottlingRequestManager.isEmpty}
129
- * reports `true`, so the crawler idles instead of holding a concurrency slot open.
118
+ * remaining request belongs to a throttled domain it returns `null` and
119
+ * {@link ThrottlingRequestManager.checkReadiness|`checkReadiness()`} reports `waiting` with the moment the
120
+ * earliest of them comes due, so the crawler idles instead of holding a concurrency slot open.
130
121
  *
131
122
  * Each throttled domain runs two independent clocks, and may be dispatched to once **both** have run out:
132
123
  * - **Backoff**, set by HTTP 429 responses - honouring `Retry-After`, and otherwise doubling from `baseDelaySecs`.
@@ -141,12 +132,19 @@ export interface ThrottlingRequestManagerOptions<T extends IRequestManager = IRe
141
132
  * Which domains get those clocks is {@link ThrottlingRequestManagerOptions.domains|`domains`} - a list, or
142
133
  * `'all'` for every domain the crawl encounters.
143
134
  *
135
+ * Pass one as a crawler's `requestManager`; the `sameDomainDelaySecs` shorthand builds one with `domains: 'all'`
136
+ * and `throttleBy: 'registrableDomain'`. Construct it yourself to name the domains or tune the delays - one
137
+ * covering every domain also makes `sameDomainDelaySecs` land on it as a floor rather than adding a second pacer.
138
+ *
139
+ * Signals - 429s, robots.txt `Crawl-delay`, that floor - arrive through
140
+ * {@link IRequestManager.recordPacingSignal|`recordPacingSignal`}, which wrapping managers forward, so this
141
+ * works wherever it sits in a composition, including inside a {@link RequestManagerTandem}.
142
+ *
144
143
  * **Example usage:**
145
144
  *
146
145
  * ```ts
147
146
  * const crawler = new CheerioCrawler({
148
147
  * requestManager: new ThrottlingRequestManager({
149
- * inner: await RequestQueue.open(),
150
148
  * domains: ['api.example.com', 'slow-site.org'],
151
149
  * }),
152
150
  * requestHandler: async ({ request }) => { ... },
@@ -155,39 +153,27 @@ export interface ThrottlingRequestManagerOptions<T extends IRequestManager = IRe
155
153
  *
156
154
  * @category Sources
157
155
  */
158
- export declare class ThrottlingRequestManager<T extends IRequestManager = IRequestManager> implements IRequestManager, SupportsDomainThrottling {
156
+ export declare class ThrottlingRequestManager<T extends IRequestManager = IRequestManager> implements IRequestManager {
159
157
  #private;
160
158
  private readonly config;
161
159
  private readonly domainStates;
162
160
  private readonly log;
163
161
  constructor(options: ThrottlingRequestManagerOptions<T>, config?: Configuration);
164
- /** The wrapped manager, holding every request whose domain is not throttled. */
165
- get innerManager(): T;
166
- /**
167
- * Records a 429 response and puts the URL's domain into backoff.
168
- *
169
- * @returns `false` if the domain is not configured for throttling, in which case this is a no-op.
170
- */
171
- recordDomainDelay(url: string, retryAfterMs?: number | null): boolean;
172
162
  /**
173
- * Records the `Crawl-delay` a domain's robots.txt asked for, which becomes its crawl delay unless
174
- * {@link ThrottlingRequestManagerOptions.minCrawlDelaySecs|`minCrawlDelaySecs`} asks for longer.
175
- *
176
- * The first value wins, so a robots.txt re-fetch cannot change the cadence mid-crawl.
177
- *
178
- * @returns `false` if the domain is not throttled, in which case this is a no-op.
163
+ * The wrapped manager, holding every request whose domain is not throttled. `undefined` until an `inner`
164
+ * passed as a factory is resolved - reading this never forces it, because a getter should not open a queue
165
+ * behind a caller's back.
179
166
  */
180
- setCrawlDelay(url: string, delaySeconds: number): boolean;
167
+ get innerManager(): T | undefined;
181
168
  /**
182
- * Throws {@link PersistentRateLimitError} if any domain has been rate-limiting us past
183
- * {@link ThrottlingRequestManagerOptions.maxDomainStallSecs|`maxDomainStallSecs`} without letting a single
184
- * request through.
169
+ * Records a pacing signal: a refusal puts the URL's domain into backoff, a declared interval becomes its
170
+ * crawl delay, and a crawl-wide floor raises {@link recordEverywhereFloor|the floor under all of them}.
185
171
  *
186
- * A domain qualifies only while it still has queued requests and is actively rate-limiting - a domain that
187
- * has simply run out of work is finished, not stalled, and one being waited out under a long robots.txt
188
- * `Crawl-delay` is being obeyed, not stonewalled.
172
+ * @returns `false` if the domain the signal covers is not throttled, in which case this is a no-op.
173
+ * @throws If the signal's scope is one this manager cannot honour - see {@link assertScopeHonourable}.
174
+ * @inheritdoc
189
175
  */
190
- assertNoStalledDomains(): Promise<void>;
176
+ recordPacingSignal(signal: PacingSignal): boolean;
191
177
  addRequest(requestLike: Source, options?: RequestQueueOperationOptions): Promise<RequestQueueOperationInfo>;
192
178
  /**
193
179
  * Adds requests in batches, routing each one to the manager that owns its domain.
@@ -203,34 +189,31 @@ export declare class ThrottlingRequestManager<T extends IRequestManager = IReque
203
189
  getPendingCount(): Promise<number>;
204
190
  getHandledCount(): Promise<number>;
205
191
  /**
206
- * Whether the next {@link ThrottlingRequestManager.fetchNextRequest} would return `null`.
192
+ * Reports whether anything can be dispatched right now, and if not, when — or why never.
207
193
  *
208
- * Requests waiting on a throttled domain count as unavailable, so a crawler whose task loop is gated on
209
- * this idles for the backoff instead of spinning on a fetch that cannot succeed yet.
194
+ * One traversal of the domain clocks answers all of it: only domains whose delays have run out are probed,
195
+ * the rest merely contribute the moment they come due. Throttled requests count as outstanding work, so a
196
+ * crawler gated on this idles for the backoff instead of concluding it is done.
197
+ *
198
+ * `ready` from anywhere else outranks a stalling domain and is returned without looking at the stall clocks,
199
+ * so one hopeless domain never ends a crawl making progress elsewhere. It cannot outrank itself, though -
200
+ * see the traversal.
210
201
  */
211
- isEmpty(): Promise<boolean>;
212
- /** Unlike {@link ThrottlingRequestManager.isEmpty}, throttled requests still count as outstanding work. */
213
- isFinished(): Promise<boolean>;
202
+ checkReadiness(): Promise<RequestSourceStatus>;
214
203
  /**
215
204
  * Empties every manager and clears the accumulated backoff. A robots.txt `Crawl-delay` is a property of the
216
205
  * site rather than of the run, so it survives.
217
206
  */
218
207
  purge(): Promise<void>;
219
- /**
220
- * Empties the per-domain queues, leaving the wrapped manager alone.
221
- *
222
- * Those queues are this manager's own no matter who owns the one it wraps, which is what makes this safe to
223
- * call where a full {@link ThrottlingRequestManager.purge|`purge()`} would not be.
224
- */
225
- purgeDomainQueues(): Promise<void>;
226
208
  setExpectedRequestProcessingTimeSecs(secs: number): Promise<void>;
227
209
  /**
228
210
  * Returns the next request from a domain that is not backing off, or from the inner manager.
229
211
  *
230
212
  * Returns `null` while every remaining request belongs to a throttled domain - it never waits the backoff
231
213
  * out, because a consumer parked in here holds a concurrency slot, which the autoscaler reads as spare
232
- * capacity and answers by scaling up. Callers poll instead, and {@link ThrottlingRequestManager.isEmpty}
233
- * reports `true` meanwhile so the crawler's task loop idles rather than spins.
214
+ * capacity and answers by scaling up. Callers poll instead, and
215
+ * {@link ThrottlingRequestManager.checkReadiness|`checkReadiness()`} reports `waiting` meanwhile so the
216
+ * crawler's task loop idles rather than spins.
234
217
  */
235
218
  fetchNextRequest<R extends Dictionary = Dictionary>(): Promise<Request<R> | null>;
236
219
  [Symbol.asyncIterator](): AsyncGenerator<Request<Dictionary>, void, unknown>;