@crawlee/core 4.0.0-beta.25 → 4.0.0-beta.250

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 (265) hide show
  1. package/README.md +15 -15
  2. package/byte-utils.d.ts +17 -0
  3. package/byte-utils.js +42 -0
  4. package/configuration.d.ts +77 -235
  5. package/configuration.js +159 -227
  6. package/debug.d.ts +36 -0
  7. package/debug.js +70 -0
  8. package/errors.d.ts +45 -18
  9. package/errors.js +58 -27
  10. package/events/event-manager.d.ts +78 -0
  11. package/events/{event_manager.js → event-manager.js} +12 -13
  12. package/events/index.d.ts +3 -3
  13. package/events/index.js +2 -3
  14. package/events/local-event-manager.d.ts +32 -0
  15. package/events/local-event-manager.js +101 -0
  16. package/events/system-info.d.ts +38 -0
  17. package/events/system-info.js +1 -0
  18. package/index.d.ts +9 -12
  19. package/index.js +10 -11
  20. package/internal.d.ts +8 -0
  21. package/internal.js +9 -0
  22. package/iterables.d.ts +79 -0
  23. package/iterables.js +134 -0
  24. package/log.d.ts +77 -1
  25. package/log.js +134 -1
  26. package/memory-storage/consts.d.ts +4 -0
  27. package/memory-storage/consts.js +4 -0
  28. package/memory-storage/index.d.ts +1 -0
  29. package/memory-storage/index.js +1 -0
  30. package/memory-storage/memory-storage.d.ts +43 -0
  31. package/memory-storage/memory-storage.js +158 -0
  32. package/memory-storage/resource-clients/common/base-client.d.ts +4 -0
  33. package/memory-storage/resource-clients/common/base-client.js +6 -0
  34. package/memory-storage/resource-clients/dataset.d.ts +35 -0
  35. package/memory-storage/resource-clients/dataset.js +108 -0
  36. package/memory-storage/resource-clients/key-value-store.d.ts +54 -0
  37. package/memory-storage/resource-clients/key-value-store.js +179 -0
  38. package/memory-storage/resource-clients/request-queue.d.ts +55 -0
  39. package/memory-storage/resource-clients/request-queue.js +413 -0
  40. package/memory-storage/utils.d.ts +16 -0
  41. package/memory-storage/utils.js +41 -0
  42. package/owned-or-injected.d.ts +58 -0
  43. package/owned-or-injected.js +98 -0
  44. package/package.json +23 -23
  45. package/proxy-configuration.d.ts +118 -0
  46. package/proxy-configuration.js +141 -0
  47. package/recoverable-state.d.ts +177 -0
  48. package/recoverable-state.js +268 -0
  49. package/request.d.ts +48 -93
  50. package/request.js +133 -230
  51. package/serialization.d.ts +0 -1
  52. package/serialization.js +18 -18
  53. package/service-locator.d.ts +168 -0
  54. package/service-locator.js +255 -0
  55. package/storages/batched-adds.d.ts +37 -0
  56. package/storages/batched-adds.js +73 -0
  57. package/storages/dataset.d.ts +105 -58
  58. package/storages/dataset.js +292 -150
  59. package/storages/index.d.ts +11 -12
  60. package/storages/index.js +7 -12
  61. package/storages/key-value-store-codec.d.ts +32 -0
  62. package/storages/key-value-store-codec.js +108 -0
  63. package/storages/{key_value_store.d.ts → key-value-store.d.ts} +169 -72
  64. package/storages/key-value-store.js +767 -0
  65. package/storages/request-dedup-cache.d.ts +20 -0
  66. package/storages/request-dedup-cache.js +48 -0
  67. package/storages/{request_list.d.ts → request-list.d.ts} +33 -173
  68. package/storages/{request_list.js → request-list.js} +276 -255
  69. package/storages/request-loader.d.ts +127 -0
  70. package/storages/request-loader.js +36 -0
  71. package/storages/request-manager-tandem.d.ts +82 -0
  72. package/storages/request-manager-tandem.js +200 -0
  73. package/storages/request-manager.d.ts +119 -0
  74. package/storages/request-manager.js +1 -0
  75. package/storages/{request_provider.d.ts → request-queue.d.ts} +128 -159
  76. package/storages/request-queue.js +870 -0
  77. package/storages/storage-instance-manager.d.ts +86 -0
  78. package/storages/storage-instance-manager.js +256 -0
  79. package/storages/storage-stats.d.ts +48 -0
  80. package/storages/storage-stats.js +29 -0
  81. package/storages/transaction.d.ts +270 -0
  82. package/storages/transaction.js +296 -0
  83. package/storages/utils.d.ts +59 -11
  84. package/storages/utils.js +76 -16
  85. package/system-info/cpu-info.d.ts +67 -0
  86. package/system-info/cpu-info.js +216 -0
  87. package/system-info/memory-info.d.ts +31 -0
  88. package/system-info/memory-info.js +115 -0
  89. package/system-info/ps-tree.d.ts +17 -0
  90. package/system-info/ps-tree.js +144 -0
  91. package/system-info/runtime.d.ts +14 -0
  92. package/system-info/runtime.js +80 -0
  93. package/typedefs.d.ts +0 -6
  94. package/typedefs.js +0 -1
  95. package/url.d.ts +9 -0
  96. package/url.js +11 -0
  97. package/validators.d.ts +23 -18
  98. package/validators.js +14 -18
  99. package/autoscaling/autoscaled_pool.d.ts +0 -300
  100. package/autoscaling/autoscaled_pool.d.ts.map +0 -1
  101. package/autoscaling/autoscaled_pool.js +0 -533
  102. package/autoscaling/autoscaled_pool.js.map +0 -1
  103. package/autoscaling/index.d.ts +0 -4
  104. package/autoscaling/index.d.ts.map +0 -1
  105. package/autoscaling/index.js +0 -4
  106. package/autoscaling/index.js.map +0 -1
  107. package/autoscaling/snapshotter.d.ts +0 -189
  108. package/autoscaling/snapshotter.d.ts.map +0 -1
  109. package/autoscaling/snapshotter.js +0 -285
  110. package/autoscaling/snapshotter.js.map +0 -1
  111. package/autoscaling/system_status.d.ts +0 -181
  112. package/autoscaling/system_status.d.ts.map +0 -1
  113. package/autoscaling/system_status.js +0 -169
  114. package/autoscaling/system_status.js.map +0 -1
  115. package/configuration.d.ts.map +0 -1
  116. package/configuration.js.map +0 -1
  117. package/cookie_utils.d.ts +0 -43
  118. package/cookie_utils.d.ts.map +0 -1
  119. package/cookie_utils.js +0 -111
  120. package/cookie_utils.js.map +0 -1
  121. package/crawlers/context_pipeline.d.ts +0 -61
  122. package/crawlers/context_pipeline.d.ts.map +0 -1
  123. package/crawlers/context_pipeline.js +0 -99
  124. package/crawlers/context_pipeline.js.map +0 -1
  125. package/crawlers/crawler_commons.d.ts +0 -191
  126. package/crawlers/crawler_commons.d.ts.map +0 -1
  127. package/crawlers/crawler_commons.js +0 -108
  128. package/crawlers/crawler_commons.js.map +0 -1
  129. package/crawlers/crawler_utils.d.ts +0 -10
  130. package/crawlers/crawler_utils.d.ts.map +0 -1
  131. package/crawlers/crawler_utils.js +0 -12
  132. package/crawlers/crawler_utils.js.map +0 -1
  133. package/crawlers/error_snapshotter.d.ts +0 -60
  134. package/crawlers/error_snapshotter.d.ts.map +0 -1
  135. package/crawlers/error_snapshotter.js +0 -118
  136. package/crawlers/error_snapshotter.js.map +0 -1
  137. package/crawlers/error_tracker.d.ts +0 -55
  138. package/crawlers/error_tracker.d.ts.map +0 -1
  139. package/crawlers/error_tracker.js +0 -309
  140. package/crawlers/error_tracker.js.map +0 -1
  141. package/crawlers/index.d.ts +0 -7
  142. package/crawlers/index.d.ts.map +0 -1
  143. package/crawlers/index.js +0 -7
  144. package/crawlers/index.js.map +0 -1
  145. package/crawlers/internals/types.d.ts +0 -8
  146. package/crawlers/internals/types.d.ts.map +0 -1
  147. package/crawlers/internals/types.js +0 -2
  148. package/crawlers/internals/types.js.map +0 -1
  149. package/crawlers/statistics.d.ts +0 -213
  150. package/crawlers/statistics.d.ts.map +0 -1
  151. package/crawlers/statistics.js +0 -349
  152. package/crawlers/statistics.js.map +0 -1
  153. package/enqueue_links/enqueue_links.d.ts +0 -249
  154. package/enqueue_links/enqueue_links.d.ts.map +0 -1
  155. package/enqueue_links/enqueue_links.js +0 -269
  156. package/enqueue_links/enqueue_links.js.map +0 -1
  157. package/enqueue_links/index.d.ts +0 -3
  158. package/enqueue_links/index.d.ts.map +0 -1
  159. package/enqueue_links/index.js +0 -3
  160. package/enqueue_links/index.js.map +0 -1
  161. package/enqueue_links/shared.d.ts +0 -73
  162. package/enqueue_links/shared.d.ts.map +0 -1
  163. package/enqueue_links/shared.js +0 -212
  164. package/enqueue_links/shared.js.map +0 -1
  165. package/errors.d.ts.map +0 -1
  166. package/errors.js.map +0 -1
  167. package/events/event_manager.d.ts +0 -52
  168. package/events/event_manager.d.ts.map +0 -1
  169. package/events/event_manager.js.map +0 -1
  170. package/events/index.d.ts.map +0 -1
  171. package/events/index.js.map +0 -1
  172. package/events/local_event_manager.d.ts +0 -27
  173. package/events/local_event_manager.d.ts.map +0 -1
  174. package/events/local_event_manager.js +0 -75
  175. package/events/local_event_manager.js.map +0 -1
  176. package/index.d.ts.map +0 -1
  177. package/index.js.map +0 -1
  178. package/log.d.ts.map +0 -1
  179. package/log.js.map +0 -1
  180. package/proxy_configuration.d.ts +0 -169
  181. package/proxy_configuration.d.ts.map +0 -1
  182. package/proxy_configuration.js +0 -247
  183. package/proxy_configuration.js.map +0 -1
  184. package/recoverable_state.d.ts +0 -123
  185. package/recoverable_state.d.ts.map +0 -1
  186. package/recoverable_state.js +0 -140
  187. package/recoverable_state.js.map +0 -1
  188. package/request.d.ts.map +0 -1
  189. package/request.js.map +0 -1
  190. package/router.d.ts +0 -134
  191. package/router.d.ts.map +0 -1
  192. package/router.js +0 -164
  193. package/router.js.map +0 -1
  194. package/serialization.d.ts.map +0 -1
  195. package/serialization.js.map +0 -1
  196. package/session_pool/consts.d.ts +0 -4
  197. package/session_pool/consts.d.ts.map +0 -1
  198. package/session_pool/consts.js +0 -4
  199. package/session_pool/consts.js.map +0 -1
  200. package/session_pool/errors.d.ts +0 -8
  201. package/session_pool/errors.d.ts.map +0 -1
  202. package/session_pool/errors.js +0 -12
  203. package/session_pool/errors.js.map +0 -1
  204. package/session_pool/events.d.ts +0 -3
  205. package/session_pool/events.d.ts.map +0 -1
  206. package/session_pool/events.js +0 -3
  207. package/session_pool/events.js.map +0 -1
  208. package/session_pool/index.d.ts +0 -6
  209. package/session_pool/index.d.ts.map +0 -1
  210. package/session_pool/index.js +0 -6
  211. package/session_pool/index.js.map +0 -1
  212. package/session_pool/session.d.ts +0 -186
  213. package/session_pool/session.d.ts.map +0 -1
  214. package/session_pool/session.js +0 -278
  215. package/session_pool/session.js.map +0 -1
  216. package/session_pool/session_pool.d.ts +0 -258
  217. package/session_pool/session_pool.d.ts.map +0 -1
  218. package/session_pool/session_pool.js +0 -387
  219. package/session_pool/session_pool.js.map +0 -1
  220. package/storages/access_checking.d.ts +0 -13
  221. package/storages/access_checking.d.ts.map +0 -1
  222. package/storages/access_checking.js +0 -14
  223. package/storages/access_checking.js.map +0 -1
  224. package/storages/dataset.d.ts.map +0 -1
  225. package/storages/dataset.js.map +0 -1
  226. package/storages/index.d.ts.map +0 -1
  227. package/storages/index.js.map +0 -1
  228. package/storages/key_value_store.d.ts.map +0 -1
  229. package/storages/key_value_store.js +0 -512
  230. package/storages/key_value_store.js.map +0 -1
  231. package/storages/request_list.d.ts.map +0 -1
  232. package/storages/request_list.js.map +0 -1
  233. package/storages/request_list_adapter.d.ts +0 -58
  234. package/storages/request_list_adapter.d.ts.map +0 -1
  235. package/storages/request_list_adapter.js +0 -81
  236. package/storages/request_list_adapter.js.map +0 -1
  237. package/storages/request_manager_tandem.d.ts +0 -68
  238. package/storages/request_manager_tandem.d.ts.map +0 -1
  239. package/storages/request_manager_tandem.js +0 -124
  240. package/storages/request_manager_tandem.js.map +0 -1
  241. package/storages/request_provider.d.ts.map +0 -1
  242. package/storages/request_provider.js +0 -591
  243. package/storages/request_provider.js.map +0 -1
  244. package/storages/request_queue.d.ts +0 -128
  245. package/storages/request_queue.d.ts.map +0 -1
  246. package/storages/request_queue.js +0 -320
  247. package/storages/request_queue.js.map +0 -1
  248. package/storages/request_queue_v2.d.ts +0 -87
  249. package/storages/request_queue_v2.d.ts.map +0 -1
  250. package/storages/request_queue_v2.js +0 -438
  251. package/storages/request_queue_v2.js.map +0 -1
  252. package/storages/sitemap_request_list.d.ts +0 -250
  253. package/storages/sitemap_request_list.d.ts.map +0 -1
  254. package/storages/sitemap_request_list.js +0 -431
  255. package/storages/sitemap_request_list.js.map +0 -1
  256. package/storages/storage_manager.d.ts +0 -62
  257. package/storages/storage_manager.d.ts.map +0 -1
  258. package/storages/storage_manager.js +0 -105
  259. package/storages/storage_manager.js.map +0 -1
  260. package/storages/utils.d.ts.map +0 -1
  261. package/storages/utils.js.map +0 -1
  262. package/typedefs.d.ts.map +0 -1
  263. package/typedefs.js.map +0 -1
  264. package/validators.d.ts.map +0 -1
  265. package/validators.js.map +0 -1
@@ -0,0 +1,870 @@
1
+ import { inspect } from 'node:util';
2
+ import { isAsyncIterable, isIterable } from '@crawlee/utils/internal';
3
+ import { downloadListOfUrls } from '@crawlee/utils';
4
+ import { z } from 'zod';
5
+ import { LruCache } from '@apify/datastructures';
6
+ import { tryCancel } from '@apify/timeout';
7
+ import { Configuration } from '../configuration.js';
8
+ import { getObjectType } from '../debug.js';
9
+ import { EventType } from '../events/event-manager.js';
10
+ import { Request } from '../request.js';
11
+ import { serviceLocator } from '../service-locator.js';
12
+ import { parseArgument, schemas, validators } from '../validators.js';
13
+ import { activeStorageTransaction, rejectOperationInTransaction } from './transaction.js';
14
+ import { drainRequestBatches } from './batched-adds.js';
15
+ import { StorageStatsTracker } from './storage-stats.js';
16
+ import { resolveStorageIdentifier } from './storage-instance-manager.js';
17
+ import { getRequestId, purgeDefaultStorages } from './utils.js';
18
+ import { RequestDeduplicationCache } from './request-dedup-cache.js';
19
+ /**
20
+ * The maximum number of requests cached locally to avoid redundant calls to the storage backend.
21
+ * @internal
22
+ */
23
+ const MAX_CACHED_REQUESTS = 2_000_000;
24
+ const iterableSchema = z.custom((value) => isIterable(value) || isAsyncIterable(value), {
25
+ error: (issue) => `Expected an iterable or async iterable, got ${getObjectType(issue.input)}`,
26
+ });
27
+ const operationOptionsSchema = z.strictObject({
28
+ forefront: z.boolean().default(false),
29
+ });
30
+ const addRequestsOptionsSchema = z.strictObject({
31
+ forefront: z.boolean().default(false),
32
+ cache: z.boolean().default(true),
33
+ });
34
+ const addRequestsBatchedOptionsSchema = z.strictObject({
35
+ forefront: z.boolean().optional(),
36
+ waitForAllRequestsToBeAdded: z.boolean().default(false),
37
+ batchSize: schemas.anyNumber.default(1000),
38
+ waitBetweenBatchesMillis: schemas.anyNumber.default(1000),
39
+ maxNewRequests: schemas.anyNumber.optional(),
40
+ });
41
+ // Compiled: these run once per request.
42
+ const newRequestLikeSchema = z.compile(z.looseObject({
43
+ url: z.string(),
44
+ id: z.undefined().optional(),
45
+ }));
46
+ const handledRequestSchema = z.compile(z.looseObject({
47
+ id: z.string(),
48
+ uniqueKey: z.string(),
49
+ handledAt: z.string().optional(),
50
+ }));
51
+ const reclaimedRequestSchema = z.compile(z.looseObject({
52
+ id: z.string(),
53
+ uniqueKey: z.string(),
54
+ }));
55
+ const uniqueKeySchema = z.string();
56
+ const openOptionsSchema = z.strictObject({
57
+ configuration: z.instanceof(Configuration).optional(),
58
+ storageBackend: validators.storageBackend.optional(),
59
+ proxyConfiguration: validators.proxyConfiguration.optional(),
60
+ httpClient: schemas.httpClient.optional(),
61
+ });
62
+ /**
63
+ * Represents a queue of URLs to crawl, which is used for deep crawling of websites
64
+ * where you start with several URLs and then recursively
65
+ * follow links to other pages. The data structure supports both breadth-first and depth-first crawling orders.
66
+ *
67
+ * Each URL is represented using an instance of the {@link Request} class.
68
+ * The queue can only contain unique URLs. More precisely, it can only contain {@link Request} instances
69
+ * with distinct `uniqueKey` properties. By default, `uniqueKey` is generated from the URL, but it can also be overridden.
70
+ * To add a single URL multiple times to the queue,
71
+ * corresponding {@link Request} objects will need to have different `uniqueKey` properties.
72
+ *
73
+ * Do not instantiate this class directly, use the {@link RequestQueue.open} function instead.
74
+ *
75
+ * `RequestQueue` is used by {@link BasicCrawler}, {@link CheerioCrawler}, {@link PuppeteerCrawler}
76
+ * and {@link PlaywrightCrawler} as a source of URLs to crawl.
77
+ * Unlike {@link RequestList}, `RequestQueue` supports dynamic adding and removing of requests.
78
+ * On the other hand, the queue is not optimized for operations that add or remove a large number of URLs in a batch.
79
+ *
80
+ * **Example usage:**
81
+ *
82
+ * ```javascript
83
+ * // Open the default request queue associated with the crawler run
84
+ * const queue = await RequestQueue.open();
85
+ *
86
+ * // Open a named request queue
87
+ * const queueWithName = await RequestQueue.open('some-name');
88
+ *
89
+ * // Enqueue few requests
90
+ * await queue.addRequest({ url: 'http://example.com/aaa' });
91
+ * await queue.addRequest({ url: 'http://example.com/bbb' });
92
+ * await queue.addRequest({ url: 'http://example.com/foo/bar' }, { forefront: true });
93
+ * ```
94
+ * @category Sources
95
+ */
96
+ export class RequestQueue {
97
+ id;
98
+ name;
99
+ backend;
100
+ #proxyConfiguration;
101
+ log;
102
+ #requestCache;
103
+ /**
104
+ * Remembers the `requestId` of every request already submitted to the client — including background
105
+ * batches that `requestCache` skips — so overlapping URL sets aren't re-submitted.
106
+ * See {@link RequestDeduplicationCache} for why this is a separate, cheaper cache.
107
+ */
108
+ #requestSeenCache;
109
+ #queuePausedForMigration = false;
110
+ #inProgressRequestBatchCount = 0;
111
+ /**
112
+ * The largest expected request-processing time (in seconds) seen so far via
113
+ * {@link setExpectedRequestProcessingTimeSecs}. Used to ensure that value is only ever raised, never
114
+ * lowered, before being forwarded to the storage backend.
115
+ */
116
+ #expectedRequestProcessingSecs = 0;
117
+ #httpClient;
118
+ #events;
119
+ #statsTracker = new StorageStatsTracker({
120
+ writeCount: 0,
121
+ headItemReadCount: 0,
122
+ });
123
+ /**
124
+ * Backend-independent usage counters tracked for this request queue (write operations and
125
+ * queue-head reads issued to the underlying storage backend). Counted per backend call.
126
+ */
127
+ get stats() {
128
+ return this.#statsTracker.current;
129
+ }
130
+ /**
131
+ * @internal
132
+ */
133
+ constructor(options) {
134
+ this.id = options.metadata.id;
135
+ this.name = options.metadata.name;
136
+ this.#events = serviceLocator.getEventManager();
137
+ this.backend = options.backend;
138
+ this.#proxyConfiguration = options.proxyConfiguration;
139
+ this.#requestCache = new LruCache({ maxLength: MAX_CACHED_REQUESTS });
140
+ this.#requestSeenCache = new RequestDeduplicationCache();
141
+ this.log = serviceLocator.getLogger().child({ prefix: `RequestQueue(${this.id}, ${this.name ?? 'no-name'})` });
142
+ this.#events.on(EventType.MIGRATING, async () => {
143
+ this.#queuePausedForMigration = true;
144
+ });
145
+ }
146
+ /**
147
+ * Returns the total number of requests in the queue (i.e. pending + handled).
148
+ *
149
+ * Survives restarts and actor migrations.
150
+ */
151
+ async getTotalCount() {
152
+ const { totalRequestCount } = await this.getInfo();
153
+ return totalRequestCount;
154
+ }
155
+ /**
156
+ * Returns the total number of pending requests in the queue.
157
+ *
158
+ * Survives restarts and Actor migrations.
159
+ */
160
+ async getPendingCount() {
161
+ const { totalRequestCount, handledRequestCount } = await this.getInfo();
162
+ return totalRequestCount - handledRequestCount;
163
+ }
164
+ /**
165
+ * Adds a request to the queue.
166
+ *
167
+ * If a request with the same `uniqueKey` property is already present in the queue,
168
+ * it will not be updated. You can find out whether this happened from the resulting
169
+ * {@link QueueOperationInfo} object.
170
+ *
171
+ * To add multiple requests to the queue by extracting links from a webpage,
172
+ * see the {@link enqueueLinks} helper function.
173
+ *
174
+ * @param requestLike {@link Request} object or vanilla object with request data.
175
+ * Note that the function sets the `uniqueKey` and `id` fields to the passed Request.
176
+ * @param [options] Request queue operation options.
177
+ */
178
+ async addRequest(requestLike, options = {}) {
179
+ const transaction = activeStorageTransaction();
180
+ parseArgument(requestLike, schemas.anyObject);
181
+ const { forefront } = parseArgument(options, operationOptionsSchema);
182
+ if ('requestsFromUrl' in requestLike) {
183
+ const requests = await this.#fetchRequestsFromUrl(requestLike);
184
+ const processedRequests = await this.#addFetchedRequests(requestLike, requests, options);
185
+ return { ...processedRequests[0], forefront };
186
+ }
187
+ parseArgument(requestLike, newRequestLikeSchema);
188
+ const request = requestLike instanceof Request ? requestLike : new Request(requestLike);
189
+ if (transaction?.policy.requestQueue === 'deferred') {
190
+ return this.#addRequestDeferred(transaction, request, forefront);
191
+ }
192
+ const cacheKey = getRequestId(request.uniqueKey);
193
+ const cachedInfo = this.#requestCache.get(cacheKey);
194
+ if (cachedInfo) {
195
+ request.id = cachedInfo.id;
196
+ this.#recordRequestJournalEntry(transaction, [request], forefront, true);
197
+ return {
198
+ wasAlreadyPresent: true,
199
+ // We may assume that if request is in local cache then also the information if the
200
+ // request was already handled is there because just one client should be using one queue.
201
+ wasAlreadyHandled: cachedInfo.isHandled,
202
+ requestId: cachedInfo.id,
203
+ uniqueKey: cachedInfo.uniqueKey,
204
+ forefront,
205
+ };
206
+ }
207
+ this.#statsTracker.add('writeCount');
208
+ const { processedRequests } = await this.backend.addBatchOfRequests([request], { forefront });
209
+ this.#recordRequestJournalEntry(transaction, [request], forefront, true);
210
+ const queueOperationInfo = {
211
+ ...processedRequests[0],
212
+ uniqueKey: request.uniqueKey,
213
+ forefront,
214
+ };
215
+ this.#cacheRequest(cacheKey, queueOperationInfo);
216
+ this.#requestSeenCache.add(cacheKey, request.id);
217
+ return queueOperationInfo;
218
+ }
219
+ /**
220
+ * Journals an addition for introspection only; these entries are never replayed. A no-op unless the
221
+ * transaction is open, so detached and outliving writers stay out of the journal.
222
+ */
223
+ #recordRequestJournalEntry(transaction, requests, forefront, writeThrough) {
224
+ if (!transaction?.isActive || requests.length === 0)
225
+ return;
226
+ transaction.recordJournalEntry({
227
+ type: 'requestQueue',
228
+ participant: this,
229
+ requests: requests.map((request) => ({
230
+ url: request.url,
231
+ uniqueKey: request.uniqueKey,
232
+ label: request.label,
233
+ })),
234
+ forefront,
235
+ writeThrough,
236
+ });
237
+ }
238
+ /**
239
+ * The requests buffered by the given transaction for this queue, keyed by `uniqueKey` — a dedup
240
+ * index derived from the transaction journal.
241
+ */
242
+ #bufferedRequests(transaction) {
243
+ const buffered = new Map();
244
+ // Only `deferred` records snapshots, so scanning the journal under `writeThrough` never finds any.
245
+ if (transaction.policy.requestQueue !== 'deferred')
246
+ return buffered;
247
+ for (const entry of transaction.journal) {
248
+ if (entry.type !== 'requestQueue' || entry.participant !== this)
249
+ continue;
250
+ for (const request of entry.requests) {
251
+ if (request.snapshot !== undefined)
252
+ buffered.set(request.uniqueKey, request.snapshot);
253
+ }
254
+ }
255
+ return buffered;
256
+ }
257
+ /**
258
+ * Adds a request under the `deferred` policy: journaled now, really added by the commit replay.
259
+ * A new request's `requestId` is the local `uniqueKey` hash and is **provisional** — never write it
260
+ * to `request.id` or the dedup caches. Dedup is cheapest-first: buffer, caches, then a backend probe.
261
+ */
262
+ async #addRequestDeferred(transaction, request, forefront, buffered = this.#bufferedRequests(transaction)) {
263
+ // This transaction's own buffered adds; the shared caches never see them (provisional ids).
264
+ if (buffered.has(request.uniqueKey)) {
265
+ this.#recordRequestJournalEntry(transaction, [request], forefront, false);
266
+ return {
267
+ wasAlreadyPresent: true,
268
+ wasAlreadyHandled: false,
269
+ requestId: getRequestId(request.uniqueKey),
270
+ uniqueKey: request.uniqueKey,
271
+ forefront,
272
+ };
273
+ }
274
+ // The caches hold real backend ids. Only *writing* provisional ids to them would be wrong;
275
+ // reading saves a probe. Same lookup as the write-through path.
276
+ const cacheKey = getRequestId(request.uniqueKey);
277
+ const cachedInfo = this.#requestCache.get(cacheKey);
278
+ const knownRequestId = cachedInfo?.id ?? this.#requestSeenCache.get(cacheKey);
279
+ if (knownRequestId) {
280
+ this.#recordRequestJournalEntry(transaction, [request], forefront, false);
281
+ return {
282
+ wasAlreadyPresent: true,
283
+ // The dedup cache doesn't track the handled state; only the full record does.
284
+ wasAlreadyHandled: cachedInfo?.isHandled ?? false,
285
+ requestId: knownRequestId,
286
+ uniqueKey: request.uniqueKey,
287
+ forefront,
288
+ };
289
+ }
290
+ // The caches are bounded, so a miss is not proof of absence - probe for an accurate answer.
291
+ const existing = await this.backend.getRequest(request.uniqueKey);
292
+ if (existing) {
293
+ this.#recordRequestJournalEntry(transaction, [request], forefront, false);
294
+ return {
295
+ wasAlreadyPresent: true,
296
+ wasAlreadyHandled: existing.handledAt != null,
297
+ requestId: existing.id,
298
+ uniqueKey: request.uniqueKey,
299
+ forefront,
300
+ };
301
+ }
302
+ // The entry below *is* the write, so a transaction closed during the probe must not receive it -
303
+ // pass through instead, per the closed-transaction rule. Under `deferred` that can land an
304
+ // addition a rollback would have discarded; dedup bounds that cost, silent loss is unbounded.
305
+ if (!transaction.isActive) {
306
+ return await this.addRequest(request, { forefront });
307
+ }
308
+ const snapshot = JSON.parse(JSON.stringify(request));
309
+ // Strip-list, not allow-list: every user-facing field flows through, including ones added to
310
+ // `Request` in the future. The exceptions are `id` and `handledAt`, the two backend-owned
311
+ // lifecycle fields.
312
+ delete snapshot.id;
313
+ delete snapshot.handledAt;
314
+ transaction.recordJournalEntry({
315
+ type: 'requestQueue',
316
+ participant: this,
317
+ requests: [{ url: request.url, uniqueKey: request.uniqueKey, label: request.label, snapshot }],
318
+ forefront,
319
+ writeThrough: false,
320
+ });
321
+ buffered.set(request.uniqueKey, snapshot);
322
+ return {
323
+ wasAlreadyPresent: false,
324
+ wasAlreadyHandled: false,
325
+ requestId: getRequestId(request.uniqueKey),
326
+ uniqueKey: request.uniqueKey,
327
+ forefront,
328
+ };
329
+ }
330
+ /** @internal */
331
+ async commitJournalEntries(entries) {
332
+ // Replay through `backend.addBatchOfRequests`, *not* the batched frontend wrapper - the wrapper
333
+ // resolves after the first chunk and sleeps between the rest, neither of which commit may
334
+ // inherit. One call per `forefront` flag; the order of forefront additions is arbitrary anyway.
335
+ for (const forefront of [false, true]) {
336
+ const requests = entries.flatMap((entry) => entry.type === 'requestQueue' && entry.forefront === forefront
337
+ ? // Requests without a snapshot were deduplicated or written through; nothing to replay.
338
+ entry.requests
339
+ .filter((journaled) => journaled.snapshot !== undefined)
340
+ .map((journaled) => Request.fromSchema(journaled.snapshot))
341
+ : []);
342
+ if (requests.length === 0)
343
+ continue;
344
+ this.#statsTracker.add('writeCount');
345
+ const { processedRequests, unprocessedRequests } = await this.backend.addBatchOfRequests(requests, {
346
+ forefront,
347
+ });
348
+ // Only now, with the real backend-assigned ids, may the shared dedup caches be populated.
349
+ for (const processed of processedRequests) {
350
+ const cacheKey = getRequestId(processed.uniqueKey);
351
+ this.#cacheRequest(cacheKey, { ...processed, forefront });
352
+ this.#requestSeenCache.add(cacheKey, processed.requestId);
353
+ }
354
+ if (unprocessedRequests.length > 0) {
355
+ // Warn and skip, rather than retry or fail. `unprocessedRequests` is what remains after
356
+ // the backend's own transient-error handling - a semantic rejection that retrying here
357
+ // would only re-poke. And failing the commit would let one malformed request hold the
358
+ // whole transaction hostage.
359
+ this.log.warning('Some requests were rejected by the request queue while committing a storage transaction and will be skipped. ' +
360
+ "This usually means the request data is malformed (e.g. an invalid 'userData' shape).", { unprocessedRequests });
361
+ }
362
+ }
363
+ }
364
+ /**
365
+ * Adds requests to the queue in batches of 25. This method will wait till all the requests are added
366
+ * to the queue before resolving. You should prefer using `queue.addRequestsBatched()` or `crawler.addRequests()`
367
+ * if you don't want to block the processing, as those methods will only wait for the initial 1000 requests,
368
+ * start processing right after that happens, and continue adding more in the background.
369
+ *
370
+ * If a request passed in is already present due to its `uniqueKey` property being the same,
371
+ * it will not be updated. You can find out whether this happened by finding the request in the resulting
372
+ * {@link BatchAddRequestsResult} object.
373
+ *
374
+ * @param requestsLike {@link Request} objects or vanilla objects with request data.
375
+ * Note that the function sets the `uniqueKey` and `id` fields to the passed requests if missing.
376
+ * @param [options] Request queue operation options.
377
+ */
378
+ async addRequests(requestsLike, options = {}) {
379
+ const transaction = activeStorageTransaction();
380
+ parseArgument(requestsLike, iterableSchema);
381
+ const { forefront, cache } = parseArgument(options, addRequestsOptionsSchema);
382
+ const uniqueKeyToCacheKey = new Map();
383
+ const getCachedRequestId = (uniqueKey) => {
384
+ const cached = uniqueKeyToCacheKey.get(uniqueKey);
385
+ if (cached)
386
+ return cached;
387
+ const newCacheKey = getRequestId(uniqueKey);
388
+ uniqueKeyToCacheKey.set(uniqueKey, newCacheKey);
389
+ return newCacheKey;
390
+ };
391
+ const results = {
392
+ processedRequests: [],
393
+ unprocessedRequests: [],
394
+ };
395
+ const requests = [];
396
+ for await (const requestLike of requestsLike) {
397
+ if (typeof requestLike === 'string') {
398
+ requests.push(new Request({ url: requestLike }));
399
+ }
400
+ else if ('requestsFromUrl' in requestLike) {
401
+ const fetchedRequests = await this.#fetchRequestsFromUrl(requestLike);
402
+ await this.#addFetchedRequests(requestLike, fetchedRequests, options);
403
+ }
404
+ else {
405
+ requests.push(requestLike instanceof Request ? requestLike : new Request(requestLike));
406
+ }
407
+ }
408
+ if (transaction?.policy.requestQueue === 'deferred') {
409
+ const buffered = this.#bufferedRequests(transaction);
410
+ for (const request of requests) {
411
+ results.processedRequests.push(await this.#addRequestDeferred(transaction, request, forefront, buffered));
412
+ }
413
+ return results;
414
+ }
415
+ this.#recordRequestJournalEntry(transaction, requests, forefront, true);
416
+ const requestsToAdd = new Map();
417
+ for (const request of requests) {
418
+ const cacheKey = getCachedRequestId(request.uniqueKey);
419
+ // Prefer the full `requestCache` record; fall back to the dedup cache for background batches it skips.
420
+ const cachedInfo = this.#requestCache.get(cacheKey);
421
+ const knownRequestId = cachedInfo?.id ?? this.#requestSeenCache.get(cacheKey);
422
+ if (knownRequestId) {
423
+ request.id = knownRequestId;
424
+ results.processedRequests.push({
425
+ wasAlreadyPresent: true,
426
+ // The dedup cache doesn't track the handled state; only the full record does.
427
+ wasAlreadyHandled: cachedInfo?.isHandled ?? false,
428
+ requestId: knownRequestId,
429
+ uniqueKey: request.uniqueKey,
430
+ });
431
+ }
432
+ else if (!requestsToAdd.has(request.uniqueKey)) {
433
+ requestsToAdd.set(request.uniqueKey, request);
434
+ }
435
+ }
436
+ // Early exit if all provided requests were already added
437
+ if (!requestsToAdd.size) {
438
+ return results;
439
+ }
440
+ this.#statsTracker.add('writeCount');
441
+ const apiResults = await this.backend.addBatchOfRequests([...requestsToAdd.values()], { forefront });
442
+ // Report unprocessed requests
443
+ results.unprocessedRequests = apiResults.unprocessedRequests;
444
+ // Add all new requests to the requestCache
445
+ for (const newRequest of apiResults.processedRequests) {
446
+ // Add the new request to the processed list
447
+ results.processedRequests.push(newRequest);
448
+ const cacheKey = getCachedRequestId(newRequest.uniqueKey);
449
+ if (cache) {
450
+ this.#cacheRequest(cacheKey, { ...newRequest, forefront });
451
+ }
452
+ // Unlike `requestCache`, populate this on every batch (including background ones).
453
+ this.#requestSeenCache.add(cacheKey, newRequest.requestId);
454
+ }
455
+ return results;
456
+ }
457
+ /**
458
+ * Adds requests to the queue in batches. By default, it will resolve after the initial batch is added, and continue
459
+ * adding the rest in the background. You can configure the batch size via `batchSize` option and the sleep time in between
460
+ * the batches via `waitBetweenBatchesMillis`. If you want to wait for all batches to be added to the queue, you can use
461
+ * the `waitForAllRequestsToBeAdded` promise you get in the response object.
462
+ *
463
+ * @param requests The requests to add
464
+ * @param options Options for the request queue
465
+ */
466
+ async addRequestsBatched(requests, options = {}) {
467
+ parseArgument(requests, iterableSchema);
468
+ const { forefront, waitForAllRequestsToBeAdded, batchSize, waitBetweenBatchesMillis, maxNewRequests } = parseArgument(options, addRequestsBatchedOptionsSchema);
469
+ const addRequest = this.addRequest.bind(this);
470
+ async function* generateRequests() {
471
+ for await (const opts of requests) {
472
+ // Validate the input
473
+ if (typeof opts === 'object' && opts !== null) {
474
+ if (opts.url !== undefined && typeof opts.url !== 'string') {
475
+ throw new Error(`Request options are not valid, the 'url' property is not a string. Input: ${inspect(opts)}`);
476
+ }
477
+ if ('id' in opts && opts.id !== undefined) {
478
+ throw new Error(`Request options are not valid, the 'id' property must not be present. Input: ${inspect(opts)}`);
479
+ }
480
+ if (opts.requestsFromUrl !== undefined &&
481
+ typeof opts.requestsFromUrl !== 'string') {
482
+ throw new Error(`Request options are not valid, the 'requestsFromUrl' property is not a string. Input: ${inspect(opts)}`);
483
+ }
484
+ }
485
+ if (opts && typeof opts === 'object' && 'requestsFromUrl' in opts) {
486
+ // Handle URL lists right away
487
+ await addRequest(opts, { forefront });
488
+ }
489
+ else {
490
+ // Yield valid requests
491
+ yield typeof opts === 'string' ? { url: opts } : opts;
492
+ }
493
+ }
494
+ }
495
+ return drainRequestBatches({
496
+ items: generateRequests(),
497
+ batchSize,
498
+ waitBetweenBatchesMillis,
499
+ waitForAllRequestsToBeAdded,
500
+ maxNewRequests,
501
+ /**
502
+ * Requests the backend reports as unprocessed are warned about and skipped rather than retried:
503
+ * `unprocessedRequests` is what remains after the backend's own transient-error handling - a
504
+ * semantic rejection (e.g. a malformed `userData` shape) that re-sending would only re-poke.
505
+ * Retrying transient failures is the storage backend's job, not the frontend's.
506
+ */
507
+ processChunk: async (chunk, isInitial) => {
508
+ const { processedRequests, unprocessedRequests } = await this.addRequests(chunk, {
509
+ forefront,
510
+ cache: isInitial,
511
+ });
512
+ if (unprocessedRequests.length > 0) {
513
+ this.log.warning('Some requests were rejected by the request queue and will be skipped. ' +
514
+ "This usually means the request data is malformed (e.g. an invalid 'userData' shape).", { unprocessedRequests });
515
+ }
516
+ return processedRequests;
517
+ },
518
+ trackBackgroundBatches: (batches) => {
519
+ this.#inProgressRequestBatchCount += 1;
520
+ void batches.finally(() => {
521
+ this.#inProgressRequestBatchCount -= 1;
522
+ });
523
+ },
524
+ });
525
+ }
526
+ /**
527
+ * Gets the request from the queue specified by its `uniqueKey`.
528
+ *
529
+ * @param uniqueKey Unique key of the request.
530
+ * @returns Returns the request object, or `null` if it was not found.
531
+ */
532
+ async getRequest(uniqueKey) {
533
+ const transaction = activeStorageTransaction();
534
+ parseArgument(uniqueKey, uniqueKeySchema);
535
+ // Requests buffered by the active transaction (under the `deferred` write policy) are visible to it.
536
+ const buffered = transaction && this.#bufferedRequests(transaction).get(uniqueKey);
537
+ if (buffered) {
538
+ return Request.fromSchema(buffered);
539
+ }
540
+ const schema = await this.backend.getRequest(uniqueKey);
541
+ if (!schema)
542
+ return null;
543
+ return Request.fromSchema(schema);
544
+ }
545
+ /**
546
+ * Returns a next request in the queue to be processed, or `null` if there are no more pending requests.
547
+ *
548
+ * Once you successfully finish processing of the request, you need to call
549
+ * {@link RequestQueue.markRequestAsHandled}
550
+ * to mark the request as handled in the queue. If there was some error in processing the request,
551
+ * call {@link RequestQueue.reclaimRequest} instead,
552
+ * so that the queue will give the request to some other consumer in another call to the `fetchNextRequest` function.
553
+ *
554
+ * Note that the `null` return value doesn't mean the queue processing finished,
555
+ * it means there are currently no pending requests.
556
+ * To check whether all requests in queue were finished,
557
+ * use {@link RequestQueue.checkReadiness} instead.
558
+ *
559
+ * @returns
560
+ * Returns the request object or `null` if there are no more pending requests.
561
+ */
562
+ async fetchNextRequest() {
563
+ rejectOperationInTransaction('RequestQueue.fetchNextRequest()', 'it is part of the crawler request-processing bookkeeping, which a transaction must not affect.');
564
+ if (this.#queuePausedForMigration) {
565
+ return null;
566
+ }
567
+ this.#statsTracker.add('headItemReadCount');
568
+ const schema = await this.backend.fetchNextRequest();
569
+ if (!schema)
570
+ return null;
571
+ return Request.fromSchema(schema);
572
+ }
573
+ /**
574
+ * Marks a request that was previously returned by the
575
+ * {@link RequestQueue.fetchNextRequest}
576
+ * function as handled after successful processing.
577
+ * Handled requests will never again be returned by the `fetchNextRequest` function.
578
+ */
579
+ async markRequestAsHandled(request) {
580
+ rejectOperationInTransaction('RequestQueue.markRequestAsHandled()', 'it is part of the crawler request-processing bookkeeping, which a transaction must not affect.');
581
+ parseArgument(request, handledRequestSchema);
582
+ const forefront = this.#requestCache.get(getRequestId(request.uniqueKey))?.forefront ?? false;
583
+ const handledAt = request.handledAt ?? new Date().toISOString();
584
+ this.#statsTracker.add('writeCount');
585
+ const processedRequest = await this.backend.markRequestAsHandled({
586
+ ...request,
587
+ handledAt,
588
+ });
589
+ // The request was not in progress (e.g. already handled) — nothing to do.
590
+ if (!processedRequest) {
591
+ return null;
592
+ }
593
+ request.handledAt = handledAt;
594
+ const queueOperationInfo = {
595
+ ...processedRequest,
596
+ uniqueKey: request.uniqueKey,
597
+ forefront,
598
+ };
599
+ this.#cacheRequest(getRequestId(request.uniqueKey), queueOperationInfo);
600
+ return queueOperationInfo;
601
+ }
602
+ /**
603
+ * Reclaims a failed request back to the queue, so that it can be returned for processing later again
604
+ * by another call to {@link RequestQueue.fetchNextRequest}.
605
+ * The request record in the queue is updated using the provided `request` parameter.
606
+ * For example, this lets you store the number of retries or error messages for the request.
607
+ */
608
+ async reclaimRequest(request, options = {}) {
609
+ rejectOperationInTransaction('RequestQueue.reclaimRequest()', 'it is part of the crawler request-processing bookkeeping, which a transaction must not affect.');
610
+ parseArgument(request, reclaimedRequestSchema);
611
+ const { forefront } = parseArgument(options, operationOptionsSchema);
612
+ this.#statsTracker.add('writeCount');
613
+ const processedRequest = await this.backend.reclaimRequest(request, {
614
+ forefront,
615
+ });
616
+ // The request was not in progress — nothing to reclaim.
617
+ if (!processedRequest) {
618
+ return null;
619
+ }
620
+ const queueOperationInfo = {
621
+ ...processedRequest,
622
+ uniqueKey: request.uniqueKey,
623
+ forefront,
624
+ };
625
+ this.#cacheRequest(getRequestId(request.uniqueKey), queueOperationInfo);
626
+ return queueOperationInfo;
627
+ }
628
+ /**
629
+ * A queue hands requests out as fast as they are asked for; pacing is a job for a manager wrapped around it,
630
+ * such as {@link ThrottlingRequestManager}.
631
+ * @inheritdoc
632
+ */
633
+ recordPacingSignal(_signal) {
634
+ return false;
635
+ }
636
+ /**
637
+ * Reports whether the queue has a request to hand over, is waiting on one, or is done.
638
+ *
639
+ * `waiting` means requests are in progress (fetched but not yet handled or reclaimed, possibly by another
640
+ * client sharing the queue) or a background add is still landing; neither has a clock, so no `readyAt`.
641
+ *
642
+ * Due to the nature of distributed storage used by the queue, `finished` may occasionally arrive a probe or
643
+ * two late, but it is never reported early.
644
+ */
645
+ async checkReadiness() {
646
+ const transaction = activeStorageTransaction();
647
+ // Requests buffered by the active transaction count as pending from its point of view.
648
+ if (transaction && this.#bufferedRequests(transaction).size > 0) {
649
+ return { status: 'ready' };
650
+ }
651
+ // Something fetchable outranks everything below, so this is the only backend call a probe needs.
652
+ if (!(await this.backend.isEmpty())) {
653
+ return { status: 'ready' };
654
+ }
655
+ // We are not finished if we're still adding new requests in the background.
656
+ if (this.#inProgressRequestBatchCount > 0) {
657
+ return { status: 'waiting' };
658
+ }
659
+ return (await this.backend.isFinished()) ? { status: 'finished' } : { status: 'waiting' };
660
+ }
661
+ /**
662
+ * Tells the queue how long a consumer expects to hold a fetched request before marking it handled
663
+ * or reclaiming it (typically the request-handler timeout plus padding), so that a storage backend
664
+ * that reserves requests via locking does not hand the same request out again while it is still
665
+ * being processed.
666
+ *
667
+ * Several consumers may share one queue (and therefore one client) in a single process, so we only
668
+ * ever raise the reservation duration, never lower it — otherwise a short-lived consumer could cut
669
+ * short the reservation of a long-lived one and have its in-flight request stolen.
670
+ */
671
+ async setExpectedRequestProcessingTimeSecs(secs) {
672
+ if (secs <= this.#expectedRequestProcessingSecs) {
673
+ return;
674
+ }
675
+ this.#expectedRequestProcessingSecs = secs;
676
+ await this.backend.setExpectedRequestProcessingTimeSecs?.(secs);
677
+ }
678
+ /**
679
+ * @inheritdoc
680
+ * Unlike {@link RequestQueue.setExpectedRequestProcessingTimeSecs}, which sizes every future lock,
681
+ * this only touches the one request it is given.
682
+ */
683
+ async extendRequestProcessingTimeSecs(request, secs) {
684
+ if (!request.id) {
685
+ return false;
686
+ }
687
+ return (await this.backend.extendRequestProcessingTimeSecs?.(request.id, secs)) ?? false;
688
+ }
689
+ /**
690
+ * Caches information about request to beware of unneeded addRequest() calls.
691
+ */
692
+ #cacheRequest(cacheKey, queueOperationInfo) {
693
+ // Remove the previous entry, as otherwise our cache will never update 👀
694
+ this.#requestCache.remove(cacheKey);
695
+ this.#requestCache.add(cacheKey, {
696
+ id: queueOperationInfo.requestId,
697
+ isHandled: queueOperationInfo.wasAlreadyHandled,
698
+ uniqueKey: queueOperationInfo.uniqueKey,
699
+ hydrated: null,
700
+ forefront: queueOperationInfo.forefront,
701
+ });
702
+ }
703
+ /**
704
+ * Removes the queue either from the Apify Cloud storage or from the local database,
705
+ * depending on the mode of operation.
706
+ */
707
+ async drop() {
708
+ rejectOperationInTransaction('RequestQueue.drop()');
709
+ await this.backend.drop();
710
+ serviceLocator.getStorageInstanceManager().removeFromCache(this);
711
+ }
712
+ /**
713
+ * Remove all requests from the queue but keep the queue itself, resetting it
714
+ * so it can be reused (e.g. across multiple `crawler.run()` calls).
715
+ */
716
+ async purge() {
717
+ rejectOperationInTransaction('RequestQueue.purge()');
718
+ await this.backend.purge();
719
+ // Reset in-memory bookkeeping so the queue behaves as if freshly opened.
720
+ this.#requestCache.clear();
721
+ this.#requestSeenCache.clear();
722
+ this.#inProgressRequestBatchCount = 0;
723
+ // Reset the expected-processing-time high-water mark too, otherwise the monotonic-raise guard
724
+ // in `setExpectedRequestProcessingTimeSecs` would let a value raised in an earlier run leak into a
725
+ // later one and silently swallow a lower hint (the queue is meant to be reusable across runs).
726
+ this.#expectedRequestProcessingSecs = 0;
727
+ }
728
+ /**
729
+ * @inheritdoc
730
+ */
731
+ async *[Symbol.asyncIterator]() {
732
+ while (true) {
733
+ const req = await this.fetchNextRequest();
734
+ if (!req)
735
+ break;
736
+ yield req;
737
+ }
738
+ }
739
+ /**
740
+ * Returns the number of handled requests.
741
+ *
742
+ * This function is just a convenient shortcut for:
743
+ *
744
+ * ```javascript
745
+ * const { handledRequestCount } = await queue.getInfo();
746
+ * ```
747
+ * @inheritdoc
748
+ */
749
+ async getHandledCount() {
750
+ // NOTE: We keep this function for compatibility with RequestList.getHandledCount()
751
+ const { handledRequestCount } = await this.getInfo();
752
+ return handledRequestCount;
753
+ }
754
+ /**
755
+ * Returns an object containing general information about the request queue.
756
+ *
757
+ * **Example:**
758
+ * ```
759
+ * {
760
+ * id: "WkzbQMuFYuamGv3YF",
761
+ * name: "my-queue",
762
+ * createdAt: new Date("2015-12-12T07:34:14.202Z"),
763
+ * modifiedAt: new Date("2015-12-13T08:36:13.202Z"),
764
+ * accessedAt: new Date("2015-12-14T08:36:13.202Z"),
765
+ * totalRequestCount: 25,
766
+ * handledRequestCount: 5,
767
+ * pendingRequestCount: 20,
768
+ * }
769
+ * ```
770
+ *
771
+ * @throws If the underlying storage no longer exists (e.g. it was deleted externally).
772
+ */
773
+ async getInfo() {
774
+ const transaction = activeStorageTransaction();
775
+ const metadata = await this.backend.getMetadata();
776
+ const bufferedCount = transaction ? this.#bufferedRequests(transaction).size : 0;
777
+ if (bufferedCount > 0) {
778
+ return {
779
+ ...metadata,
780
+ totalRequestCount: metadata.totalRequestCount + bufferedCount,
781
+ pendingRequestCount: metadata.pendingRequestCount + bufferedCount,
782
+ };
783
+ }
784
+ return metadata;
785
+ }
786
+ /**
787
+ * Fetches URLs from requestsFromUrl and returns them in format of list of requests
788
+ */
789
+ async #fetchRequestsFromUrl(source) {
790
+ const { requestsFromUrl, regex, ...sharedOpts } = source;
791
+ // Download remote resource and parse URLs.
792
+ let urlsArr;
793
+ try {
794
+ urlsArr = await this.#downloadListOfUrls({
795
+ url: requestsFromUrl,
796
+ urlRegExp: regex,
797
+ proxyUrl: (await this.#proxyConfiguration?.newProxyInfo())?.url,
798
+ });
799
+ }
800
+ catch (err) {
801
+ throw new Error(`Cannot fetch a request list from ${requestsFromUrl}: ${err}`);
802
+ }
803
+ // Skip if resource contained no URLs.
804
+ if (!urlsArr.length) {
805
+ this.log.warning('The fetched list contains no valid URLs.', { requestsFromUrl, regex });
806
+ return [];
807
+ }
808
+ return urlsArr.map((url) => ({ url, ...sharedOpts }));
809
+ }
810
+ /**
811
+ * Adds all fetched requests from a URL from a remote resource.
812
+ */
813
+ async #addFetchedRequests(source, fetchedRequests, options) {
814
+ const { requestsFromUrl, regex } = source;
815
+ const { addedRequests } = await this.addRequestsBatched(fetchedRequests, options);
816
+ this.log.info('Fetched and loaded Requests from a remote resource.', {
817
+ requestsFromUrl,
818
+ regex,
819
+ fetchedCount: fetchedRequests.length,
820
+ importedCount: addedRequests.length,
821
+ duplicateCount: fetchedRequests.length - addedRequests.length,
822
+ sample: JSON.stringify(fetchedRequests.slice(0, 5)),
823
+ });
824
+ return addedRequests;
825
+ }
826
+ /**
827
+ * @internal wraps public utility for mocking purposes
828
+ */
829
+ async #downloadListOfUrls(options) {
830
+ return downloadListOfUrls({
831
+ ...options,
832
+ httpClient: this.#httpClient,
833
+ });
834
+ }
835
+ /**
836
+ * Opens a request queue and returns a promise resolving to an instance
837
+ * of the {@link RequestQueue} class.
838
+ *
839
+ * {@link RequestQueue} represents a queue of URLs to crawl, which is stored either on local filesystem or in the cloud.
840
+ * The queue is used for deep crawling of websites, where you start with several URLs and then
841
+ * recursively follow links to other pages. The data structure supports both breadth-first
842
+ * and depth-first crawling orders.
843
+ *
844
+ * For more details and code examples, see the {@link RequestQueue} class.
845
+ *
846
+ * @param [identifier]
847
+ * ID or name of the request queue to be opened. If a string is provided, it will first be
848
+ * looked up as an ID; if no such storage exists, it will be treated as a name.
849
+ * If `null` or `undefined`, the function returns the default request queue associated with the crawler run.
850
+ * @param [options] Open Request Queue options.
851
+ */
852
+ static async open(identifier, options = {}) {
853
+ tryCancel();
854
+ const parsedOptions = parseArgument(options, openOptionsSchema);
855
+ const storageBackend = parsedOptions.storageBackend ?? serviceLocator.getStorageBackend();
856
+ const configuration = parsedOptions.configuration ?? serviceLocator.getConfiguration();
857
+ await purgeDefaultStorages({ onlyPurgeOnce: true, storageBackend, configuration });
858
+ const resolved = await resolveStorageIdentifier(identifier, storageBackend, 'RequestQueue');
859
+ const queue = await serviceLocator
860
+ .getStorageInstanceManager()
861
+ .openStorage(this, {
862
+ ...resolved,
863
+ backendOpener: () => storageBackend.createRequestQueueBackend(resolved),
864
+ backendCacheKey: storageBackend.getStorageBackendCacheKey?.() ?? storageBackend.constructor.name,
865
+ });
866
+ queue.#proxyConfiguration = parsedOptions.proxyConfiguration;
867
+ queue.#httpClient = parsedOptions.httpClient;
868
+ return queue;
869
+ }
870
+ }