@crawlee/core 4.0.0-beta.104 → 4.0.0-beta.106

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 (83) hide show
  1. package/autoscaling/autoscaled_pool.d.ts +3 -21
  2. package/autoscaling/autoscaled_pool.js +85 -85
  3. package/autoscaling/client_load_signal.d.ts +1 -5
  4. package/autoscaling/client_load_signal.js +20 -20
  5. package/autoscaling/concurrency_system.d.ts +5 -20
  6. package/autoscaling/concurrency_system.js +81 -80
  7. package/autoscaling/cpu_load_signal.d.ts +1 -2
  8. package/autoscaling/cpu_load_signal.js +10 -10
  9. package/autoscaling/event_loop_load_signal.d.ts +1 -4
  10. package/autoscaling/event_loop_load_signal.js +18 -18
  11. package/autoscaling/load_signal.d.ts +1 -1
  12. package/autoscaling/load_signal.js +12 -11
  13. package/autoscaling/memory_load_signal.d.ts +3 -12
  14. package/autoscaling/memory_load_signal.js +40 -41
  15. package/autoscaling/snapshotter.d.ts +1 -4
  16. package/autoscaling/snapshotter.js +12 -12
  17. package/autoscaling/system_status.d.ts +1 -3
  18. package/autoscaling/system_status.js +11 -11
  19. package/configuration.d.ts +1 -1
  20. package/configuration.js +3 -3
  21. package/crawlers/context_pipeline.js +6 -6
  22. package/crawlers/crawler_commons.d.ts +6 -56
  23. package/crawlers/crawler_commons.js +1 -107
  24. package/crawlers/index.d.ts +1 -1
  25. package/crawlers/index.js +0 -1
  26. package/crawlers/statistics.d.ts +1 -8
  27. package/crawlers/statistics.js +45 -44
  28. package/events/event_manager.d.ts +1 -1
  29. package/events/event_manager.js +3 -3
  30. package/events/local_event_manager.d.ts +1 -1
  31. package/events/local_event_manager.js +3 -3
  32. package/log.js +5 -1
  33. package/memory-storage/memory-storage.d.ts +1 -5
  34. package/memory-storage/memory-storage.js +2 -2
  35. package/memory-storage/resource-clients/dataset.d.ts +1 -1
  36. package/memory-storage/resource-clients/dataset.js +6 -5
  37. package/memory-storage/resource-clients/key-value-store.d.ts +1 -1
  38. package/memory-storage/resource-clients/key-value-store.js +13 -12
  39. package/memory-storage/resource-clients/request-queue.d.ts +4 -23
  40. package/memory-storage/resource-clients/request-queue.js +59 -58
  41. package/owned_or_injected.d.ts +1 -3
  42. package/owned_or_injected.js +17 -17
  43. package/package.json +5 -5
  44. package/proxy_configuration.d.ts +1 -3
  45. package/proxy_configuration.js +8 -8
  46. package/recoverable_state.d.ts +1 -10
  47. package/recoverable_state.js +41 -41
  48. package/request.d.ts +1 -2
  49. package/request.js +10 -13
  50. package/router.d.ts +1 -4
  51. package/router.js +23 -23
  52. package/serialization.js +8 -9
  53. package/service_locator.d.ts +1 -10
  54. package/service_locator.js +48 -48
  55. package/session_pool/session.d.ts +1 -12
  56. package/session_pool/session.js +50 -50
  57. package/session_pool/session_pool.d.ts +2 -11
  58. package/session_pool/session_pool.js +59 -58
  59. package/storages/dataset.d.ts +12 -1
  60. package/storages/dataset.js +121 -22
  61. package/storages/index.d.ts +1 -1
  62. package/storages/index.js +1 -1
  63. package/storages/key_value_store.d.ts +19 -4
  64. package/storages/key_value_store.js +174 -48
  65. package/storages/request_dedup_cache.d.ts +1 -2
  66. package/storages/request_dedup_cache.js +9 -9
  67. package/storages/request_list.d.ts +2 -22
  68. package/storages/request_list.js +74 -73
  69. package/storages/request_manager_tandem.d.ts +1 -10
  70. package/storages/request_manager_tandem.js +27 -27
  71. package/storages/request_queue.d.ts +21 -18
  72. package/storages/request_queue.js +256 -53
  73. package/storages/sitemap_request_loader.d.ts +1 -44
  74. package/storages/sitemap_request_loader.js +87 -87
  75. package/storages/storage_instance_manager.d.ts +1 -2
  76. package/storages/storage_instance_manager.js +17 -17
  77. package/storages/storage_stats.d.ts +1 -1
  78. package/storages/storage_stats.js +4 -4
  79. package/storages/transaction.d.ts +252 -0
  80. package/storages/transaction.js +251 -0
  81. package/system-info/runtime.js +7 -7
  82. package/storages/access_checking.d.ts +0 -12
  83. package/storages/access_checking.js +0 -17
@@ -2,12 +2,13 @@ import { inspect } from 'node:util';
2
2
  import { downloadListOfUrls, isAsyncIterable, isIterable, sleep } from '@crawlee/utils';
3
3
  import ow from 'ow';
4
4
  import { LruCache } from '@apify/datastructures';
5
+ import { tryCancel } from '@apify/timeout';
5
6
  import { Configuration } from '../configuration.js';
6
7
  import { getObjectType } from '../debug.js';
7
8
  import { chunkedAsyncIterable, peekableAsyncIterable } from '../iterables.js';
8
9
  import { Request } from '../request.js';
9
10
  import { serviceLocator } from '../service_locator.js';
10
- import { checkStorageAccess } from './access_checking.js';
11
+ import { activeStorageTransaction, rejectOperationInTransaction, withDirectStorageAccess } from './transaction.js';
11
12
  import { StorageStatsTracker } from './storage_stats.js';
12
13
  import { resolveStorageIdentifier } from './storage_instance_manager.js';
13
14
  import { getRequestId, purgeDefaultStorages } from './utils.js';
@@ -55,26 +56,28 @@ export class RequestQueue {
55
56
  id;
56
57
  name;
57
58
  backend;
58
- proxyConfiguration;
59
+ #proxyConfiguration;
59
60
  log;
61
+ // kept as TS-private: request_queue tests read this cache directly
60
62
  requestCache;
61
63
  /**
62
64
  * Remembers the `requestId` of every request already submitted to the client — including background
63
65
  * batches that `requestCache` skips — so overlapping URL sets aren't re-submitted.
64
66
  * See {@link RequestDeduplicationCache} for why this is a separate, cheaper cache.
65
67
  */
66
- requestSeenCache;
67
- queuePausedForMigration = false;
68
+ #requestSeenCache;
69
+ #queuePausedForMigration = false;
70
+ // kept as TS-private: packages/core/test request-queue tests write this counter directly
68
71
  inProgressRequestBatchCount = 0;
69
72
  /**
70
73
  * The largest expected request-processing time (in seconds) seen so far via
71
74
  * {@link setExpectedRequestProcessingTimeSecs}. Used to ensure that value is only ever raised, never
72
75
  * lowered, before being forwarded to the storage backend.
73
76
  */
74
- expectedRequestProcessingSecs = 0;
75
- httpClient;
76
- events;
77
- statsTracker = new StorageStatsTracker({
77
+ #expectedRequestProcessingSecs = 0;
78
+ #httpClient;
79
+ #events;
80
+ #statsTracker = new StorageStatsTracker({
78
81
  writeCount: 0,
79
82
  headItemReadCount: 0,
80
83
  });
@@ -83,7 +86,7 @@ export class RequestQueue {
83
86
  * queue-head reads issued to the underlying storage backend). Counted per backend call.
84
87
  */
85
88
  get stats() {
86
- return this.statsTracker.current;
89
+ return this.#statsTracker.current;
87
90
  }
88
91
  /**
89
92
  * @internal
@@ -91,14 +94,14 @@ export class RequestQueue {
91
94
  constructor(options) {
92
95
  this.id = options.metadata.id;
93
96
  this.name = options.metadata.name;
94
- this.events = serviceLocator.getEventManager();
97
+ this.#events = serviceLocator.getEventManager();
95
98
  this.backend = options.backend;
96
- this.proxyConfiguration = options.proxyConfiguration;
99
+ this.#proxyConfiguration = options.proxyConfiguration;
97
100
  this.requestCache = new LruCache({ maxLength: MAX_CACHED_REQUESTS });
98
- this.requestSeenCache = new RequestDeduplicationCache();
101
+ this.#requestSeenCache = new RequestDeduplicationCache();
99
102
  this.log = serviceLocator.getLogger().child({ prefix: `RequestQueue(${this.id}, ${this.name ?? 'no-name'})` });
100
- this.events.on("migrating" /* EventType.MIGRATING */, async () => {
101
- this.queuePausedForMigration = true;
103
+ this.#events.on("migrating" /* EventType.MIGRATING */, async () => {
104
+ this.#queuePausedForMigration = true;
102
105
  });
103
106
  }
104
107
  /**
@@ -134,7 +137,7 @@ export class RequestQueue {
134
137
  * @param [options] Request queue operation options.
135
138
  */
136
139
  async addRequest(requestLike, options = {}) {
137
- checkStorageAccess();
140
+ const transaction = activeStorageTransaction();
138
141
  ow(requestLike, ow.object);
139
142
  ow(options, ow.object.exactShape({
140
143
  forefront: ow.optional.boolean,
@@ -150,10 +153,14 @@ export class RequestQueue {
150
153
  id: ow.undefined,
151
154
  }));
152
155
  const request = requestLike instanceof Request ? requestLike : new Request(requestLike);
156
+ if (transaction?.policy.requestQueue === 'deferred') {
157
+ return this.addRequestDeferred(transaction, request, forefront);
158
+ }
153
159
  const cacheKey = getRequestId(request.uniqueKey);
154
160
  const cachedInfo = this.requestCache.get(cacheKey);
155
161
  if (cachedInfo) {
156
162
  request.id = cachedInfo.id;
163
+ this.recordRequestJournalEntry(transaction, [request], forefront, true);
157
164
  return {
158
165
  wasAlreadyPresent: true,
159
166
  // We may assume that if request is in local cache then also the information if the
@@ -164,17 +171,163 @@ export class RequestQueue {
164
171
  forefront,
165
172
  };
166
173
  }
167
- this.statsTracker.add('writeCount');
174
+ this.#statsTracker.add('writeCount');
168
175
  const { processedRequests } = await this.backend.addBatchOfRequests([request], { forefront });
176
+ this.recordRequestJournalEntry(transaction, [request], forefront, true);
169
177
  const queueOperationInfo = {
170
178
  ...processedRequests[0],
171
179
  uniqueKey: request.uniqueKey,
172
180
  forefront,
173
181
  };
174
182
  this.cacheRequest(cacheKey, queueOperationInfo);
175
- this.requestSeenCache.add(cacheKey, request.id);
183
+ this.#requestSeenCache.add(cacheKey, request.id);
176
184
  return queueOperationInfo;
177
185
  }
186
+ /**
187
+ * Journals an addition for introspection only; these entries are never replayed. A no-op unless the
188
+ * transaction is open, so detached and outliving writers stay out of the journal.
189
+ */
190
+ recordRequestJournalEntry(transaction, requests, forefront, writeThrough) {
191
+ if (!transaction?.isActive || requests.length === 0)
192
+ return;
193
+ transaction.recordJournalEntry({
194
+ type: 'requestQueue',
195
+ participant: this,
196
+ requests: requests.map((request) => ({
197
+ url: request.url,
198
+ uniqueKey: request.uniqueKey,
199
+ label: request.label,
200
+ })),
201
+ forefront,
202
+ writeThrough,
203
+ });
204
+ }
205
+ /**
206
+ * The requests buffered by the given transaction for this queue, keyed by `uniqueKey` — a dedup
207
+ * index derived from the transaction journal.
208
+ */
209
+ bufferedRequests(transaction) {
210
+ const buffered = new Map();
211
+ // Only `deferred` records snapshots, so scanning the journal under `writeThrough` never finds any.
212
+ if (transaction.policy.requestQueue !== 'deferred')
213
+ return buffered;
214
+ for (const entry of transaction.journal) {
215
+ if (entry.type !== 'requestQueue' || entry.participant !== this)
216
+ continue;
217
+ for (const request of entry.requests) {
218
+ if (request.snapshot !== undefined)
219
+ buffered.set(request.uniqueKey, request.snapshot);
220
+ }
221
+ }
222
+ return buffered;
223
+ }
224
+ /**
225
+ * Adds a request under the `deferred` policy: journaled now, really added by the commit replay.
226
+ * A new request's `requestId` is the local `uniqueKey` hash and is **provisional** — never write it
227
+ * to `request.id` or the dedup caches. Dedup is cheapest-first: buffer, caches, then a backend probe.
228
+ */
229
+ async addRequestDeferred(transaction, request, forefront, buffered = this.bufferedRequests(transaction)) {
230
+ // This transaction's own buffered adds; the shared caches never see them (provisional ids).
231
+ if (buffered.has(request.uniqueKey)) {
232
+ this.recordRequestJournalEntry(transaction, [request], forefront, false);
233
+ return {
234
+ wasAlreadyPresent: true,
235
+ wasAlreadyHandled: false,
236
+ requestId: getRequestId(request.uniqueKey),
237
+ uniqueKey: request.uniqueKey,
238
+ forefront,
239
+ };
240
+ }
241
+ // The caches hold real backend ids. Only *writing* provisional ids to them would be wrong;
242
+ // reading saves a probe. Same lookup as the write-through path.
243
+ const cacheKey = getRequestId(request.uniqueKey);
244
+ const cachedInfo = this.requestCache.get(cacheKey);
245
+ const knownRequestId = cachedInfo?.id ?? this.#requestSeenCache.get(cacheKey);
246
+ if (knownRequestId) {
247
+ this.recordRequestJournalEntry(transaction, [request], forefront, false);
248
+ return {
249
+ wasAlreadyPresent: true,
250
+ // The dedup cache doesn't track the handled state; only the full record does.
251
+ wasAlreadyHandled: cachedInfo?.isHandled ?? false,
252
+ requestId: knownRequestId,
253
+ uniqueKey: request.uniqueKey,
254
+ forefront,
255
+ };
256
+ }
257
+ // The caches are bounded, so a miss is not proof of absence - probe for an accurate answer.
258
+ const existing = await this.backend.getRequest(request.uniqueKey);
259
+ if (existing) {
260
+ this.recordRequestJournalEntry(transaction, [request], forefront, false);
261
+ return {
262
+ wasAlreadyPresent: true,
263
+ wasAlreadyHandled: existing.handledAt != null,
264
+ requestId: existing.id,
265
+ uniqueKey: request.uniqueKey,
266
+ forefront,
267
+ };
268
+ }
269
+ // The entry below *is* the write, so a transaction closed during the probe must not receive it -
270
+ // pass through instead, per the closed-transaction rule. Under `deferred` that can land an
271
+ // addition a rollback would have discarded; dedup bounds that cost, silent loss is unbounded.
272
+ if (!transaction.isActive) {
273
+ return await this.addRequest(request, { forefront });
274
+ }
275
+ const snapshot = JSON.parse(JSON.stringify(request));
276
+ // Strip-list, not allow-list: every user-facing field flows through, including ones added to
277
+ // `Request` in the future. The exceptions are `id` and `handledAt`, the two backend-owned
278
+ // lifecycle fields.
279
+ delete snapshot.id;
280
+ delete snapshot.handledAt;
281
+ transaction.recordJournalEntry({
282
+ type: 'requestQueue',
283
+ participant: this,
284
+ requests: [{ url: request.url, uniqueKey: request.uniqueKey, label: request.label, snapshot }],
285
+ forefront,
286
+ writeThrough: false,
287
+ });
288
+ buffered.set(request.uniqueKey, snapshot);
289
+ return {
290
+ wasAlreadyPresent: false,
291
+ wasAlreadyHandled: false,
292
+ requestId: getRequestId(request.uniqueKey),
293
+ uniqueKey: request.uniqueKey,
294
+ forefront,
295
+ };
296
+ }
297
+ /** @internal */
298
+ async commitJournalEntries(entries) {
299
+ // Replay through `backend.addBatchOfRequests`, *not* the batched frontend wrapper - the wrapper
300
+ // resolves after the first chunk and sleeps between the rest, neither of which commit may
301
+ // inherit. One call per `forefront` flag; the order of forefront additions is arbitrary anyway.
302
+ for (const forefront of [false, true]) {
303
+ const requests = entries.flatMap((entry) => entry.type === 'requestQueue' && entry.forefront === forefront
304
+ ? // Requests without a snapshot were deduplicated or written through; nothing to replay.
305
+ entry.requests
306
+ .filter((journaled) => journaled.snapshot !== undefined)
307
+ .map((journaled) => new Request(journaled.snapshot))
308
+ : []);
309
+ if (requests.length === 0)
310
+ continue;
311
+ this.#statsTracker.add('writeCount');
312
+ const { processedRequests, unprocessedRequests } = await this.backend.addBatchOfRequests(requests, {
313
+ forefront,
314
+ });
315
+ // Only now, with the real backend-assigned ids, may the shared dedup caches be populated.
316
+ for (const processed of processedRequests) {
317
+ const cacheKey = getRequestId(processed.uniqueKey);
318
+ this.cacheRequest(cacheKey, { ...processed, forefront });
319
+ this.#requestSeenCache.add(cacheKey, processed.requestId);
320
+ }
321
+ if (unprocessedRequests.length > 0) {
322
+ // Warn and skip, rather than retry or fail. `unprocessedRequests` is what remains after
323
+ // the backend's own transient-error handling - a semantic rejection that retrying here
324
+ // would only re-poke. And failing the commit would let one malformed request hold the
325
+ // whole transaction hostage.
326
+ this.log.warning('Some requests were rejected by the request queue while committing a storage transaction and will be skipped. ' +
327
+ "This usually means the request data is malformed (e.g. an invalid 'userData' shape).", { unprocessedRequests });
328
+ }
329
+ }
330
+ }
178
331
  /**
179
332
  * Adds requests to the queue in batches of 25. This method will wait till all the requests are added
180
333
  * to the queue before resolving. You should prefer using `queue.addRequestsBatched()` or `crawler.addRequests()`
@@ -190,7 +343,7 @@ export class RequestQueue {
190
343
  * @param [options] Request queue operation options.
191
344
  */
192
345
  async addRequests(requestsLike, options = {}) {
193
- checkStorageAccess();
346
+ const transaction = activeStorageTransaction();
194
347
  ow(requestsLike, ow.object
195
348
  .is((value) => isIterable(value) || isAsyncIterable(value))
196
349
  .message((value) => `Expected an iterable or async iterable, got ${getObjectType(value)}`));
@@ -225,12 +378,20 @@ export class RequestQueue {
225
378
  requests.push(requestLike instanceof Request ? requestLike : new Request(requestLike));
226
379
  }
227
380
  }
381
+ if (transaction?.policy.requestQueue === 'deferred') {
382
+ const buffered = this.bufferedRequests(transaction);
383
+ for (const request of requests) {
384
+ results.processedRequests.push(await this.addRequestDeferred(transaction, request, forefront, buffered));
385
+ }
386
+ return results;
387
+ }
388
+ this.recordRequestJournalEntry(transaction, requests, forefront, true);
228
389
  const requestsToAdd = new Map();
229
390
  for (const request of requests) {
230
391
  const cacheKey = getCachedRequestId(request.uniqueKey);
231
392
  // Prefer the full `requestCache` record; fall back to the dedup cache for background batches it skips.
232
393
  const cachedInfo = this.requestCache.get(cacheKey);
233
- const knownRequestId = cachedInfo?.id ?? this.requestSeenCache.get(cacheKey);
394
+ const knownRequestId = cachedInfo?.id ?? this.#requestSeenCache.get(cacheKey);
234
395
  if (knownRequestId) {
235
396
  request.id = knownRequestId;
236
397
  results.processedRequests.push({
@@ -249,7 +410,7 @@ export class RequestQueue {
249
410
  if (!requestsToAdd.size) {
250
411
  return results;
251
412
  }
252
- this.statsTracker.add('writeCount');
413
+ this.#statsTracker.add('writeCount');
253
414
  const apiResults = await this.backend.addBatchOfRequests([...requestsToAdd.values()], { forefront });
254
415
  // Report unprocessed requests
255
416
  results.unprocessedRequests = apiResults.unprocessedRequests;
@@ -262,7 +423,7 @@ export class RequestQueue {
262
423
  this.cacheRequest(cacheKey, { ...newRequest, forefront });
263
424
  }
264
425
  // Unlike `requestCache`, populate this on every batch (including background ones).
265
- this.requestSeenCache.add(cacheKey, newRequest.requestId);
426
+ this.#requestSeenCache.add(cacheKey, newRequest.requestId);
266
427
  }
267
428
  return results;
268
429
  }
@@ -276,7 +437,8 @@ export class RequestQueue {
276
437
  * @param options Options for the request queue
277
438
  */
278
439
  async addRequestsBatched(requests, options = {}) {
279
- checkStorageAccess();
440
+ const transaction = activeStorageTransaction();
441
+ const deferred = transaction?.policy.requestQueue === 'deferred';
280
442
  ow(requests, ow.object
281
443
  .is((value) => isIterable(value) || isAsyncIterable(value))
282
444
  .message((value) => `Expected an iterable or async iterable, got ${getObjectType(value)}`));
@@ -313,7 +475,9 @@ export class RequestQueue {
313
475
  }
314
476
  }
315
477
  }
316
- const { batchSize = 1000, waitBetweenBatchesMillis = 1000, maxNewRequests = undefined } = options;
478
+ const { batchSize = 1000, maxNewRequests = undefined } = options;
479
+ // Under `deferred` no chunk performs backend I/O, so pacing them would only stall the handler.
480
+ const waitBetweenBatchesMillis = deferred ? 0 : (options.waitBetweenBatchesMillis ?? 1000);
317
481
  let remainingBudget = maxNewRequests ?? Infinity;
318
482
  const requestsOverLimit = [];
319
483
  // If there's a limit on the number of added requests, do not send batches bigger than the limit
@@ -373,21 +537,38 @@ export class RequestQueue {
373
537
  if ((await chunksIterator.peek()) === undefined) {
374
538
  return buildResult(addedRequests, Promise.resolve([]), requestIterator);
375
539
  }
376
- // eslint-disable-next-line no-async-promise-executor
377
- const promise = new Promise(async (resolve) => {
540
+ const processRemainingChunks = async () => {
378
541
  const finalAddedRequests = [];
379
542
  for await (const requestChunk of chunks) {
380
543
  finalAddedRequests.push(...(await processChunk(requestChunk, false)));
381
544
  await sleep(waitBetweenBatchesMillis);
382
545
  }
383
- resolve(finalAddedRequests);
546
+ return finalAddedRequests;
547
+ };
548
+ // maxNewRequests needs all batches to report skipped requests accurately; `deferred` needs them
549
+ // too - a writer that finishes after commit would have nowhere to put its journal entries.
550
+ const awaitsRemainingChunks = options.waitForAllRequestsToBeAdded || maxNewRequests !== undefined || deferred;
551
+ // eslint-disable-next-line no-async-promise-executor
552
+ const promise = new Promise(async (resolve) => {
553
+ if (awaitsRemainingChunks) {
554
+ // Awaited below, i.e. still within the caller's transaction scope, so the additions are
555
+ // journaled like the initial chunk - introspection must not depend on where the chunk
556
+ // boundary happened to fall.
557
+ resolve(await processRemainingChunks());
558
+ }
559
+ else {
560
+ // Nobody awaits this writer, so it outlives the transaction scope it inherits and must
561
+ // not record into a transaction that may already be closed. It writes directly - its
562
+ // write-through additions were never going to be rolled back anyway - which means the
563
+ // requests it adds are not journaled. See `StorageTransactionView.enqueuedUrls`.
564
+ resolve(await withDirectStorageAccess(processRemainingChunks));
565
+ }
384
566
  });
385
567
  this.inProgressRequestBatchCount += 1;
386
568
  void promise.finally(() => {
387
569
  this.inProgressRequestBatchCount -= 1;
388
570
  });
389
- // When maxNewRequests is set, we must wait for all batches so we can accurately report skipped requests.
390
- if (options.waitForAllRequestsToBeAdded || maxNewRequests !== undefined) {
571
+ if (awaitsRemainingChunks) {
391
572
  addedRequests.push(...(await promise));
392
573
  }
393
574
  return buildResult(addedRequests, promise, requestIterator);
@@ -399,8 +580,13 @@ export class RequestQueue {
399
580
  * @returns Returns the request object, or `null` if it was not found.
400
581
  */
401
582
  async getRequest(uniqueKey) {
402
- checkStorageAccess();
583
+ const transaction = activeStorageTransaction();
403
584
  ow(uniqueKey, ow.string);
585
+ // Requests buffered by the active transaction (under the `deferred` write policy) are visible to it.
586
+ const buffered = transaction && this.bufferedRequests(transaction).get(uniqueKey);
587
+ if (buffered) {
588
+ return new Request(buffered);
589
+ }
404
590
  const requestOptions = await this.backend.getRequest(uniqueKey);
405
591
  if (!requestOptions)
406
592
  return null;
@@ -424,11 +610,11 @@ export class RequestQueue {
424
610
  * Returns the request object or `null` if there are no more pending requests.
425
611
  */
426
612
  async fetchNextRequest() {
427
- checkStorageAccess();
428
- if (this.queuePausedForMigration) {
613
+ rejectOperationInTransaction('RequestQueue.fetchNextRequest()', 'it is part of the crawler request-processing bookkeeping, which a transaction must not affect.');
614
+ if (this.#queuePausedForMigration) {
429
615
  return null;
430
616
  }
431
- this.statsTracker.add('headItemReadCount');
617
+ this.#statsTracker.add('headItemReadCount');
432
618
  const requestOptions = await this.backend.fetchNextRequest();
433
619
  if (!requestOptions)
434
620
  return null;
@@ -441,7 +627,7 @@ export class RequestQueue {
441
627
  * Handled requests will never again be returned by the `fetchNextRequest` function.
442
628
  */
443
629
  async markRequestAsHandled(request) {
444
- checkStorageAccess();
630
+ rejectOperationInTransaction('RequestQueue.markRequestAsHandled()', 'it is part of the crawler request-processing bookkeeping, which a transaction must not affect.');
445
631
  ow(request, ow.object.partialShape({
446
632
  id: ow.string,
447
633
  uniqueKey: ow.string,
@@ -449,7 +635,7 @@ export class RequestQueue {
449
635
  }));
450
636
  const forefront = this.requestCache.get(getRequestId(request.uniqueKey))?.forefront ?? false;
451
637
  const handledAt = request.handledAt ?? new Date().toISOString();
452
- this.statsTracker.add('writeCount');
638
+ this.#statsTracker.add('writeCount');
453
639
  const processedRequest = await this.backend.markRequestAsHandled({
454
640
  ...request,
455
641
  handledAt,
@@ -474,7 +660,7 @@ export class RequestQueue {
474
660
  * For example, this lets you store the number of retries or error messages for the request.
475
661
  */
476
662
  async reclaimRequest(request, options = {}) {
477
- checkStorageAccess();
663
+ rejectOperationInTransaction('RequestQueue.reclaimRequest()', 'it is part of the crawler request-processing bookkeeping, which a transaction must not affect.');
478
664
  ow(request, ow.object.partialShape({
479
665
  id: ow.string,
480
666
  uniqueKey: ow.string,
@@ -483,7 +669,7 @@ export class RequestQueue {
483
669
  forefront: ow.optional.boolean,
484
670
  }));
485
671
  const { forefront = false } = options;
486
- this.statsTracker.add('writeCount');
672
+ this.#statsTracker.add('writeCount');
487
673
  const processedRequest = await this.backend.reclaimRequest(request, { forefront });
488
674
  // The request was not in progress — nothing to reclaim.
489
675
  if (!processedRequest) {
@@ -508,7 +694,11 @@ export class RequestQueue {
508
694
  * {@link RequestQueue.isFinished}.
509
695
  */
510
696
  async isEmpty() {
511
- checkStorageAccess();
697
+ const transaction = activeStorageTransaction();
698
+ // Requests buffered by the active transaction count as pending from its point of view.
699
+ if (transaction && this.bufferedRequests(transaction).size > 0) {
700
+ return false;
701
+ }
512
702
  return this.backend.isEmpty();
513
703
  }
514
704
  /**
@@ -520,11 +710,15 @@ export class RequestQueue {
520
710
  * a false negative, but it shall never return a false positive.
521
711
  */
522
712
  async isFinished() {
523
- checkStorageAccess();
713
+ const transaction = activeStorageTransaction();
524
714
  // We are not finished if we're still adding new requests in the background.
525
715
  if (this.inProgressRequestBatchCount > 0) {
526
716
  return false;
527
717
  }
718
+ // Requests buffered by the active transaction count as pending from its point of view.
719
+ if (transaction && this.bufferedRequests(transaction).size > 0) {
720
+ return false;
721
+ }
528
722
  return this.backend.isFinished();
529
723
  }
530
724
  /**
@@ -538,10 +732,10 @@ export class RequestQueue {
538
732
  * short the reservation of a long-lived one and have its in-flight request stolen.
539
733
  */
540
734
  async setExpectedRequestProcessingTimeSecs(secs) {
541
- if (secs <= this.expectedRequestProcessingSecs) {
735
+ if (secs <= this.#expectedRequestProcessingSecs) {
542
736
  return;
543
737
  }
544
- this.expectedRequestProcessingSecs = secs;
738
+ this.#expectedRequestProcessingSecs = secs;
545
739
  await this.backend.setExpectedRequestProcessingTimeSecs?.(secs);
546
740
  }
547
741
  /**
@@ -564,7 +758,7 @@ export class RequestQueue {
564
758
  * depending on the mode of operation.
565
759
  */
566
760
  async drop() {
567
- checkStorageAccess();
761
+ rejectOperationInTransaction('RequestQueue.drop()');
568
762
  await this.backend.drop();
569
763
  serviceLocator.getStorageInstanceManager().removeFromCache(this);
570
764
  }
@@ -573,16 +767,16 @@ export class RequestQueue {
573
767
  * so it can be reused (e.g. across multiple `crawler.run()` calls).
574
768
  */
575
769
  async purge() {
576
- checkStorageAccess();
770
+ rejectOperationInTransaction('RequestQueue.purge()');
577
771
  await this.backend.purge();
578
772
  // Reset in-memory bookkeeping so the queue behaves as if freshly opened.
579
773
  this.requestCache.clear();
580
- this.requestSeenCache.clear();
774
+ this.#requestSeenCache.clear();
581
775
  this.inProgressRequestBatchCount = 0;
582
776
  // Reset the expected-processing-time high-water mark too, otherwise the monotonic-raise guard
583
777
  // in `setExpectedRequestProcessingTimeSecs` would let a value raised in an earlier run leak into a
584
778
  // later one and silently swallow a lower hint (the queue is meant to be reusable across runs).
585
- this.expectedRequestProcessingSecs = 0;
779
+ this.#expectedRequestProcessingSecs = 0;
586
780
  }
587
781
  /**
588
782
  * @inheritdoc
@@ -630,8 +824,17 @@ export class RequestQueue {
630
824
  * @throws If the underlying storage no longer exists (e.g. it was deleted externally).
631
825
  */
632
826
  async getInfo() {
633
- checkStorageAccess();
634
- return this.backend.getMetadata();
827
+ const transaction = activeStorageTransaction();
828
+ const metadata = await this.backend.getMetadata();
829
+ const bufferedCount = transaction ? this.bufferedRequests(transaction).size : 0;
830
+ if (bufferedCount > 0) {
831
+ return {
832
+ ...metadata,
833
+ totalRequestCount: metadata.totalRequestCount + bufferedCount,
834
+ pendingRequestCount: metadata.pendingRequestCount + bufferedCount,
835
+ };
836
+ }
837
+ return metadata;
635
838
  }
636
839
  /**
637
840
  * Fetches URLs from requestsFromUrl and returns them in format of list of requests
@@ -641,10 +844,10 @@ export class RequestQueue {
641
844
  // Download remote resource and parse URLs.
642
845
  let urlsArr;
643
846
  try {
644
- urlsArr = await this._downloadListOfUrls({
847
+ urlsArr = await this.downloadListOfUrls({
645
848
  url: requestsFromUrl,
646
849
  urlRegExp: regex,
647
- proxyUrl: (await this.proxyConfiguration?.newProxyInfo())?.url,
850
+ proxyUrl: (await this.#proxyConfiguration?.newProxyInfo())?.url,
648
851
  });
649
852
  }
650
853
  catch (err) {
@@ -676,10 +879,10 @@ export class RequestQueue {
676
879
  /**
677
880
  * @internal wraps public utility for mocking purposes
678
881
  */
679
- async _downloadListOfUrls(options) {
882
+ async downloadListOfUrls(options) {
680
883
  return downloadListOfUrls({
681
884
  ...options,
682
- httpClient: this.httpClient,
885
+ httpClient: this.#httpClient,
683
886
  });
684
887
  }
685
888
  /**
@@ -700,7 +903,7 @@ export class RequestQueue {
700
903
  * @param [options] Open Request Queue options.
701
904
  */
702
905
  static async open(identifier, options = {}) {
703
- checkStorageAccess();
906
+ tryCancel();
704
907
  ow(options, ow.object.exactShape({
705
908
  configuration: ow.optional.object.instanceOf(Configuration),
706
909
  storageBackend: ow.optional.object,
@@ -718,8 +921,8 @@ export class RequestQueue {
718
921
  backendOpener: () => storageBackend.createRequestQueueBackend(resolved),
719
922
  backendCacheKey: storageBackend.getStorageBackendCacheKey?.() ?? storageBackend.constructor.name,
720
923
  });
721
- queue.proxyConfiguration = options.proxyConfiguration;
722
- queue.httpClient = options.httpClient;
924
+ queue.#proxyConfiguration = options.proxyConfiguration;
925
+ queue.#httpClient = options.httpClient;
723
926
  return queue;
724
927
  }
725
928
  }
@@ -80,55 +80,12 @@ export interface SitemapRequestLoaderOptions extends UrlConstraints {
80
80
  * The loading of the sitemap is performed in the background so that crawling can start before the sitemap is fully loaded.
81
81
  */
82
82
  export declare class SitemapRequestLoader implements IRequestLoader {
83
+ #private;
83
84
  /**
84
85
  * Set of URLs that were returned by `fetchNextRequest()` and not marked as handled yet.
85
86
  * @internal
86
87
  */
87
88
  inProgress: Set<string>;
88
- /**
89
- * Map of returned Request objects that have not been marked as handled yet.
90
- *
91
- * We use this to persist custom user fields on the in-progress requests.
92
- */
93
- private requestData;
94
- /**
95
- * Object for keeping track of the sitemap parsing progress.
96
- */
97
- private sitemapParsingProgress;
98
- /**
99
- * Object stream of URLs parsed from the sitemaps.
100
- * Using `highWaterMark`, this can manage the speed of the sitemap loading.
101
- *
102
- * Fetch the next URL to be processed using `fetchNextRequest()`.
103
- */
104
- private urlQueueStream;
105
- /**
106
- * Indicates whether the request list sitemap loading was aborted.
107
- *
108
- * If the loading was aborted before the sitemaps were fully loaded, the request list might be missing some URLs.
109
- * The `isSitemapFullyLoaded` method can be used to check if the sitemaps were fully loaded.
110
- *
111
- * If the loading is aborted and all the requests are handled, `isFinished()` will return `true`.
112
- */
113
- private abortLoading;
114
- /** Number of URLs that were marked as handled */
115
- private handledUrlCount;
116
- private persistStateKey?;
117
- private store?;
118
- private closed;
119
- /**
120
- * Proxy URL to be used for sitemap loading.
121
- */
122
- private proxyUrl?;
123
- /**
124
- * Logger instance.
125
- */
126
- private log;
127
- private urlExcludePatternObjects;
128
- private urlPatternObjects;
129
- /** EventManager used to handle persistence */
130
- private events;
131
- private persistenceOptions;
132
89
  /** @internal */
133
90
  private constructor();
134
91
  /**