@crawlee/core 4.0.0-rc.0 → 4.0.0-rc.1

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.
Files changed (122) hide show
  1. package/README.md +1 -1
  2. package/configuration.d.ts +15 -46
  3. package/configuration.js +8 -20
  4. package/errors.d.ts +12 -53
  5. package/errors.js +13 -66
  6. package/events/index.d.ts +1 -0
  7. package/events/local_event_manager.d.ts +0 -7
  8. package/events/local_event_manager.js +8 -8
  9. package/events/system_info.d.ts +38 -0
  10. package/index.d.ts +2 -8
  11. package/index.js +4 -8
  12. package/internal.d.ts +8 -0
  13. package/internal.js +9 -0
  14. package/log.d.ts +10 -11
  15. package/log.js +53 -25
  16. package/memory-storage/memory-storage.d.ts +12 -7
  17. package/memory-storage/memory-storage.js +45 -17
  18. package/memory-storage/resource-clients/dataset.d.ts +0 -5
  19. package/memory-storage/resource-clients/dataset.js +17 -20
  20. package/memory-storage/resource-clients/key-value-store.d.ts +0 -9
  21. package/memory-storage/resource-clients/key-value-store.js +11 -33
  22. package/memory-storage/resource-clients/request-queue.d.ts +0 -22
  23. package/memory-storage/resource-clients/request-queue.js +57 -53
  24. package/package.json +16 -18
  25. package/proxy_configuration.d.ts +20 -23
  26. package/proxy_configuration.js +18 -12
  27. package/recoverable_state.d.ts +28 -6
  28. package/recoverable_state.js +51 -14
  29. package/request.d.ts +18 -104
  30. package/request.js +41 -220
  31. package/serialization.js +3 -3
  32. package/service_locator.d.ts +3 -0
  33. package/service_locator.js +2 -0
  34. package/storages/dataset.d.ts +9 -15
  35. package/storages/dataset.js +22 -14
  36. package/storages/index.d.ts +3 -4
  37. package/storages/index.js +1 -4
  38. package/storages/key_value_store.d.ts +12 -46
  39. package/storages/key_value_store.js +31 -47
  40. package/storages/key_value_store_codec.js +6 -11
  41. package/storages/request_dedup_cache.d.ts +0 -2
  42. package/storages/request_dedup_cache.js +8 -8
  43. package/storages/request_list.d.ts +6 -82
  44. package/storages/request_list.js +175 -179
  45. package/storages/request_loader.d.ts +48 -22
  46. package/storages/request_loader.js +36 -1
  47. package/storages/request_manager.d.ts +86 -0
  48. package/storages/request_manager_tandem.d.ts +13 -28
  49. package/storages/request_manager_tandem.js +46 -43
  50. package/storages/request_queue.d.ts +19 -49
  51. package/storages/request_queue.js +92 -88
  52. package/storages/storage_instance_manager.d.ts +1 -2
  53. package/storages/storage_instance_manager.js +4 -4
  54. package/storages/transaction.d.ts +27 -9
  55. package/storages/transaction.js +56 -11
  56. package/storages/utils.d.ts +2 -2
  57. package/validators.d.ts +3 -2
  58. package/validators.js +3 -2
  59. package/autoscaling/autoscaled_pool.d.ts +0 -195
  60. package/autoscaling/autoscaled_pool.js +0 -386
  61. package/autoscaling/concurrency_system.d.ts +0 -268
  62. package/autoscaling/concurrency_system.js +0 -362
  63. package/autoscaling/cpu_load_signal.d.ts +0 -43
  64. package/autoscaling/cpu_load_signal.js +0 -47
  65. package/autoscaling/event_loop_load_signal.d.ts +0 -51
  66. package/autoscaling/event_loop_load_signal.js +0 -60
  67. package/autoscaling/index.d.ts +0 -9
  68. package/autoscaling/index.js +0 -9
  69. package/autoscaling/load_signal.d.ts +0 -100
  70. package/autoscaling/load_signal.js +0 -105
  71. package/autoscaling/memory_load_signal.d.ts +0 -47
  72. package/autoscaling/memory_load_signal.js +0 -106
  73. package/autoscaling/snapshotter.d.ts +0 -84
  74. package/autoscaling/snapshotter.js +0 -67
  75. package/autoscaling/storage_backend_load_signal.d.ts +0 -56
  76. package/autoscaling/storage_backend_load_signal.js +0 -73
  77. package/autoscaling/system_status.d.ts +0 -159
  78. package/autoscaling/system_status.js +0 -139
  79. package/autoscaling/weighted_avg.d.ts +0 -5
  80. package/autoscaling/weighted_avg.js +0 -14
  81. package/cookie_utils.d.ts +0 -44
  82. package/cookie_utils.js +0 -122
  83. package/crawlers/context_pipeline.d.ts +0 -70
  84. package/crawlers/context_pipeline.js +0 -122
  85. package/crawlers/crawler_commons.d.ts +0 -159
  86. package/crawlers/error_snapshotter.d.ts +0 -57
  87. package/crawlers/error_snapshotter.js +0 -117
  88. package/crawlers/error_tracker.d.ts +0 -54
  89. package/crawlers/error_tracker.js +0 -308
  90. package/crawlers/index.d.ts +0 -5
  91. package/crawlers/index.js +0 -4
  92. package/crawlers/internals/types.d.ts +0 -7
  93. package/crawlers/internals/types.js +0 -1
  94. package/crawlers/statistics.d.ts +0 -328
  95. package/crawlers/statistics.js +0 -536
  96. package/enqueue_links/enqueue_links.d.ts +0 -156
  97. package/enqueue_links/enqueue_links.js +0 -78
  98. package/enqueue_links/index.d.ts +0 -2
  99. package/enqueue_links/index.js +0 -2
  100. package/enqueue_links/shared.d.ts +0 -93
  101. package/enqueue_links/shared.js +0 -239
  102. package/http.d.ts +0 -9
  103. package/http.js +0 -28
  104. package/router.d.ts +0 -306
  105. package/router.js +0 -309
  106. package/session_pool/consts.d.ts +0 -3
  107. package/session_pool/consts.js +0 -3
  108. package/session_pool/errors.d.ts +0 -7
  109. package/session_pool/errors.js +0 -11
  110. package/session_pool/fingerprint.d.ts +0 -9
  111. package/session_pool/fingerprint.js +0 -30
  112. package/session_pool/index.d.ts +0 -4
  113. package/session_pool/index.js +0 -4
  114. package/session_pool/session.d.ts +0 -150
  115. package/session_pool/session.js +0 -220
  116. package/session_pool/session_pool.d.ts +0 -240
  117. package/session_pool/session_pool.js +0 -394
  118. package/storages/sitemap_request_loader.d.ts +0 -201
  119. package/storages/sitemap_request_loader.js +0 -438
  120. package/storages/throttling_request_manager.d.ts +0 -239
  121. package/storages/throttling_request_manager.js +0 -646
  122. /package/{crawlers/crawler_commons.js → events/system_info.js} +0 -0
@@ -2,6 +2,44 @@ import type { Dictionary } from '@crawlee/types';
2
2
  import type { Request } from '../request.js';
3
3
  import type { IRequestManager } from './request_manager.js';
4
4
  import type { RequestQueueOperationInfo } from './request_queue.js';
5
+ /**
6
+ * A request source's own availability, in a single answer.
7
+ *
8
+ * - `ready` — the next {@link IRequestLoader.fetchNextRequest} is expected to hand something over.
9
+ * - `waiting` — nothing to fetch right now, but the source is not done: requests are in progress, are being
10
+ * added in the background, or are held back until `readyAt`.
11
+ * - `stalled` — the source holds requests it cannot make progress on. Only a manager that paces its own
12
+ * dispatch can reach this; see {@link ThrottlingRequestManager}.
13
+ * - `finished` — everything has been handled.
14
+ */
15
+ export type RequestSourceStatus = {
16
+ status: 'ready';
17
+ } | {
18
+ status: 'waiting';
19
+ /**
20
+ * A `Date.now()` timestamp at which the source expects to become `ready`. Absent when the wait has
21
+ * no clock (an in-progress request, a background add), leaving a consumer to poll.
22
+ */
23
+ readyAt?: number;
24
+ } | {
25
+ status: 'stalled';
26
+ reason: string;
27
+ } | {
28
+ status: 'finished';
29
+ };
30
+ /** Loaders never stall — only a manager that paces its own dispatch can. */
31
+ export type RequestLoaderStatus = Exclude<RequestSourceStatus, {
32
+ status: 'stalled';
33
+ }>;
34
+ /**
35
+ * Combines two request sources' statuses, with the precedence `ready` > `stalled` > `waiting` > `finished`.
36
+ *
37
+ * Binary rather than variadic on purpose: it is on the task loop's probe path and folding a pair allocates
38
+ * nothing.
39
+ *
40
+ * @internal
41
+ */
42
+ export declare function joinRequestSourceStatuses(a: RequestSourceStatus, b: RequestSourceStatus): RequestSourceStatus;
5
43
  /**
6
44
  * An abstract interface defining a read-only stream of requests to crawl.
7
45
  *
@@ -21,10 +59,10 @@ import type { RequestQueueOperationInfo } from './request_queue.js';
21
59
  * "finished with this request", whether processing succeeded or was abandoned after exhausting retries.
22
60
  *
23
61
  * Honoring this contract matters for three reasons:
24
- * - **Restarts and migrations:** loaders that persist their state (see {@link IRequestLoader.persistState})
62
+ * - **Restarts and migrations:** loaders that persist their state (such as {@link RequestList})
25
63
  * treat in-progress requests as interrupted and re-serve them after a restart. A request that is fetched
26
64
  * but never marked handled will be crawled again.
27
- * - **Termination detection:** {@link IRequestLoader.isFinished} only resolves to `true` once nothing is
65
+ * - **Termination detection:** {@link IRequestLoader.checkReadiness} only reports `finished` once nothing is
28
66
  * in progress. Leaving a request unmarked keeps the crawler running indefinitely.
29
67
  * - **Bookkeeping:** the handled and pending counts are derived from the set of in-progress requests, so
30
68
  * skipping {@link IRequestLoader.markRequestAsHandled} corrupts {@link IRequestLoader.getHandledCount}
@@ -47,20 +85,13 @@ export interface IRequestLoader {
47
85
  */
48
86
  getHandledCount(): Promise<number>;
49
87
  /**
50
- * Returns `true` if all requests were already handled and there are no more left.
51
- */
52
- isFinished(): Promise<boolean>;
53
- /**
54
- * Resolves to `true` if the next call to {@link IRequestLoader.fetchNextRequest} function
55
- * would return `null`, otherwise it resolves to `false`.
56
- * Note that even if the loader is empty, there might be some pending requests currently being processed.
88
+ * Reports whether the loader has a request to hand over, is waiting on one, or is done — see
89
+ * {@link RequestSourceStatus}.
57
90
  *
58
- * This is a statement about what the *next fetch* would return, not about how much work is left, so it
59
- * may report `true` while {@link IRequestLoader.getPendingCount} is non-zero - a loader that withholds
60
- * requests for a while (as {@link ThrottlingRequestManager} does for a rate-limited domain) is empty
61
- * for as long as it will not hand anything over. Use `isFinished()` to ask whether the work is done.
91
+ * A consumer's task loop is gated on this, so implementations MUST answer `ready` before evaluating
92
+ * anything else. `finished` may arrive late behind distributed storage, but it is never wrong.
62
93
  */
63
- isEmpty(): Promise<boolean>;
94
+ checkReadiness(): Promise<RequestSourceStatus>;
64
95
  /**
65
96
  * Gets the next {@link Request} to process, or `null` if there are no more pending requests.
66
97
  *
@@ -81,16 +112,11 @@ export interface IRequestLoader {
81
112
  *
82
113
  * Call this once you are done with the request — whether processing succeeded or was abandoned after
83
114
  * exhausting retries. Because a loader cannot take a request back, marking it handled is the only way to
84
- * signal completion; failing to do so prevents {@link IRequestLoader.isFinished} from ever resolving to
85
- * `true` and skews the handled and pending counts. See the request lifecycle contract on {@link IRequestLoader}.
115
+ * signal completion; failing to do so prevents {@link IRequestLoader.checkReadiness} from ever reporting
116
+ * `finished` and skews the handled and pending counts. See the request lifecycle contract on
117
+ * {@link IRequestLoader}.
86
118
  */
87
119
  markRequestAsHandled(request: Request): Promise<RequestQueueOperationInfo | void | null>;
88
- /**
89
- * Persists the current state of the loader into the default {@link KeyValueStore}.
90
- *
91
- * Not all loaders support persistence; implementations that do not should leave this `undefined`.
92
- */
93
- persistState?(): Promise<void>;
94
120
  /**
95
121
  * Combines the loader with a request manager to support adding and reclaiming requests.
96
122
  *
@@ -1 +1,36 @@
1
- export {};
1
+ /**
2
+ * Combines two request sources' statuses, with the precedence `ready` > `stalled` > `waiting` > `finished`.
3
+ *
4
+ * Binary rather than variadic on purpose: it is on the task loop's probe path and folding a pair allocates
5
+ * nothing.
6
+ *
7
+ * @internal
8
+ */
9
+ export function joinRequestSourceStatuses(a, b) {
10
+ if (a.status === 'ready') {
11
+ return a;
12
+ }
13
+ if (b.status === 'ready') {
14
+ return b;
15
+ }
16
+ // `ready` outranking `stalled` masks a stalled source while the other still has work. That is parity with
17
+ // the crawler before this was a single answer: the stall check was only reached from `isFinishedFunction`,
18
+ // which the task loop calls only when nothing is in flight and nothing is ready. "Fixing" the masking
19
+ // turns a crawl that is progressing elsewhere into a `PersistentRateLimitError`. `stalled` outranking
20
+ // `waiting` is the same parity - that call site fired regardless of other domains' clocks.
21
+ if (a.status === 'stalled') {
22
+ return a;
23
+ }
24
+ if (b.status === 'stalled') {
25
+ return b;
26
+ }
27
+ if (a.status === 'waiting') {
28
+ // The earlier of the two known wake-up times - unknown only if neither source announced one.
29
+ if (b.status !== 'waiting' || b.readyAt === undefined) {
30
+ return a;
31
+ }
32
+ return a.readyAt !== undefined && a.readyAt <= b.readyAt ? a : b;
33
+ }
34
+ // `a` is finished, so `b` decides.
35
+ return b;
36
+ }
@@ -1,3 +1,4 @@
1
+ import type { LiteralUnion } from 'type-fest';
1
2
  import type { Request, Source } from '../request.js';
2
3
  import type { IRequestLoader } from './request_loader.js';
3
4
  import type { AddRequestsBatchedOptions, AddRequestsBatchedResult, RequestQueueOperationInfo, RequestQueueOperationOptions } from './request_queue.js';
@@ -30,4 +31,89 @@ export interface IRequestManager extends IRequestLoader {
30
31
  * this hint may leave it `undefined`.
31
32
  */
32
33
  setExpectedRequestProcessingTimeSecs?(secs: number): Promise<void>;
34
+ /**
35
+ * Records something said about the pace requests should go out at, so that a manager which paces its own
36
+ * dispatch can hold requests back.
37
+ *
38
+ * Required rather than optional, so that a wrapping manager always forwards it and a pacer nested in a
39
+ * composition still receives it; a manager that does not pace returns `false`.
40
+ *
41
+ * @returns `true` if anything in the composition took responsibility for the signal.
42
+ */
43
+ recordPacingSignal(signal: PacingSignal): boolean;
44
+ /**
45
+ * Extends the lock on a request previously handed out by `fetchNextRequest()` and still being
46
+ * processed, on storage backends that reserve requests via locking (e.g. via
47
+ * {@link CrawlingContext.extendTimeout|`context.extendTimeout`}).
48
+ *
49
+ * @returns `true` when the lock was prolonged, `false` when this manager does not lock
50
+ * requests or no longer holds this one; non-locking implementations may leave it `undefined`,
51
+ * which callers treat as `false`.
52
+ */
53
+ extendRequestProcessingTimeSecs?(request: Request, secs: number): Promise<boolean>;
33
54
  }
55
+ /**
56
+ * How much of the URL space a {@link PacingSignal} covers.
57
+ *
58
+ * Open on purpose: `'hostname'` and `'registrableDomain'` are what Crawlee's own reporters send and what
59
+ * {@link ThrottlingRequestManager} understands, but any string is accepted, so a pacer keyed on something
60
+ * else can be reported to in its own vocabulary.
61
+ */
62
+ export type PacingScope = LiteralUnion<'hostname' | 'registrableDomain', string>;
63
+ /**
64
+ * Something said about the pace requests should go out at, reported to a request manager through
65
+ * {@link IRequestManager.recordPacingSignal}.
66
+ *
67
+ * One shape rather than a method per channel: a pacing manager switches on `reason`, a wrapping one forwards the
68
+ * value without knowing what is in it, and a new kind of signal costs the interface nothing. The `url` travels
69
+ * inside the value because the crawl-wide variant has none. Nothing here names the mechanism a signal came
70
+ * from - status codes, headers and robots.txt are the crawler's business - and every delay is in milliseconds.
71
+ *
72
+ * ## Scope
73
+ *
74
+ * A manager may apply a signal to a **wider** scope than it was given - a floor that holds for one host still
75
+ * holds when a whole site is paced by it - but never to a narrower one, which would leave some of the URLs the
76
+ * signal covers running unpaced. A manager that can only do the latter, or that does not recognise the scope at
77
+ * all, MUST throw rather than quietly under-apply it.
78
+ */
79
+ export type PacingSignal = {
80
+ /**
81
+ * The source turned a request away because we were going too fast — an HTTP 429 or 503, an exhausted
82
+ * quota. Reactive and transient: a pacer typically backs off while refusals continue, and lets that
83
+ * decay once they stop.
84
+ */
85
+ reason: 'rateLimited';
86
+ /** The URL that was turned away. */
87
+ url: string;
88
+ /** How long the source asked us to wait before trying again, if it said. */
89
+ waitMs?: number;
90
+ /**
91
+ * How far this refusal reaches, if the reporter can tell — a 429 rarely says. Left out, it asks the
92
+ * manager to apply the signal however it happens to group requests.
93
+ */
94
+ scope?: PacingScope;
95
+ } | {
96
+ /**
97
+ * The source declared a standing floor on how often it may be requested — a robots.txt `Crawl-delay`,
98
+ * a documented quota. A property of the source rather than of the run, so a pacer keeps it for the
99
+ * whole crawl.
100
+ */
101
+ reason: 'minInterval';
102
+ /** A URL of the source that declared the interval. */
103
+ url: string;
104
+ /** The declared minimum interval between two requests to the source. */
105
+ intervalMs: number;
106
+ /** Required, since whoever declares an interval knows what it applies to. */
107
+ scope: PacingScope;
108
+ } | {
109
+ /**
110
+ * A standing floor under the pace of **every** domain the manager dispatches to, declared by whoever
111
+ * owns the crawl rather than by a source — a crawler's `sameDomainDelaySecs`. A manager that paces only
112
+ * some of its domains MUST throw rather than under-apply it.
113
+ */
114
+ reason: 'minIntervalEverywhere';
115
+ /** The declared minimum interval between two requests to any one source. */
116
+ intervalMs: number;
117
+ /** At what granularity the floor applies. */
118
+ scope: PacingScope;
119
+ };
@@ -1,7 +1,7 @@
1
1
  import type { Dictionary } from '@crawlee/types';
2
2
  import type { Request, Source } from '../request.js';
3
- import type { IRequestLoader } from './request_loader.js';
4
- import type { IRequestManager, RequestsLike } from './request_manager.js';
3
+ import type { IRequestLoader, RequestSourceStatus } from './request_loader.js';
4
+ import type { IRequestManager, PacingSignal, RequestsLike } from './request_manager.js';
5
5
  import type { AddRequestsBatchedOptions, AddRequestsBatchedResult, RequestQueueOperationInfo, RequestQueueOperationOptions } from './request_queue.js';
6
6
  /**
7
7
  * A request manager that combines a {@link IRequestLoader} (such as a `RequestList`) with a writable
@@ -18,33 +18,15 @@ export declare class RequestManagerTandem implements IRequestManager {
18
18
  */
19
19
  constructor(requestLoader: IRequestLoader, requestManager: IRequestManager | (() => IRequestManager | Promise<IRequestManager>));
20
20
  /**
21
- * Resolves the writable request manager, opening it lazily (via the factory) on first use and memoizing the result.
22
- * @private
23
- */
24
- private getRequestManager;
25
- /**
26
- * Transfers a single request from the read-only loader to the writable manager.
27
- * If the transfer fails, the request is dropped (and logged) rather than reclaimed.
28
- *
29
- * @returns `true` if a request was successfully transferred (or there was nothing to transfer), and `false` if a
30
- * transfer was attempted but failed - in which case the caller should not fetch from the manager this round.
31
- * @private
32
- */
33
- private transferNextRequestToQueue;
34
- /**
35
- * Fetches the next request from the request manager. If the manager is empty and the loader
36
- * is not finished, it will transfer a request from the loader to the manager first.
21
+ * Fetches the next request, transferring one from the loader first if the loader still has work.
37
22
  * @inheritdoc
38
23
  */
39
24
  fetchNextRequest<T extends Dictionary = Dictionary>(): Promise<Request<T> | null>;
40
25
  /**
26
+ * The loader and the manager read as one source.
41
27
  * @inheritdoc
42
28
  */
43
- isFinished(): Promise<boolean>;
44
- /**
45
- * @inheritdoc
46
- */
47
- isEmpty(): Promise<boolean>;
29
+ checkReadiness(): Promise<RequestSourceStatus>;
48
30
  /**
49
31
  * @inheritdoc
50
32
  */
@@ -77,11 +59,6 @@ export declare class RequestManagerTandem implements IRequestManager {
77
59
  * @inheritdoc
78
60
  */
79
61
  addRequestsBatched(requests: RequestsLike, options?: AddRequestsBatchedOptions): Promise<AddRequestsBatchedResult>;
80
- /**
81
- * Persists the state of the underlying read-only loader, if it supports persistence.
82
- * @inheritdoc
83
- */
84
- persistState(): Promise<void>;
85
62
  /**
86
63
  * Purges the writable request manager so the tandem can be reused (e.g. across repeated `crawler.run()` calls).
87
64
  * The read-only loader is immutable and cannot be purged, so only the manager side is reset.
@@ -94,4 +71,12 @@ export declare class RequestManagerTandem implements IRequestManager {
94
71
  * @inheritdoc
95
72
  */
96
73
  setExpectedRequestProcessingTimeSecs(secs: number): Promise<void>;
74
+ /**
75
+ * Forwards a pacing signal to the writable manager - the loader side is read-only and dispatches nothing of
76
+ * its own. Only a resolved manager is signalled; the tandem will not open a queue to answer a question about
77
+ * pacing.
78
+ * @inheritdoc
79
+ */
80
+ recordPacingSignal(signal: PacingSignal): boolean;
81
+ extendRequestProcessingTimeSecs(request: Request, secs: number): Promise<boolean>;
97
82
  }
@@ -1,4 +1,5 @@
1
1
  import { serviceLocator } from '../service_locator.js';
2
+ import { joinRequestSourceStatuses } from './request_loader.js';
2
3
  /**
3
4
  * A request manager that combines a {@link IRequestLoader} (such as a `RequestList`) with a writable
4
5
  * {@link IRequestManager} (such as a `RequestQueue`).
@@ -24,13 +25,19 @@ export class RequestManagerTandem {
24
25
  constructor(requestLoader, requestManager) {
25
26
  this.#log = serviceLocator.getLogger().child({ prefix: 'RequestManagerTandem' });
26
27
  this.#requestLoader = requestLoader;
27
- this.#requestManagerFactory = typeof requestManager === 'function' ? requestManager : () => requestManager;
28
+ if (typeof requestManager === 'function') {
29
+ this.#requestManagerFactory = requestManager;
30
+ }
31
+ else {
32
+ // Nothing to open, so mark it resolved up front - synchronous pacing signals can then reach it.
33
+ this.#resolvedRequestManager = requestManager;
34
+ this.#requestManagerFactory = () => requestManager;
35
+ }
28
36
  }
29
37
  /**
30
38
  * Resolves the writable request manager, opening it lazily (via the factory) on first use and memoizing the result.
31
- * @private
32
39
  */
33
- async getRequestManager() {
40
+ async #getRequestManager() {
34
41
  if (this.#resolvedRequestManager === undefined) {
35
42
  this.#requestManagerPromise ??= Promise.resolve(this.#requestManagerFactory());
36
43
  this.#resolvedRequestManager = await this.#requestManagerPromise;
@@ -47,14 +54,13 @@ export class RequestManagerTandem {
47
54
  *
48
55
  * @returns `true` if a request was successfully transferred (or there was nothing to transfer), and `false` if a
49
56
  * transfer was attempted but failed - in which case the caller should not fetch from the manager this round.
50
- * @private
51
57
  */
52
- async transferNextRequestToQueue() {
58
+ async #transferNextRequestToQueue() {
53
59
  const request = await this.#requestLoader.fetchNextRequest();
54
60
  if (request === null) {
55
61
  return true;
56
62
  }
57
- const requestManager = await this.getRequestManager();
63
+ const requestManager = await this.#getRequestManager();
58
64
  try {
59
65
  await requestManager.addRequest(request, { forefront: true });
60
66
  return true;
@@ -69,54 +75,46 @@ export class RequestManagerTandem {
69
75
  }
70
76
  }
71
77
  /**
72
- * Fetches the next request from the request manager. If the manager is empty and the loader
73
- * is not finished, it will transfer a request from the loader to the manager first.
78
+ * Fetches the next request, transferring one from the loader first if the loader still has work.
74
79
  * @inheritdoc
75
80
  */
76
81
  async fetchNextRequest() {
77
- // First, try to transfer a request from the requestList
78
- const [listEmpty, listFinished] = await Promise.all([
79
- this.#requestLoader.isEmpty(),
80
- this.#requestLoader.isFinished(),
81
- ]);
82
- if (!listEmpty && !listFinished) {
82
+ // Only the loader's own state decides this: a manager waiting out a backoff must not freeze the
83
+ // loader's unrelated requests for the length of it.
84
+ if ((await this.#requestLoader.checkReadiness()).status === 'ready') {
83
85
  // If the transfer failed, the request was dropped; don't fetch from the manager this round (matching
84
86
  // crawlee-python behaviour). The next `fetchNextRequest()` call will pick up where we left off.
85
- if (!(await this.transferNextRequestToQueue())) {
87
+ if (!(await this.#transferNextRequestToQueue())) {
86
88
  return null;
87
89
  }
88
90
  }
89
91
  // Try to fetch from manager after the transfer
90
- return (await this.getRequestManager()).fetchNextRequest();
92
+ return (await this.#getRequestManager()).fetchNextRequest();
91
93
  }
92
94
  /**
95
+ * The loader and the manager read as one source.
93
96
  * @inheritdoc
94
97
  */
95
- async isFinished() {
96
- const requestManager = await this.getRequestManager();
97
- const storagesFinished = await Promise.all([this.#requestLoader.isFinished(), requestManager.isFinished()]);
98
- return storagesFinished.every(Boolean);
99
- }
100
- /**
101
- * @inheritdoc
102
- */
103
- async isEmpty() {
104
- const requestManager = await this.getRequestManager();
105
- const storagesEmpty = await Promise.all([this.#requestLoader.isEmpty(), requestManager.isEmpty()]);
106
- return storagesEmpty.every(Boolean);
98
+ async checkReadiness() {
99
+ const requestManager = await this.#getRequestManager();
100
+ const [loaderStatus, managerStatus] = await Promise.all([
101
+ this.#requestLoader.checkReadiness(),
102
+ requestManager.checkReadiness(),
103
+ ]);
104
+ return joinRequestSourceStatuses(loaderStatus, managerStatus);
107
105
  }
108
106
  /**
109
107
  * @inheritdoc
110
108
  */
111
109
  async getHandledCount() {
112
110
  // Since one of the stores needs to have priority when both are present, we query the request manager - the request loader will first be dumped into the manager and then left empty.
113
- return (await this.getRequestManager()).getHandledCount();
111
+ return (await this.#getRequestManager()).getHandledCount();
114
112
  }
115
113
  /**
116
114
  * @inheritdoc
117
115
  */
118
116
  async getTotalCount() {
119
- const requestManager = await this.getRequestManager();
117
+ const requestManager = await this.#getRequestManager();
120
118
  const [managerTotal, loaderTotal] = await Promise.all([
121
119
  requestManager.getTotalCount(),
122
120
  // count only pending to avoid double counting, requests marked as "handled" have been moved to requestManager
@@ -128,7 +126,7 @@ export class RequestManagerTandem {
128
126
  * @inheritdoc
129
127
  */
130
128
  async getPendingCount() {
131
- const requestManager = await this.getRequestManager();
129
+ const requestManager = await this.#getRequestManager();
132
130
  const [managerPending, loaderPending] = await Promise.all([
133
131
  requestManager.getPendingCount(),
134
132
  this.#requestLoader.getPendingCount(),
@@ -150,32 +148,25 @@ export class RequestManagerTandem {
150
148
  * @inheritdoc
151
149
  */
152
150
  async markRequestAsHandled(request) {
153
- return (await this.getRequestManager()).markRequestAsHandled(request);
151
+ return (await this.#getRequestManager()).markRequestAsHandled(request);
154
152
  }
155
153
  /**
156
154
  * @inheritdoc
157
155
  */
158
156
  async reclaimRequest(request, options) {
159
- return (await this.getRequestManager()).reclaimRequest(request, options);
157
+ return (await this.#getRequestManager()).reclaimRequest(request, options);
160
158
  }
161
159
  /**
162
160
  * @inheritdoc
163
161
  */
164
162
  async addRequest(requestLike, options) {
165
- return (await this.getRequestManager()).addRequest(requestLike, options);
163
+ return (await this.#getRequestManager()).addRequest(requestLike, options);
166
164
  }
167
165
  /**
168
166
  * @inheritdoc
169
167
  */
170
168
  async addRequestsBatched(requests, options) {
171
- return (await this.getRequestManager()).addRequestsBatched(requests, options);
172
- }
173
- /**
174
- * Persists the state of the underlying read-only loader, if it supports persistence.
175
- * @inheritdoc
176
- */
177
- async persistState() {
178
- await this.#requestLoader.persistState?.();
169
+ return (await this.#getRequestManager()).addRequestsBatched(requests, options);
179
170
  }
180
171
  /**
181
172
  * Purges the writable request manager so the tandem can be reused (e.g. across repeated `crawler.run()` calls).
@@ -183,7 +174,7 @@ export class RequestManagerTandem {
183
174
  * @inheritdoc
184
175
  */
185
176
  async purge() {
186
- await (await this.getRequestManager()).purge?.();
177
+ await (await this.#getRequestManager()).purge?.();
187
178
  }
188
179
  /**
189
180
  * Forwards the hint to the writable request manager — that is where requests are fetched from and
@@ -194,4 +185,16 @@ export class RequestManagerTandem {
194
185
  this.#expectedRequestProcessingSecs = secs;
195
186
  await this.#resolvedRequestManager?.setExpectedRequestProcessingTimeSecs?.(secs);
196
187
  }
188
+ /**
189
+ * Forwards a pacing signal to the writable manager - the loader side is read-only and dispatches nothing of
190
+ * its own. Only a resolved manager is signalled; the tandem will not open a queue to answer a question about
191
+ * pacing.
192
+ * @inheritdoc
193
+ */
194
+ recordPacingSignal(signal) {
195
+ return this.#resolvedRequestManager?.recordPacingSignal(signal) ?? false;
196
+ }
197
+ async extendRequestProcessingTimeSecs(request, secs) {
198
+ return (await this.#getRequestManager()).extendRequestProcessingTimeSecs?.(request, secs) ?? false;
199
+ }
197
200
  }
@@ -5,7 +5,8 @@ import type { IProxyConfiguration } from '../proxy_configuration.js';
5
5
  import type { Source } from '../request.js';
6
6
  import { Request } from '../request.js';
7
7
  import type { JournalEntry } from './transaction.js';
8
- import type { IRequestManager, RequestsLike } from './request_manager.js';
8
+ import type { RequestLoaderStatus } from './request_loader.js';
9
+ import type { IRequestManager, PacingSignal, RequestsLike } from './request_manager.js';
9
10
  import type { RequestQueueStats } from './storage_stats.js';
10
11
  import type { IStorage, StorageIdentifier } from './storage_instance_manager.js';
11
12
  import type { StorageOpenOptions } from './utils.js';
@@ -49,8 +50,6 @@ export declare class RequestQueue implements IStorage, IRequestManager {
49
50
  readonly name?: string;
50
51
  readonly backend: RequestQueueBackend;
51
52
  readonly log: CrawleeLogger;
52
- private requestCache;
53
- private inProgressRequestBatchCount;
54
53
  /**
55
54
  * Backend-independent usage counters tracked for this request queue (write operations and
56
55
  * queue-head reads issued to the underlying storage backend). Counted per backend call.
@@ -87,22 +86,6 @@ export declare class RequestQueue implements IStorage, IRequestManager {
87
86
  * @param [options] Request queue operation options.
88
87
  */
89
88
  addRequest(requestLike: Source, options?: RequestQueueOperationOptions): Promise<RequestQueueOperationInfo>;
90
- /**
91
- * Journals an addition for introspection only; these entries are never replayed. A no-op unless the
92
- * transaction is open, so detached and outliving writers stay out of the journal.
93
- */
94
- private recordRequestJournalEntry;
95
- /**
96
- * The requests buffered by the given transaction for this queue, keyed by `uniqueKey` — a dedup
97
- * index derived from the transaction journal.
98
- */
99
- private bufferedRequests;
100
- /**
101
- * Adds a request under the `deferred` policy: journaled now, really added by the commit replay.
102
- * A new request's `requestId` is the local `uniqueKey` hash and is **provisional** — never write it
103
- * to `request.id` or the dedup caches. Dedup is cheapest-first: buffer, caches, then a backend probe.
104
- */
105
- private addRequestDeferred;
106
89
  /** @internal */
107
90
  commitJournalEntries(entries: JournalEntry[]): Promise<void>;
108
91
  /**
@@ -149,7 +132,7 @@ export declare class RequestQueue implements IStorage, IRequestManager {
149
132
  * Note that the `null` return value doesn't mean the queue processing finished,
150
133
  * it means there are currently no pending requests.
151
134
  * To check whether all requests in queue were finished,
152
- * use {@link RequestQueue.isFinished} instead.
135
+ * use {@link RequestQueue.checkReadiness} instead.
153
136
  *
154
137
  * @returns
155
138
  * Returns the request object or `null` if there are no more pending requests.
@@ -170,25 +153,21 @@ export declare class RequestQueue implements IStorage, IRequestManager {
170
153
  */
171
154
  reclaimRequest(request: Request, options?: RequestQueueOperationOptions): Promise<RequestQueueOperationInfo | null>;
172
155
  /**
173
- * Resolves to `true` if the next call to {@link RequestQueue.fetchNextRequest} would return
174
- * `null`, i.e. there are no pending requests to fetch right now. Otherwise it resolves to `false`.
175
- *
176
- * Note that even if the queue is empty, there might be some requests currently being processed
177
- * (fetched but not yet handled or reclaimed). An empty queue therefore does not mean crawling is
178
- * finished — those in-progress requests may still be reclaimed, and background tasks may still be
179
- * adding more requests. To check whether all activity in the queue has finished, use
180
- * {@link RequestQueue.isFinished}.
156
+ * A queue hands requests out as fast as they are asked for; pacing is a job for a manager wrapped around it,
157
+ * such as {@link ThrottlingRequestManager}.
158
+ * @inheritdoc
181
159
  */
182
- isEmpty(): Promise<boolean>;
160
+ recordPacingSignal(_signal: PacingSignal): boolean;
183
161
  /**
184
- * Resolves to `true` if all requests were already handled and there are no more left — including no
185
- * requests currently in progress (fetched but not yet handled or reclaimed, including requests
186
- * locked by other clients sharing the same queue) and no background add operations still in flight.
162
+ * Reports whether the queue has a request to hand over, is waiting on one, or is done.
187
163
  *
188
- * Due to the nature of distributed storage used by the queue, the function may occasionally return
189
- * a false negative, but it shall never return a false positive.
164
+ * `waiting` means requests are in progress (fetched but not yet handled or reclaimed, possibly by another
165
+ * client sharing the queue) or a background add is still landing; neither has a clock, so no `readyAt`.
166
+ *
167
+ * Due to the nature of distributed storage used by the queue, `finished` may occasionally arrive a probe or
168
+ * two late, but it is never reported early.
190
169
  */
191
- isFinished(): Promise<boolean>;
170
+ checkReadiness(): Promise<RequestLoaderStatus>;
192
171
  /**
193
172
  * Tells the queue how long a consumer expects to hold a fetched request before marking it handled
194
173
  * or reclaiming it (typically the request-handler timeout plus padding), so that a storage backend
@@ -201,9 +180,11 @@ export declare class RequestQueue implements IStorage, IRequestManager {
201
180
  */
202
181
  setExpectedRequestProcessingTimeSecs(secs: number): Promise<void>;
203
182
  /**
204
- * Caches information about request to beware of unneeded addRequest() calls.
183
+ * @inheritdoc
184
+ * Unlike {@link RequestQueue.setExpectedRequestProcessingTimeSecs}, which sizes every future lock,
185
+ * this only touches the one request it is given.
205
186
  */
206
- private cacheRequest;
187
+ extendRequestProcessingTimeSecs(request: Request, secs: number): Promise<boolean>;
207
188
  /**
208
189
  * Removes the queue either from the Apify Cloud storage or from the local database,
209
190
  * depending on the mode of operation.
@@ -249,18 +230,6 @@ export declare class RequestQueue implements IStorage, IRequestManager {
249
230
  * @throws If the underlying storage no longer exists (e.g. it was deleted externally).
250
231
  */
251
232
  getInfo(): Promise<RequestQueueInfo>;
252
- /**
253
- * Fetches URLs from requestsFromUrl and returns them in format of list of requests
254
- */
255
- private fetchRequestsFromUrl;
256
- /**
257
- * Adds all fetched requests from a URL from a remote resource.
258
- */
259
- private addFetchedRequests;
260
- /**
261
- * @internal wraps public utility for mocking purposes
262
- */
263
- private downloadListOfUrls;
264
233
  /**
265
234
  * Opens a request queue and returns a promise resolving to an instance
266
235
  * of the {@link RequestQueue} class.
@@ -280,6 +249,7 @@ export declare class RequestQueue implements IStorage, IRequestManager {
280
249
  */
281
250
  static open(identifier?: string | StorageIdentifier | null, options?: StorageOpenOptions): Promise<RequestQueue>;
282
251
  }
252
+ /** @internal */
283
253
  export interface RequestQueueOptions {
284
254
  /** Resolved metadata for the request queue, as returned by the backend's `getMetadata()`. */
285
255
  metadata: RequestQueueInfo;