@crawlee/core 4.0.0-beta.99 → 4.0.0-rc.0

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 (109) hide show
  1. package/autoscaling/autoscaled_pool.d.ts +3 -21
  2. package/autoscaling/autoscaled_pool.js +105 -97
  3. package/autoscaling/concurrency_system.d.ts +7 -22
  4. package/autoscaling/concurrency_system.js +112 -100
  5. package/autoscaling/cpu_load_signal.d.ts +1 -2
  6. package/autoscaling/cpu_load_signal.js +11 -10
  7. package/autoscaling/event_loop_load_signal.d.ts +1 -4
  8. package/autoscaling/event_loop_load_signal.js +18 -18
  9. package/autoscaling/index.d.ts +1 -1
  10. package/autoscaling/index.js +1 -1
  11. package/autoscaling/load_signal.d.ts +8 -7
  12. package/autoscaling/load_signal.js +14 -12
  13. package/autoscaling/memory_load_signal.d.ts +3 -12
  14. package/autoscaling/memory_load_signal.js +41 -41
  15. package/autoscaling/snapshotter.d.ts +7 -10
  16. package/autoscaling/snapshotter.js +18 -18
  17. package/autoscaling/{client_load_signal.d.ts → storage_backend_load_signal.d.ts} +14 -17
  18. package/autoscaling/{client_load_signal.js → storage_backend_load_signal.js} +27 -27
  19. package/autoscaling/system_status.d.ts +9 -11
  20. package/autoscaling/system_status.js +13 -13
  21. package/configuration.d.ts +16 -16
  22. package/configuration.js +6 -6
  23. package/crawlers/context_pipeline.js +6 -6
  24. package/crawlers/crawler_commons.d.ts +14 -112
  25. package/crawlers/crawler_commons.js +1 -107
  26. package/crawlers/error_snapshotter.d.ts +2 -4
  27. package/crawlers/error_snapshotter.js +5 -5
  28. package/crawlers/index.d.ts +1 -1
  29. package/crawlers/index.js +0 -1
  30. package/crawlers/statistics.d.ts +164 -45
  31. package/crawlers/statistics.js +339 -153
  32. package/debug.js +4 -4
  33. package/enqueue_links/enqueue_links.d.ts +51 -159
  34. package/enqueue_links/enqueue_links.js +38 -231
  35. package/enqueue_links/shared.d.ts +34 -24
  36. package/enqueue_links/shared.js +54 -36
  37. package/errors.d.ts +43 -12
  38. package/errors.js +48 -9
  39. package/events/event_manager.d.ts +2 -2
  40. package/events/event_manager.js +7 -6
  41. package/events/local_event_manager.d.ts +1 -1
  42. package/events/local_event_manager.js +5 -5
  43. package/http.d.ts +9 -0
  44. package/http.js +28 -0
  45. package/index.d.ts +1 -1
  46. package/index.js +1 -1
  47. package/log.js +5 -1
  48. package/memory-storage/memory-storage.d.ts +3 -11
  49. package/memory-storage/memory-storage.js +42 -48
  50. package/memory-storage/resource-clients/dataset.d.ts +1 -1
  51. package/memory-storage/resource-clients/dataset.js +8 -13
  52. package/memory-storage/resource-clients/key-value-store.d.ts +1 -1
  53. package/memory-storage/resource-clients/key-value-store.js +36 -38
  54. package/memory-storage/resource-clients/request-queue.d.ts +4 -23
  55. package/memory-storage/resource-clients/request-queue.js +68 -80
  56. package/owned_or_injected.d.ts +1 -3
  57. package/owned_or_injected.js +17 -17
  58. package/package.json +8 -9
  59. package/proxy_configuration.d.ts +1 -3
  60. package/proxy_configuration.js +18 -14
  61. package/recoverable_state.d.ts +80 -45
  62. package/recoverable_state.js +163 -75
  63. package/request.d.ts +3 -4
  64. package/request.js +57 -44
  65. package/router.d.ts +6 -9
  66. package/router.js +23 -23
  67. package/serialization.js +14 -13
  68. package/service_locator.d.ts +19 -10
  69. package/service_locator.js +57 -48
  70. package/session_pool/session.d.ts +3 -14
  71. package/session_pool/session.js +76 -74
  72. package/session_pool/session_pool.d.ts +6 -12
  73. package/session_pool/session_pool.js +85 -77
  74. package/storages/batched_adds.d.ts +37 -0
  75. package/storages/batched_adds.js +73 -0
  76. package/storages/dataset.d.ts +12 -1
  77. package/storages/dataset.js +133 -32
  78. package/storages/index.d.ts +2 -1
  79. package/storages/index.js +2 -1
  80. package/storages/key_value_store.d.ts +19 -4
  81. package/storages/key_value_store.js +204 -75
  82. package/storages/request_dedup_cache.d.ts +1 -2
  83. package/storages/request_dedup_cache.js +9 -9
  84. package/storages/request_list.d.ts +4 -23
  85. package/storages/request_list.js +103 -96
  86. package/storages/request_loader.d.ts +5 -0
  87. package/storages/request_manager_tandem.d.ts +1 -10
  88. package/storages/request_manager_tandem.js +27 -27
  89. package/storages/request_queue.d.ts +21 -18
  90. package/storages/request_queue.js +318 -177
  91. package/storages/sitemap_request_loader.d.ts +22 -70
  92. package/storages/sitemap_request_loader.js +122 -116
  93. package/storages/storage_instance_manager.d.ts +1 -2
  94. package/storages/storage_instance_manager.js +17 -17
  95. package/storages/storage_stats.d.ts +1 -1
  96. package/storages/storage_stats.js +4 -4
  97. package/storages/throttling_request_manager.d.ts +239 -0
  98. package/storages/throttling_request_manager.js +646 -0
  99. package/storages/transaction.d.ts +252 -0
  100. package/storages/transaction.js +251 -0
  101. package/storages/utils.d.ts +6 -3
  102. package/storages/utils.js +11 -2
  103. package/system-info/runtime.js +7 -7
  104. package/url.d.ts +9 -0
  105. package/url.js +11 -0
  106. package/validators.d.ts +22 -25
  107. package/validators.js +13 -25
  108. package/storages/access_checking.d.ts +0 -12
  109. package/storages/access_checking.js +0 -17
@@ -1,12 +1,17 @@
1
- import { stringify } from 'csv-stringify/sync';
2
- import ow from 'ow';
1
+ import { z } from 'zod';
2
+ import { tryCancel } from '@apify/timeout';
3
3
  import { Configuration } from '../configuration.js';
4
4
  import { serviceLocator } from '../service_locator.js';
5
- import { checkStorageAccess } from './access_checking.js';
5
+ import { parseArgument, schemas, validators } from '../validators.js';
6
+ import { activeStorageTransaction, rejectOperationInTransaction, snapshotValue } from './transaction.js';
6
7
  import { KeyValueStore } from './key_value_store.js';
7
8
  import { StorageStatsTracker } from './storage_stats.js';
8
9
  import { resolveStorageIdentifier } from './storage_instance_manager.js';
9
10
  import { createDualIterable, purgeDefaultStorages } from './utils.js';
11
+ const openOptionsSchema = z.strictObject({
12
+ configuration: z.instanceof(Configuration).optional(),
13
+ storageBackend: validators.storageBackend.optional(),
14
+ });
10
15
  /** @internal */
11
16
  export const DATASET_ITERATORS_DEFAULT_LIMIT = 10000;
12
17
  /**
@@ -88,7 +93,7 @@ export class Dataset {
88
93
  name;
89
94
  backend;
90
95
  log;
91
- statsTracker = new StorageStatsTracker({
96
+ #statsTracker = new StorageStatsTracker({
92
97
  readCount: 0,
93
98
  writeCount: 0,
94
99
  });
@@ -107,7 +112,7 @@ export class Dataset {
107
112
  * the underlying storage backend). Counted per backend call.
108
113
  */
109
114
  get stats() {
110
- return this.statsTracker.current;
115
+ return this.#statsTracker.current;
111
116
  }
112
117
  /**
113
118
  * Stores an object or an array of objects to the dataset.
@@ -121,24 +126,33 @@ export class Dataset {
121
126
  * The objects must be serializable to JSON.
122
127
  */
123
128
  async pushData(data) {
124
- checkStorageAccess();
125
- ow(data, 'data', ow.object);
129
+ const transaction = activeStorageTransaction();
130
+ parseArgument(data, schemas.anyObject);
126
131
  // Normalize to array and validate each item
127
132
  const items = Array.isArray(data) ? data : [data];
128
133
  for (let i = 0; i < items.length; i++) {
129
134
  assertJsonSerializable(items[i], i);
130
135
  }
131
- this.statsTracker.add('writeCount');
136
+ if (transaction) {
137
+ // One snapshot serves both the reads and the commit replay, so the two cannot disagree.
138
+ transaction.recordJournalEntry({
139
+ type: 'dataset',
140
+ participant: this,
141
+ storageId: this.id,
142
+ items: snapshotValue(items),
143
+ recordedAt: new Date(),
144
+ });
145
+ return;
146
+ }
147
+ this.#statsTracker.add('writeCount');
132
148
  await this.backend.pushData(items);
133
149
  }
134
150
  /**
135
151
  * Returns {@link DatasetContent} object holding the items in the dataset based on the provided parameters.
136
152
  */
137
153
  async getData(options = {}) {
138
- checkStorageAccess();
139
154
  try {
140
- this.statsTracker.add('readCount');
141
- return await this.backend.getData(options);
155
+ return await this.readPage(options);
142
156
  }
143
157
  catch (e) {
144
158
  const error = e;
@@ -148,12 +162,93 @@ export class Dataset {
148
162
  throw e;
149
163
  }
150
164
  }
165
+ /**
166
+ * The single transaction-aware page read all dataset read paths go through — both `getData()` and
167
+ * the private `fetchPages()`. Returns the real page concatenated with the current transaction's
168
+ * buffered items, with `offset` / `limit` / `desc` windowing applied across the concatenation.
169
+ */
170
+ async readPage(options) {
171
+ const buffered = this.bufferedJournalEntries()?.flatMap((entry) => entry.items);
172
+ // Every branch below hits the backend exactly once.
173
+ this.#statsTracker.add('readCount');
174
+ if (!buffered?.length) {
175
+ return this.backend.getData(options);
176
+ }
177
+ const { offset = 0, limit, desc = false } = options;
178
+ if (!desc) {
179
+ const realPage = await this.backend.getData(options);
180
+ // Buffered items sit past `realPage.total`, so the window bounds must come from that - not
181
+ // from the page's shortfall, which `skipEmpty` produces without exhausting the real items.
182
+ const bufferedStart = Math.max(0, offset - realPage.total);
183
+ const bufferedEnd = limit === undefined ? buffered.length : Math.max(0, offset + limit - realPage.total);
184
+ const items = [...realPage.items, ...buffered.slice(bufferedStart, bufferedEnd)];
185
+ return {
186
+ items,
187
+ total: realPage.total + buffered.length,
188
+ offset,
189
+ // A caller that passed no limit wants everything, so the backend-reported `limit` is
190
+ // passed through - backends are free to report a page size or a sentinel there.
191
+ limit: limit ?? realPage.limit,
192
+ count: items.length,
193
+ desc,
194
+ };
195
+ }
196
+ // Descending order: the buffered items are the newest, so they come first, reversed.
197
+ const reversedBuffer = [...buffered].reverse();
198
+ const fromBuffer = limit === undefined ? reversedBuffer.slice(offset) : reversedBuffer.slice(offset, offset + limit);
199
+ const needed = limit === undefined ? Infinity : limit - fromBuffer.length;
200
+ if (needed <= 0) {
201
+ // The whole window is served from the buffer; only the real total is missing.
202
+ const { itemCount } = await this.backend.getMetadata();
203
+ return {
204
+ items: fromBuffer,
205
+ total: itemCount + buffered.length,
206
+ offset,
207
+ limit: limit,
208
+ count: fromBuffer.length,
209
+ desc,
210
+ };
211
+ }
212
+ const realPage = await this.backend.getData({
213
+ ...options,
214
+ offset: Math.max(0, offset - buffered.length),
215
+ ...(limit === undefined ? {} : { limit: needed }),
216
+ });
217
+ return {
218
+ items: [...fromBuffer, ...realPage.items],
219
+ total: realPage.total + buffered.length,
220
+ offset,
221
+ limit: limit ?? realPage.limit,
222
+ count: fromBuffer.length + realPage.items.length,
223
+ desc,
224
+ };
225
+ }
226
+ /** The active transaction's buffered writes to this dataset, derived from its journal. */
227
+ bufferedJournalEntries() {
228
+ const transaction = activeStorageTransaction();
229
+ return transaction?.journal.filter((entry) => entry.type === 'dataset' && entry.participant === this);
230
+ }
231
+ /** @internal */
232
+ async commitJournalEntries(entries) {
233
+ const items = [];
234
+ for (const entry of entries) {
235
+ if (entry.type === 'dataset') {
236
+ items.push(...entry.items);
237
+ }
238
+ }
239
+ // One backend call with all journaled items, in order - as close to atomic as the backend allows.
240
+ // Straight to the backend: the items were validated and snapshotted at write time.
241
+ if (items.length > 0) {
242
+ this.#statsTracker.add('writeCount');
243
+ await this.backend.pushData(items);
244
+ }
245
+ }
151
246
  /**
152
247
  * Returns all the data from the dataset. This will iterate through the whole dataset
153
248
  * via the `listItems()` client method, which gives you only paginated results.
154
249
  */
155
250
  async export(options = {}) {
156
- checkStorageAccess();
251
+ tryCancel();
157
252
  const items = [];
158
253
  for await (const page of this.fetchPages(options)) {
159
254
  items.push(...page.items);
@@ -179,6 +274,7 @@ export class Dataset {
179
274
  const keys = options?.collectAllKeys
180
275
  ? Array.from(new Set(items.flatMap(Object.keys)))
181
276
  : Object.keys(items[0]);
277
+ const { stringify } = await import('csv-stringify/sync');
182
278
  const value = stringify([
183
279
  keys,
184
280
  ...items.map((item) => {
@@ -219,7 +315,7 @@ export class Dataset {
219
315
  * @param [options] An optional options object where you can provide the dataset and target KVS name.
220
316
  */
221
317
  static async exportToJSON(key, options) {
222
- checkStorageAccess();
318
+ tryCancel();
223
319
  const dataset = await this.open(options?.fromDataset);
224
320
  await dataset.exportToJSON(key, options);
225
321
  }
@@ -230,7 +326,7 @@ export class Dataset {
230
326
  * @param [options] An optional options object where you can provide the dataset and target KVS name.
231
327
  */
232
328
  static async exportToCSV(key, options) {
233
- checkStorageAccess();
329
+ tryCancel();
234
330
  const dataset = await this.open(options?.fromDataset);
235
331
  await dataset.exportToCSV(key, options);
236
332
  }
@@ -252,8 +348,17 @@ export class Dataset {
252
348
  * @throws If the underlying storage no longer exists (e.g. it was deleted externally).
253
349
  */
254
350
  async getInfo() {
255
- checkStorageAccess();
256
- return this.backend.getMetadata();
351
+ const buffered = this.bufferedJournalEntries();
352
+ const metadata = await this.backend.getMetadata();
353
+ if (buffered?.length) {
354
+ const lastWriteAt = buffered[buffered.length - 1].recordedAt;
355
+ return {
356
+ ...metadata,
357
+ itemCount: metadata.itemCount + buffered.reduce((sum, entry) => sum + entry.items.length, 0),
358
+ modifiedAt: metadata.modifiedAt > lastWriteAt ? metadata.modifiedAt : lastWriteAt,
359
+ };
360
+ }
361
+ return metadata;
257
362
  }
258
363
  /**
259
364
  * Iterates over dataset items, yielding each in turn to an `iteratee` function.
@@ -276,7 +381,7 @@ export class Dataset {
276
381
  * @default 0
277
382
  */
278
383
  async forEach(iteratee, options = {}, index = 0) {
279
- checkStorageAccess();
384
+ tryCancel();
280
385
  if (!options.offset)
281
386
  options.offset = 0;
282
387
  if (options.format && options.format !== 'json')
@@ -303,7 +408,7 @@ export class Dataset {
303
408
  * @param [options] All `map()` parameters.
304
409
  */
305
410
  async map(iteratee, options = {}) {
306
- checkStorageAccess();
411
+ tryCancel();
307
412
  const result = [];
308
413
  await this.forEach(async (item, index) => {
309
414
  const res = await iteratee(item, index);
@@ -312,7 +417,7 @@ export class Dataset {
312
417
  return result;
313
418
  }
314
419
  async reduce(iteratee, memo, options = {}) {
315
- checkStorageAccess();
420
+ tryCancel();
316
421
  let currentMemo = memo;
317
422
  const wrappedFunc = async (item, index) => {
318
423
  if (index === 0 && currentMemo === undefined) {
@@ -344,8 +449,7 @@ export class Dataset {
344
449
  const fetchLimit = totalLimit !== undefined ? Math.min(pageSize, totalLimit - yielded) : pageSize;
345
450
  if (fetchLimit <= 0)
346
451
  break;
347
- this.statsTracker.add('readCount');
348
- const page = await this.backend.getData({ ...options, offset, limit: fetchLimit });
452
+ const page = await this.readPage({ ...options, offset, limit: fetchLimit });
349
453
  yield page;
350
454
  yielded += page.items.length;
351
455
  if (page.items.length < fetchLimit || offset + page.items.length >= page.total)
@@ -377,7 +481,7 @@ export class Dataset {
377
481
  * @param options Options for the iteration.
378
482
  */
379
483
  values(options = {}) {
380
- checkStorageAccess();
484
+ tryCancel();
381
485
  return createDualIterable({
382
486
  createPages: () => this.fetchPages(options),
383
487
  extractItems: (page) => page.items,
@@ -407,7 +511,7 @@ export class Dataset {
407
511
  * @param options Options for the iteration.
408
512
  */
409
513
  entries(options = {}) {
410
- checkStorageAccess();
514
+ tryCancel();
411
515
  return createDualIterable({
412
516
  createPages: () => this.fetchEntryPages(options),
413
517
  extractItems: (page) => page.items,
@@ -433,7 +537,7 @@ export class Dataset {
433
537
  * depending on the mode of operation.
434
538
  */
435
539
  async drop() {
436
- checkStorageAccess();
540
+ rejectOperationInTransaction('Dataset.drop()');
437
541
  await this.backend.drop();
438
542
  serviceLocator.getStorageInstanceManager().removeFromCache(this);
439
543
  }
@@ -453,14 +557,11 @@ export class Dataset {
453
557
  * @param [options] Storage manager options.
454
558
  */
455
559
  static async open(identifier, options = {}) {
456
- checkStorageAccess();
457
- ow(options, ow.object.exactShape({
458
- configuration: ow.optional.object.instanceOf(Configuration),
459
- storageBackend: ow.optional.object,
460
- }));
461
- options.configuration ??= Configuration.getGlobalConfiguration();
462
- const storageBackend = options.storageBackend ?? serviceLocator.getStorageBackend();
463
- await purgeDefaultStorages({ onlyPurgeOnce: true, storageBackend, configuration: options.configuration });
560
+ tryCancel();
561
+ const parsedOptions = parseArgument(options, openOptionsSchema);
562
+ const configuration = parsedOptions.configuration ?? Configuration.getGlobalConfiguration();
563
+ const storageBackend = parsedOptions.storageBackend ?? serviceLocator.getStorageBackend();
564
+ await purgeDefaultStorages({ onlyPurgeOnce: true, storageBackend, configuration });
464
565
  const resolved = await resolveStorageIdentifier(identifier, storageBackend, 'Dataset');
465
566
  return serviceLocator.getStorageInstanceManager().openStorage(this, {
466
567
  ...resolved,
@@ -8,6 +8,7 @@ export * from './request_queue.js';
8
8
  export * from './storage_instance_manager.js';
9
9
  export * from './storage_stats.js';
10
10
  export * from './utils.js';
11
- export * from './access_checking.js';
11
+ export * from './transaction.js';
12
12
  export * from './sitemap_request_loader.js';
13
13
  export * from './request_manager_tandem.js';
14
+ export * from './throttling_request_manager.js';
package/storages/index.js CHANGED
@@ -6,6 +6,7 @@ export * from './request_queue.js';
6
6
  export * from './storage_instance_manager.js';
7
7
  export * from './storage_stats.js';
8
8
  export * from './utils.js';
9
- export * from './access_checking.js';
9
+ export * from './transaction.js';
10
10
  export * from './sitemap_request_loader.js';
11
11
  export * from './request_manager_tandem.js';
12
+ export * from './throttling_request_manager.js';
@@ -1,5 +1,6 @@
1
1
  import type { Awaitable, Dictionary, KeyValueStoreBackend, KeyValueStoreInfo } from '@crawlee/types';
2
2
  import { Configuration } from '../configuration.js';
3
+ import type { JournalEntry } from './transaction.js';
3
4
  import type { KeyValueStoreStats } from './storage_stats.js';
4
5
  import type { StorageOpenOptions } from './utils.js';
5
6
  import type { StorageIdentifier } from './storage_instance_manager.js';
@@ -60,14 +61,11 @@ import type { StorageIdentifier } from './storage_instance_manager.js';
60
61
  * @category Result Stores
61
62
  */
62
63
  export declare class KeyValueStore {
64
+ #private;
63
65
  readonly configuration: Configuration;
64
66
  readonly id: string;
65
67
  readonly name?: string;
66
68
  private readonly backend;
67
- private persistStateEventStarted;
68
- /** Cache for persistent (auto-saved) values. When we try to set such value, the cache will be updated automatically. */
69
- private readonly cache;
70
- private readonly statsTracker;
71
69
  /**
72
70
  * @internal
73
71
  */
@@ -141,6 +139,21 @@ export declare class KeyValueStore {
141
139
  * on the MIME content type of the record, or the default value if the key is missing from the store.
142
140
  */
143
141
  getValue<T = unknown>(key: string, defaultValue: T): Promise<T>;
142
+ /**
143
+ * The active transaction's last buffered write per key for this store, derived from its journal.
144
+ * An entry with a `null` value is a tombstone (an in-transaction deletion).
145
+ */
146
+ private bufferedJournalEntries;
147
+ /**
148
+ * The single transaction-aware record read shared by `getValue`, `getRecord`, `recordExists` and the
149
+ * listing paths: buffered key → serialized through the standard codec (same fidelity as a real
150
+ * round-trip); tombstoned key → `null`; otherwise the backend.
151
+ *
152
+ * The per-key buffered lookup requires the whole journal to be reduced to a last-write-per-key map,
153
+ * which is O(journal). Single-record callers let it default (rebuilt per call); the listing paths,
154
+ * which read many keys, pass a map built once so the read stays O(1) per key instead of O(journal).
155
+ */
156
+ private readRecord;
144
157
  /**
145
158
  * Reads a record from the key-value store without parsing the value.
146
159
  *
@@ -223,6 +236,8 @@ export declare class KeyValueStore {
223
236
  * @param [options] Record options.
224
237
  */
225
238
  setValue<T>(key: string, value: T | null, options?: RecordOptions): Promise<void>;
239
+ /** @internal */
240
+ commitJournalEntries(entries: JournalEntry[]): Promise<void>;
226
241
  /**
227
242
  * Removes the key-value store either from the Apify cloud storage or from the local directory,
228
243
  * depending on the mode of operation.