@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
@@ -38,19 +38,20 @@ const addRequestsBatchedOptionsSchema = z.strictObject({
38
38
  waitBetweenBatchesMillis: schemas.anyNumber.default(1000),
39
39
  maxNewRequests: schemas.anyNumber.optional(),
40
40
  });
41
- const newRequestLikeSchema = z.looseObject({
41
+ // Compiled: these run once per request.
42
+ const newRequestLikeSchema = z.compile(z.looseObject({
42
43
  url: z.string(),
43
44
  id: z.undefined().optional(),
44
- });
45
- const handledRequestSchema = z.looseObject({
45
+ }));
46
+ const handledRequestSchema = z.compile(z.looseObject({
46
47
  id: z.string(),
47
48
  uniqueKey: z.string(),
48
49
  handledAt: z.string().optional(),
49
- });
50
- const reclaimedRequestSchema = z.looseObject({
50
+ }));
51
+ const reclaimedRequestSchema = z.compile(z.looseObject({
51
52
  id: z.string(),
52
53
  uniqueKey: z.string(),
53
- });
54
+ }));
54
55
  const uniqueKeySchema = z.string();
55
56
  const openOptionsSchema = z.strictObject({
56
57
  configuration: z.instanceof(Configuration).optional(),
@@ -98,8 +99,7 @@ export class RequestQueue {
98
99
  backend;
99
100
  #proxyConfiguration;
100
101
  log;
101
- // kept as TS-private: request_queue tests read this cache directly
102
- requestCache;
102
+ #requestCache;
103
103
  /**
104
104
  * Remembers the `requestId` of every request already submitted to the client — including background
105
105
  * batches that `requestCache` skips — so overlapping URL sets aren't re-submitted.
@@ -107,8 +107,7 @@ export class RequestQueue {
107
107
  */
108
108
  #requestSeenCache;
109
109
  #queuePausedForMigration = false;
110
- // kept as TS-private: packages/core/test request-queue tests write this counter directly
111
- inProgressRequestBatchCount = 0;
110
+ #inProgressRequestBatchCount = 0;
112
111
  /**
113
112
  * The largest expected request-processing time (in seconds) seen so far via
114
113
  * {@link setExpectedRequestProcessingTimeSecs}. Used to ensure that value is only ever raised, never
@@ -137,7 +136,7 @@ export class RequestQueue {
137
136
  this.#events = serviceLocator.getEventManager();
138
137
  this.backend = options.backend;
139
138
  this.#proxyConfiguration = options.proxyConfiguration;
140
- this.requestCache = new LruCache({ maxLength: MAX_CACHED_REQUESTS });
139
+ this.#requestCache = new LruCache({ maxLength: MAX_CACHED_REQUESTS });
141
140
  this.#requestSeenCache = new RequestDeduplicationCache();
142
141
  this.log = serviceLocator.getLogger().child({ prefix: `RequestQueue(${this.id}, ${this.name ?? 'no-name'})` });
143
142
  this.#events.on(EventType.MIGRATING, async () => {
@@ -181,20 +180,20 @@ export class RequestQueue {
181
180
  parseArgument(requestLike, schemas.anyObject);
182
181
  const { forefront } = parseArgument(options, operationOptionsSchema);
183
182
  if ('requestsFromUrl' in requestLike) {
184
- const requests = await this.fetchRequestsFromUrl(requestLike);
185
- const processedRequests = await this.addFetchedRequests(requestLike, requests, options);
183
+ const requests = await this.#fetchRequestsFromUrl(requestLike);
184
+ const processedRequests = await this.#addFetchedRequests(requestLike, requests, options);
186
185
  return { ...processedRequests[0], forefront };
187
186
  }
188
187
  parseArgument(requestLike, newRequestLikeSchema);
189
188
  const request = requestLike instanceof Request ? requestLike : new Request(requestLike);
190
189
  if (transaction?.policy.requestQueue === 'deferred') {
191
- return this.addRequestDeferred(transaction, request, forefront);
190
+ return this.#addRequestDeferred(transaction, request, forefront);
192
191
  }
193
192
  const cacheKey = getRequestId(request.uniqueKey);
194
- const cachedInfo = this.requestCache.get(cacheKey);
193
+ const cachedInfo = this.#requestCache.get(cacheKey);
195
194
  if (cachedInfo) {
196
195
  request.id = cachedInfo.id;
197
- this.recordRequestJournalEntry(transaction, [request], forefront, true);
196
+ this.#recordRequestJournalEntry(transaction, [request], forefront, true);
198
197
  return {
199
198
  wasAlreadyPresent: true,
200
199
  // We may assume that if request is in local cache then also the information if the
@@ -207,13 +206,13 @@ export class RequestQueue {
207
206
  }
208
207
  this.#statsTracker.add('writeCount');
209
208
  const { processedRequests } = await this.backend.addBatchOfRequests([request], { forefront });
210
- this.recordRequestJournalEntry(transaction, [request], forefront, true);
209
+ this.#recordRequestJournalEntry(transaction, [request], forefront, true);
211
210
  const queueOperationInfo = {
212
211
  ...processedRequests[0],
213
212
  uniqueKey: request.uniqueKey,
214
213
  forefront,
215
214
  };
216
- this.cacheRequest(cacheKey, queueOperationInfo);
215
+ this.#cacheRequest(cacheKey, queueOperationInfo);
217
216
  this.#requestSeenCache.add(cacheKey, request.id);
218
217
  return queueOperationInfo;
219
218
  }
@@ -221,7 +220,7 @@ export class RequestQueue {
221
220
  * Journals an addition for introspection only; these entries are never replayed. A no-op unless the
222
221
  * transaction is open, so detached and outliving writers stay out of the journal.
223
222
  */
224
- recordRequestJournalEntry(transaction, requests, forefront, writeThrough) {
223
+ #recordRequestJournalEntry(transaction, requests, forefront, writeThrough) {
225
224
  if (!transaction?.isActive || requests.length === 0)
226
225
  return;
227
226
  transaction.recordJournalEntry({
@@ -240,7 +239,7 @@ export class RequestQueue {
240
239
  * The requests buffered by the given transaction for this queue, keyed by `uniqueKey` — a dedup
241
240
  * index derived from the transaction journal.
242
241
  */
243
- bufferedRequests(transaction) {
242
+ #bufferedRequests(transaction) {
244
243
  const buffered = new Map();
245
244
  // Only `deferred` records snapshots, so scanning the journal under `writeThrough` never finds any.
246
245
  if (transaction.policy.requestQueue !== 'deferred')
@@ -260,10 +259,10 @@ export class RequestQueue {
260
259
  * A new request's `requestId` is the local `uniqueKey` hash and is **provisional** — never write it
261
260
  * to `request.id` or the dedup caches. Dedup is cheapest-first: buffer, caches, then a backend probe.
262
261
  */
263
- async addRequestDeferred(transaction, request, forefront, buffered = this.bufferedRequests(transaction)) {
262
+ async #addRequestDeferred(transaction, request, forefront, buffered = this.#bufferedRequests(transaction)) {
264
263
  // This transaction's own buffered adds; the shared caches never see them (provisional ids).
265
264
  if (buffered.has(request.uniqueKey)) {
266
- this.recordRequestJournalEntry(transaction, [request], forefront, false);
265
+ this.#recordRequestJournalEntry(transaction, [request], forefront, false);
267
266
  return {
268
267
  wasAlreadyPresent: true,
269
268
  wasAlreadyHandled: false,
@@ -275,10 +274,10 @@ export class RequestQueue {
275
274
  // The caches hold real backend ids. Only *writing* provisional ids to them would be wrong;
276
275
  // reading saves a probe. Same lookup as the write-through path.
277
276
  const cacheKey = getRequestId(request.uniqueKey);
278
- const cachedInfo = this.requestCache.get(cacheKey);
277
+ const cachedInfo = this.#requestCache.get(cacheKey);
279
278
  const knownRequestId = cachedInfo?.id ?? this.#requestSeenCache.get(cacheKey);
280
279
  if (knownRequestId) {
281
- this.recordRequestJournalEntry(transaction, [request], forefront, false);
280
+ this.#recordRequestJournalEntry(transaction, [request], forefront, false);
282
281
  return {
283
282
  wasAlreadyPresent: true,
284
283
  // The dedup cache doesn't track the handled state; only the full record does.
@@ -291,7 +290,7 @@ export class RequestQueue {
291
290
  // The caches are bounded, so a miss is not proof of absence - probe for an accurate answer.
292
291
  const existing = await this.backend.getRequest(request.uniqueKey);
293
292
  if (existing) {
294
- this.recordRequestJournalEntry(transaction, [request], forefront, false);
293
+ this.#recordRequestJournalEntry(transaction, [request], forefront, false);
295
294
  return {
296
295
  wasAlreadyPresent: true,
297
296
  wasAlreadyHandled: existing.handledAt != null,
@@ -338,7 +337,7 @@ export class RequestQueue {
338
337
  ? // Requests without a snapshot were deduplicated or written through; nothing to replay.
339
338
  entry.requests
340
339
  .filter((journaled) => journaled.snapshot !== undefined)
341
- .map((journaled) => new Request(journaled.snapshot))
340
+ .map((journaled) => Request.fromSchema(journaled.snapshot))
342
341
  : []);
343
342
  if (requests.length === 0)
344
343
  continue;
@@ -349,7 +348,7 @@ export class RequestQueue {
349
348
  // Only now, with the real backend-assigned ids, may the shared dedup caches be populated.
350
349
  for (const processed of processedRequests) {
351
350
  const cacheKey = getRequestId(processed.uniqueKey);
352
- this.cacheRequest(cacheKey, { ...processed, forefront });
351
+ this.#cacheRequest(cacheKey, { ...processed, forefront });
353
352
  this.#requestSeenCache.add(cacheKey, processed.requestId);
354
353
  }
355
354
  if (unprocessedRequests.length > 0) {
@@ -399,26 +398,26 @@ export class RequestQueue {
399
398
  requests.push(new Request({ url: requestLike }));
400
399
  }
401
400
  else if ('requestsFromUrl' in requestLike) {
402
- const fetchedRequests = await this.fetchRequestsFromUrl(requestLike);
403
- await this.addFetchedRequests(requestLike, fetchedRequests, options);
401
+ const fetchedRequests = await this.#fetchRequestsFromUrl(requestLike);
402
+ await this.#addFetchedRequests(requestLike, fetchedRequests, options);
404
403
  }
405
404
  else {
406
405
  requests.push(requestLike instanceof Request ? requestLike : new Request(requestLike));
407
406
  }
408
407
  }
409
408
  if (transaction?.policy.requestQueue === 'deferred') {
410
- const buffered = this.bufferedRequests(transaction);
409
+ const buffered = this.#bufferedRequests(transaction);
411
410
  for (const request of requests) {
412
- results.processedRequests.push(await this.addRequestDeferred(transaction, request, forefront, buffered));
411
+ results.processedRequests.push(await this.#addRequestDeferred(transaction, request, forefront, buffered));
413
412
  }
414
413
  return results;
415
414
  }
416
- this.recordRequestJournalEntry(transaction, requests, forefront, true);
415
+ this.#recordRequestJournalEntry(transaction, requests, forefront, true);
417
416
  const requestsToAdd = new Map();
418
417
  for (const request of requests) {
419
418
  const cacheKey = getCachedRequestId(request.uniqueKey);
420
419
  // Prefer the full `requestCache` record; fall back to the dedup cache for background batches it skips.
421
- const cachedInfo = this.requestCache.get(cacheKey);
420
+ const cachedInfo = this.#requestCache.get(cacheKey);
422
421
  const knownRequestId = cachedInfo?.id ?? this.#requestSeenCache.get(cacheKey);
423
422
  if (knownRequestId) {
424
423
  request.id = knownRequestId;
@@ -448,7 +447,7 @@ export class RequestQueue {
448
447
  results.processedRequests.push(newRequest);
449
448
  const cacheKey = getCachedRequestId(newRequest.uniqueKey);
450
449
  if (cache) {
451
- this.cacheRequest(cacheKey, { ...newRequest, forefront });
450
+ this.#cacheRequest(cacheKey, { ...newRequest, forefront });
452
451
  }
453
452
  // Unlike `requestCache`, populate this on every batch (including background ones).
454
453
  this.#requestSeenCache.add(cacheKey, newRequest.requestId);
@@ -475,7 +474,7 @@ export class RequestQueue {
475
474
  if (opts.url !== undefined && typeof opts.url !== 'string') {
476
475
  throw new Error(`Request options are not valid, the 'url' property is not a string. Input: ${inspect(opts)}`);
477
476
  }
478
- if (opts.id !== undefined) {
477
+ if ('id' in opts && opts.id !== undefined) {
479
478
  throw new Error(`Request options are not valid, the 'id' property must not be present. Input: ${inspect(opts)}`);
480
479
  }
481
480
  if (opts.requestsFromUrl !== undefined &&
@@ -517,9 +516,9 @@ export class RequestQueue {
517
516
  return processedRequests;
518
517
  },
519
518
  trackBackgroundBatches: (batches) => {
520
- this.inProgressRequestBatchCount += 1;
519
+ this.#inProgressRequestBatchCount += 1;
521
520
  void batches.finally(() => {
522
- this.inProgressRequestBatchCount -= 1;
521
+ this.#inProgressRequestBatchCount -= 1;
523
522
  });
524
523
  },
525
524
  });
@@ -534,14 +533,14 @@ export class RequestQueue {
534
533
  const transaction = activeStorageTransaction();
535
534
  parseArgument(uniqueKey, uniqueKeySchema);
536
535
  // Requests buffered by the active transaction (under the `deferred` write policy) are visible to it.
537
- const buffered = transaction && this.bufferedRequests(transaction).get(uniqueKey);
536
+ const buffered = transaction && this.#bufferedRequests(transaction).get(uniqueKey);
538
537
  if (buffered) {
539
- return new Request(buffered);
538
+ return Request.fromSchema(buffered);
540
539
  }
541
- const requestOptions = await this.backend.getRequest(uniqueKey);
542
- if (!requestOptions)
540
+ const schema = await this.backend.getRequest(uniqueKey);
541
+ if (!schema)
543
542
  return null;
544
- return new Request(requestOptions);
543
+ return Request.fromSchema(schema);
545
544
  }
546
545
  /**
547
546
  * Returns a next request in the queue to be processed, or `null` if there are no more pending requests.
@@ -555,7 +554,7 @@ export class RequestQueue {
555
554
  * Note that the `null` return value doesn't mean the queue processing finished,
556
555
  * it means there are currently no pending requests.
557
556
  * To check whether all requests in queue were finished,
558
- * use {@link RequestQueue.isFinished} instead.
557
+ * use {@link RequestQueue.checkReadiness} instead.
559
558
  *
560
559
  * @returns
561
560
  * Returns the request object or `null` if there are no more pending requests.
@@ -566,10 +565,10 @@ export class RequestQueue {
566
565
  return null;
567
566
  }
568
567
  this.#statsTracker.add('headItemReadCount');
569
- const requestOptions = await this.backend.fetchNextRequest();
570
- if (!requestOptions)
568
+ const schema = await this.backend.fetchNextRequest();
569
+ if (!schema)
571
570
  return null;
572
- return new Request(requestOptions);
571
+ return Request.fromSchema(schema);
573
572
  }
574
573
  /**
575
574
  * Marks a request that was previously returned by the
@@ -580,7 +579,7 @@ export class RequestQueue {
580
579
  async markRequestAsHandled(request) {
581
580
  rejectOperationInTransaction('RequestQueue.markRequestAsHandled()', 'it is part of the crawler request-processing bookkeeping, which a transaction must not affect.');
582
581
  parseArgument(request, handledRequestSchema);
583
- const forefront = this.requestCache.get(getRequestId(request.uniqueKey))?.forefront ?? false;
582
+ const forefront = this.#requestCache.get(getRequestId(request.uniqueKey))?.forefront ?? false;
584
583
  const handledAt = request.handledAt ?? new Date().toISOString();
585
584
  this.#statsTracker.add('writeCount');
586
585
  const processedRequest = await this.backend.markRequestAsHandled({
@@ -597,7 +596,7 @@ export class RequestQueue {
597
596
  uniqueKey: request.uniqueKey,
598
597
  forefront,
599
598
  };
600
- this.cacheRequest(getRequestId(request.uniqueKey), queueOperationInfo);
599
+ this.#cacheRequest(getRequestId(request.uniqueKey), queueOperationInfo);
601
600
  return queueOperationInfo;
602
601
  }
603
602
  /**
@@ -623,46 +622,41 @@ export class RequestQueue {
623
622
  uniqueKey: request.uniqueKey,
624
623
  forefront,
625
624
  };
626
- this.cacheRequest(getRequestId(request.uniqueKey), queueOperationInfo);
625
+ this.#cacheRequest(getRequestId(request.uniqueKey), queueOperationInfo);
627
626
  return queueOperationInfo;
628
627
  }
629
628
  /**
630
- * Resolves to `true` if the next call to {@link RequestQueue.fetchNextRequest} would return
631
- * `null`, i.e. there are no pending requests to fetch right now. Otherwise it resolves to `false`.
632
- *
633
- * Note that even if the queue is empty, there might be some requests currently being processed
634
- * (fetched but not yet handled or reclaimed). An empty queue therefore does not mean crawling is
635
- * finished — those in-progress requests may still be reclaimed, and background tasks may still be
636
- * adding more requests. To check whether all activity in the queue has finished, use
637
- * {@link RequestQueue.isFinished}.
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
638
632
  */
639
- async isEmpty() {
640
- const transaction = activeStorageTransaction();
641
- // Requests buffered by the active transaction count as pending from its point of view.
642
- if (transaction && this.bufferedRequests(transaction).size > 0) {
643
- return false;
644
- }
645
- return this.backend.isEmpty();
633
+ recordPacingSignal(_signal) {
634
+ return false;
646
635
  }
647
636
  /**
648
- * Resolves to `true` if all requests were already handled and there are no more left — including no
649
- * requests currently in progress (fetched but not yet handled or reclaimed, including requests
650
- * locked by other clients sharing the same queue) and no background add operations still in flight.
637
+ * Reports whether the queue has a request to hand over, is waiting on one, or is done.
651
638
  *
652
- * Due to the nature of distributed storage used by the queue, the function may occasionally return
653
- * a false negative, but it shall never return a false positive.
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.
654
644
  */
655
- async isFinished() {
645
+ async checkReadiness() {
656
646
  const transaction = activeStorageTransaction();
657
- // We are not finished if we're still adding new requests in the background.
658
- if (this.inProgressRequestBatchCount > 0) {
659
- return false;
660
- }
661
647
  // Requests buffered by the active transaction count as pending from its point of view.
662
- if (transaction && this.bufferedRequests(transaction).size > 0) {
663
- return false;
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' };
664
654
  }
665
- return this.backend.isFinished();
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' };
666
660
  }
667
661
  /**
668
662
  * Tells the queue how long a consumer expects to hold a fetched request before marking it handled
@@ -681,18 +675,28 @@ export class RequestQueue {
681
675
  this.#expectedRequestProcessingSecs = secs;
682
676
  await this.backend.setExpectedRequestProcessingTimeSecs?.(secs);
683
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
+ }
684
689
  /**
685
690
  * Caches information about request to beware of unneeded addRequest() calls.
686
691
  */
687
- cacheRequest(cacheKey, queueOperationInfo) {
692
+ #cacheRequest(cacheKey, queueOperationInfo) {
688
693
  // Remove the previous entry, as otherwise our cache will never update 👀
689
- this.requestCache.remove(cacheKey);
690
- this.requestCache.add(cacheKey, {
694
+ this.#requestCache.remove(cacheKey);
695
+ this.#requestCache.add(cacheKey, {
691
696
  id: queueOperationInfo.requestId,
692
697
  isHandled: queueOperationInfo.wasAlreadyHandled,
693
698
  uniqueKey: queueOperationInfo.uniqueKey,
694
699
  hydrated: null,
695
- lockExpiresAt: null,
696
700
  forefront: queueOperationInfo.forefront,
697
701
  });
698
702
  }
@@ -713,9 +717,9 @@ export class RequestQueue {
713
717
  rejectOperationInTransaction('RequestQueue.purge()');
714
718
  await this.backend.purge();
715
719
  // Reset in-memory bookkeeping so the queue behaves as if freshly opened.
716
- this.requestCache.clear();
720
+ this.#requestCache.clear();
717
721
  this.#requestSeenCache.clear();
718
- this.inProgressRequestBatchCount = 0;
722
+ this.#inProgressRequestBatchCount = 0;
719
723
  // Reset the expected-processing-time high-water mark too, otherwise the monotonic-raise guard
720
724
  // in `setExpectedRequestProcessingTimeSecs` would let a value raised in an earlier run leak into a
721
725
  // later one and silently swallow a lower hint (the queue is meant to be reusable across runs).
@@ -769,7 +773,7 @@ export class RequestQueue {
769
773
  async getInfo() {
770
774
  const transaction = activeStorageTransaction();
771
775
  const metadata = await this.backend.getMetadata();
772
- const bufferedCount = transaction ? this.bufferedRequests(transaction).size : 0;
776
+ const bufferedCount = transaction ? this.#bufferedRequests(transaction).size : 0;
773
777
  if (bufferedCount > 0) {
774
778
  return {
775
779
  ...metadata,
@@ -782,12 +786,12 @@ export class RequestQueue {
782
786
  /**
783
787
  * Fetches URLs from requestsFromUrl and returns them in format of list of requests
784
788
  */
785
- async fetchRequestsFromUrl(source) {
789
+ async #fetchRequestsFromUrl(source) {
786
790
  const { requestsFromUrl, regex, ...sharedOpts } = source;
787
791
  // Download remote resource and parse URLs.
788
792
  let urlsArr;
789
793
  try {
790
- urlsArr = await this.downloadListOfUrls({
794
+ urlsArr = await this.#downloadListOfUrls({
791
795
  url: requestsFromUrl,
792
796
  urlRegExp: regex,
793
797
  proxyUrl: (await this.#proxyConfiguration?.newProxyInfo())?.url,
@@ -806,7 +810,7 @@ export class RequestQueue {
806
810
  /**
807
811
  * Adds all fetched requests from a URL from a remote resource.
808
812
  */
809
- async addFetchedRequests(source, fetchedRequests, options) {
813
+ async #addFetchedRequests(source, fetchedRequests, options) {
810
814
  const { requestsFromUrl, regex } = source;
811
815
  const { addedRequests } = await this.addRequestsBatched(fetchedRequests, options);
812
816
  this.log.info('Fetched and loaded Requests from a remote resource.', {
@@ -822,7 +826,7 @@ export class RequestQueue {
822
826
  /**
823
827
  * @internal wraps public utility for mocking purposes
824
828
  */
825
- async downloadListOfUrls(options) {
829
+ async #downloadListOfUrls(options) {
826
830
  return downloadListOfUrls({
827
831
  ...options,
828
832
  httpClient: this.#httpClient,
@@ -7,7 +7,6 @@ export interface IStorage {
7
7
  id: string;
8
8
  name?: string;
9
9
  }
10
- type Hashable = string;
11
10
  /**
12
11
  * Unified manager for opening and caching storage instances (Dataset, KeyValueStore, RequestQueue).
13
12
  *
@@ -35,7 +34,7 @@ export declare class StorageInstanceManager {
35
34
  */
36
35
  openStorage<TStorage extends IStorage>(cls: Constructor<TStorage>, { id, name, alias, backendOpener, backendCacheKey, }: (ExplicitStorageIdentifier | DefaultStorageIdentifier) & {
37
36
  backendOpener: () => Promise<DatasetBackend | KeyValueStoreBackend | RequestQueueBackend>;
38
- backendCacheKey: Hashable;
37
+ backendCacheKey: string;
39
38
  }): Promise<TStorage>;
40
39
  /**
41
40
  * Remove a storage instance from the cache (called from `storage.drop()`).
@@ -32,7 +32,7 @@ class StorageCache {
32
32
  return undefined;
33
33
  }
34
34
  /** Write a single entry into a given tier. */
35
- setInMap(tier, cls, key, instance, backendCacheKey) {
35
+ #setInMap(tier, cls, key, instance, backendCacheKey) {
36
36
  if (!tier.has(cls))
37
37
  tier.set(cls, new Map());
38
38
  const keyMap = tier.get(cls);
@@ -45,14 +45,14 @@ class StorageCache {
45
45
  */
46
46
  set(cls, instance, backendCacheKey, alias) {
47
47
  // Always cache by id.
48
- this.setInMap(this.byId, cls, instance.id, instance, backendCacheKey);
48
+ this.#setInMap(this.byId, cls, instance.id, instance, backendCacheKey);
49
49
  // Cache by name — only for named storages.
50
50
  if (instance.name) {
51
- this.setInMap(this.byName, cls, instance.name, instance, backendCacheKey);
51
+ this.#setInMap(this.byName, cls, instance.name, instance, backendCacheKey);
52
52
  }
53
53
  // Cache by alias — only for unnamed storages opened via alias.
54
54
  if (alias !== undefined) {
55
- this.setInMap(this.byAlias, cls, alias, instance, backendCacheKey);
55
+ this.#setInMap(this.byAlias, cls, alias, instance, backendCacheKey);
56
56
  }
57
57
  }
58
58
  removeFromCache(instance) {
@@ -1,4 +1,4 @@
1
- import type { Awaitable, Dictionary } from '@crawlee/types';
1
+ import type { Awaitable, Dictionary, RequestSchema } from '@crawlee/types';
2
2
  import type { RecordOptions } from './key_value_store.js';
3
3
  /**
4
4
  * Governs whether writes of a given storage type performed inside a {@link StorageTransaction} are
@@ -38,6 +38,7 @@ export interface TransactionParticipant {
38
38
  }
39
39
  /**
40
40
  * A single dataset write (`pushData`) recorded in a transaction journal.
41
+ * @internal
41
42
  */
42
43
  export interface DatasetJournalEntry {
43
44
  type: 'dataset';
@@ -50,6 +51,7 @@ export interface DatasetJournalEntry {
50
51
  }
51
52
  /**
52
53
  * A single key-value store write (`setValue`) recorded in a transaction journal.
54
+ * @internal
53
55
  */
54
56
  export interface KeyValueStoreJournalEntry {
55
57
  type: 'keyValueStore';
@@ -63,6 +65,7 @@ export interface KeyValueStoreJournalEntry {
63
65
  }
64
66
  /**
65
67
  * A request recorded in a transaction journal.
68
+ * @internal
66
69
  */
67
70
  export interface JournaledRequest {
68
71
  url: string;
@@ -72,10 +75,11 @@ export interface JournaledRequest {
72
75
  * A full JSON snapshot of the request for the commit replay. Only present for buffered additions —
73
76
  * deduplicated and write-through ones are journaled for introspection only.
74
77
  */
75
- snapshot?: Dictionary;
78
+ snapshot?: RequestSchema;
76
79
  }
77
80
  /**
78
81
  * A batch of request queue additions recorded in a transaction journal.
82
+ * @internal
79
83
  */
80
84
  export interface RequestQueueJournalEntry {
81
85
  type: 'requestQueue';
@@ -86,6 +90,7 @@ export interface RequestQueueJournalEntry {
86
90
  /** Write-through entries were applied immediately; they are never replayed. */
87
91
  writeThrough: boolean;
88
92
  }
93
+ /** @internal */
89
94
  export type JournalEntry = DatasetJournalEntry | KeyValueStoreJournalEntry | RequestQueueJournalEntry;
90
95
  /**
91
96
  * A read-only view of a {@link StorageTransaction}: only the journal-backed introspection accessors,
@@ -143,12 +148,18 @@ export interface StorageTransactionOptions {
143
148
  */
144
149
  export declare class StorageTransaction implements StorageTransactionView {
145
150
  #private;
146
- /** The ordered, append-only journal — the source of truth for commit, introspection and reads. */
151
+ /**
152
+ * The ordered, append-only journal — the source of truth for commit, introspection and reads.
153
+ * @internal
154
+ */
147
155
  readonly journal: JournalEntry[];
148
- /** Per-storage-type write policy. */
149
- readonly policy: StorageWritePolicy;
150
156
  /** @internal */
151
157
  constructor(options?: StorageTransactionOptions);
158
+ /**
159
+ * Per-storage-type write policy.
160
+ * @internal
161
+ */
162
+ get policy(): StorageWritePolicy;
152
163
  get state(): StorageTransactionState;
153
164
  /**
154
165
  * `true` only while `state === 'open'`. This is the single predicate every storage operation
@@ -162,6 +173,13 @@ export declare class StorageTransaction implements StorageTransactionView {
162
173
  * @internal
163
174
  */
164
175
  recordJournalEntry(entry: JournalEntry): void;
176
+ /**
177
+ * Registers a callback to run once a commit has been attempted, whether it succeeded (`error` is
178
+ * `undefined`) or failed - the only point at which a deferred write can be reacted to where it was
179
+ * made. Callbacks run in registration order; the first to throw propagates, the rest do not run, and
180
+ * on a failed commit its error replaces the commit error. Not run on rollback: nothing was written.
181
+ */
182
+ afterCommit(callback: (error?: Error) => Awaitable<void>): void;
165
183
  /**
166
184
  * Replays the journaled writes into real storage. A no-op unless the transaction is `open`.
167
185
  *
@@ -171,7 +189,6 @@ export declare class StorageTransaction implements StorageTransactionView {
171
189
  * partway may have applied some of the writes already.
172
190
  */
173
191
  commit(): Promise<void>;
174
- private flush;
175
192
  /**
176
193
  * Discards the journaled writes. A no-op unless the transaction is `open` — in particular, calling it
177
194
  * after a successful `commit()` (which the crawler's error handling can legitimately do) does nothing
@@ -179,9 +196,10 @@ export declare class StorageTransaction implements StorageTransactionView {
179
196
  */
180
197
  rollback(): void;
181
198
  /**
182
- * Releases the journal and the write-time snapshots it holds. Must be called for *every* terminal
183
- * state, `failed` included. Idempotent, never throws, and does not change `state`. Any
184
- * {@link StorageTransactionView} of this transaction is only valid until this is called.
199
+ * Releases the journal, the write-time snapshots it holds and any registered commit callbacks. Must
200
+ * be called for *every* terminal state, `failed` included. Idempotent, never throws, and does not
201
+ * change `state`. Any {@link StorageTransactionView} of this transaction is only valid until this
202
+ * is called.
185
203
  */
186
204
  dispose(): void;
187
205
  get datasetItems(): {