@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
@@ -1,8 +1,9 @@
1
1
  import { stringify } from 'csv-stringify/sync';
2
2
  import ow from 'ow';
3
+ import { tryCancel } from '@apify/timeout';
3
4
  import { Configuration } from '../configuration.js';
4
5
  import { serviceLocator } from '../service_locator.js';
5
- import { checkStorageAccess } from './access_checking.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';
@@ -88,7 +89,7 @@ export class Dataset {
88
89
  name;
89
90
  backend;
90
91
  log;
91
- statsTracker = new StorageStatsTracker({
92
+ #statsTracker = new StorageStatsTracker({
92
93
  readCount: 0,
93
94
  writeCount: 0,
94
95
  });
@@ -107,7 +108,7 @@ export class Dataset {
107
108
  * the underlying storage backend). Counted per backend call.
108
109
  */
109
110
  get stats() {
110
- return this.statsTracker.current;
111
+ return this.#statsTracker.current;
111
112
  }
112
113
  /**
113
114
  * Stores an object or an array of objects to the dataset.
@@ -121,24 +122,33 @@ export class Dataset {
121
122
  * The objects must be serializable to JSON.
122
123
  */
123
124
  async pushData(data) {
124
- checkStorageAccess();
125
+ const transaction = activeStorageTransaction();
125
126
  ow(data, 'data', ow.object);
126
127
  // Normalize to array and validate each item
127
128
  const items = Array.isArray(data) ? data : [data];
128
129
  for (let i = 0; i < items.length; i++) {
129
130
  assertJsonSerializable(items[i], i);
130
131
  }
131
- this.statsTracker.add('writeCount');
132
+ if (transaction) {
133
+ // One snapshot serves both the reads and the commit replay, so the two cannot disagree.
134
+ transaction.recordJournalEntry({
135
+ type: 'dataset',
136
+ participant: this,
137
+ storageId: this.id,
138
+ items: snapshotValue(items),
139
+ recordedAt: new Date(),
140
+ });
141
+ return;
142
+ }
143
+ this.#statsTracker.add('writeCount');
132
144
  await this.backend.pushData(items);
133
145
  }
134
146
  /**
135
147
  * Returns {@link DatasetContent} object holding the items in the dataset based on the provided parameters.
136
148
  */
137
149
  async getData(options = {}) {
138
- checkStorageAccess();
139
150
  try {
140
- this.statsTracker.add('readCount');
141
- return await this.backend.getData(options);
151
+ return await this.readPage(options);
142
152
  }
143
153
  catch (e) {
144
154
  const error = e;
@@ -148,12 +158,93 @@ export class Dataset {
148
158
  throw e;
149
159
  }
150
160
  }
161
+ /**
162
+ * The single transaction-aware page read all dataset read paths go through — both `getData()` and
163
+ * the private `fetchPages()`. Returns the real page concatenated with the current transaction's
164
+ * buffered items, with `offset` / `limit` / `desc` windowing applied across the concatenation.
165
+ */
166
+ async readPage(options) {
167
+ const buffered = this.bufferedJournalEntries()?.flatMap((entry) => entry.items);
168
+ // Every branch below hits the backend exactly once.
169
+ this.#statsTracker.add('readCount');
170
+ if (!buffered?.length) {
171
+ return this.backend.getData(options);
172
+ }
173
+ const { offset = 0, limit, desc = false } = options;
174
+ if (!desc) {
175
+ const realPage = await this.backend.getData(options);
176
+ // Buffered items sit past `realPage.total`, so the window bounds must come from that - not
177
+ // from the page's shortfall, which `skipEmpty` produces without exhausting the real items.
178
+ const bufferedStart = Math.max(0, offset - realPage.total);
179
+ const bufferedEnd = limit === undefined ? buffered.length : Math.max(0, offset + limit - realPage.total);
180
+ const items = [...realPage.items, ...buffered.slice(bufferedStart, bufferedEnd)];
181
+ return {
182
+ items,
183
+ total: realPage.total + buffered.length,
184
+ offset,
185
+ // A caller that passed no limit wants everything, so the backend-reported `limit` is
186
+ // passed through - backends are free to report a page size or a sentinel there.
187
+ limit: limit ?? realPage.limit,
188
+ count: items.length,
189
+ desc,
190
+ };
191
+ }
192
+ // Descending order: the buffered items are the newest, so they come first, reversed.
193
+ const reversedBuffer = [...buffered].reverse();
194
+ const fromBuffer = limit === undefined ? reversedBuffer.slice(offset) : reversedBuffer.slice(offset, offset + limit);
195
+ const needed = limit === undefined ? Infinity : limit - fromBuffer.length;
196
+ if (needed <= 0) {
197
+ // The whole window is served from the buffer; only the real total is missing.
198
+ const { itemCount } = await this.backend.getMetadata();
199
+ return {
200
+ items: fromBuffer,
201
+ total: itemCount + buffered.length,
202
+ offset,
203
+ limit: limit,
204
+ count: fromBuffer.length,
205
+ desc,
206
+ };
207
+ }
208
+ const realPage = await this.backend.getData({
209
+ ...options,
210
+ offset: Math.max(0, offset - buffered.length),
211
+ ...(limit === undefined ? {} : { limit: needed }),
212
+ });
213
+ return {
214
+ items: [...fromBuffer, ...realPage.items],
215
+ total: realPage.total + buffered.length,
216
+ offset,
217
+ limit: limit ?? realPage.limit,
218
+ count: fromBuffer.length + realPage.items.length,
219
+ desc,
220
+ };
221
+ }
222
+ /** The active transaction's buffered writes to this dataset, derived from its journal. */
223
+ bufferedJournalEntries() {
224
+ const transaction = activeStorageTransaction();
225
+ return transaction?.journal.filter((entry) => entry.type === 'dataset' && entry.participant === this);
226
+ }
227
+ /** @internal */
228
+ async commitJournalEntries(entries) {
229
+ const items = [];
230
+ for (const entry of entries) {
231
+ if (entry.type === 'dataset') {
232
+ items.push(...entry.items);
233
+ }
234
+ }
235
+ // One backend call with all journaled items, in order - as close to atomic as the backend allows.
236
+ // Straight to the backend: the items were validated and snapshotted at write time.
237
+ if (items.length > 0) {
238
+ this.#statsTracker.add('writeCount');
239
+ await this.backend.pushData(items);
240
+ }
241
+ }
151
242
  /**
152
243
  * Returns all the data from the dataset. This will iterate through the whole dataset
153
244
  * via the `listItems()` client method, which gives you only paginated results.
154
245
  */
155
246
  async export(options = {}) {
156
- checkStorageAccess();
247
+ tryCancel();
157
248
  const items = [];
158
249
  for await (const page of this.fetchPages(options)) {
159
250
  items.push(...page.items);
@@ -219,7 +310,7 @@ export class Dataset {
219
310
  * @param [options] An optional options object where you can provide the dataset and target KVS name.
220
311
  */
221
312
  static async exportToJSON(key, options) {
222
- checkStorageAccess();
313
+ tryCancel();
223
314
  const dataset = await this.open(options?.fromDataset);
224
315
  await dataset.exportToJSON(key, options);
225
316
  }
@@ -230,7 +321,7 @@ export class Dataset {
230
321
  * @param [options] An optional options object where you can provide the dataset and target KVS name.
231
322
  */
232
323
  static async exportToCSV(key, options) {
233
- checkStorageAccess();
324
+ tryCancel();
234
325
  const dataset = await this.open(options?.fromDataset);
235
326
  await dataset.exportToCSV(key, options);
236
327
  }
@@ -252,8 +343,17 @@ export class Dataset {
252
343
  * @throws If the underlying storage no longer exists (e.g. it was deleted externally).
253
344
  */
254
345
  async getInfo() {
255
- checkStorageAccess();
256
- return this.backend.getMetadata();
346
+ const buffered = this.bufferedJournalEntries();
347
+ const metadata = await this.backend.getMetadata();
348
+ if (buffered?.length) {
349
+ const lastWriteAt = buffered[buffered.length - 1].recordedAt;
350
+ return {
351
+ ...metadata,
352
+ itemCount: metadata.itemCount + buffered.reduce((sum, entry) => sum + entry.items.length, 0),
353
+ modifiedAt: metadata.modifiedAt > lastWriteAt ? metadata.modifiedAt : lastWriteAt,
354
+ };
355
+ }
356
+ return metadata;
257
357
  }
258
358
  /**
259
359
  * Iterates over dataset items, yielding each in turn to an `iteratee` function.
@@ -276,7 +376,7 @@ export class Dataset {
276
376
  * @default 0
277
377
  */
278
378
  async forEach(iteratee, options = {}, index = 0) {
279
- checkStorageAccess();
379
+ tryCancel();
280
380
  if (!options.offset)
281
381
  options.offset = 0;
282
382
  if (options.format && options.format !== 'json')
@@ -303,7 +403,7 @@ export class Dataset {
303
403
  * @param [options] All `map()` parameters.
304
404
  */
305
405
  async map(iteratee, options = {}) {
306
- checkStorageAccess();
406
+ tryCancel();
307
407
  const result = [];
308
408
  await this.forEach(async (item, index) => {
309
409
  const res = await iteratee(item, index);
@@ -312,7 +412,7 @@ export class Dataset {
312
412
  return result;
313
413
  }
314
414
  async reduce(iteratee, memo, options = {}) {
315
- checkStorageAccess();
415
+ tryCancel();
316
416
  let currentMemo = memo;
317
417
  const wrappedFunc = async (item, index) => {
318
418
  if (index === 0 && currentMemo === undefined) {
@@ -344,8 +444,7 @@ export class Dataset {
344
444
  const fetchLimit = totalLimit !== undefined ? Math.min(pageSize, totalLimit - yielded) : pageSize;
345
445
  if (fetchLimit <= 0)
346
446
  break;
347
- this.statsTracker.add('readCount');
348
- const page = await this.backend.getData({ ...options, offset, limit: fetchLimit });
447
+ const page = await this.readPage({ ...options, offset, limit: fetchLimit });
349
448
  yield page;
350
449
  yielded += page.items.length;
351
450
  if (page.items.length < fetchLimit || offset + page.items.length >= page.total)
@@ -377,7 +476,7 @@ export class Dataset {
377
476
  * @param options Options for the iteration.
378
477
  */
379
478
  values(options = {}) {
380
- checkStorageAccess();
479
+ tryCancel();
381
480
  return createDualIterable({
382
481
  createPages: () => this.fetchPages(options),
383
482
  extractItems: (page) => page.items,
@@ -407,7 +506,7 @@ export class Dataset {
407
506
  * @param options Options for the iteration.
408
507
  */
409
508
  entries(options = {}) {
410
- checkStorageAccess();
509
+ tryCancel();
411
510
  return createDualIterable({
412
511
  createPages: () => this.fetchEntryPages(options),
413
512
  extractItems: (page) => page.items,
@@ -433,7 +532,7 @@ export class Dataset {
433
532
  * depending on the mode of operation.
434
533
  */
435
534
  async drop() {
436
- checkStorageAccess();
535
+ rejectOperationInTransaction('Dataset.drop()');
437
536
  await this.backend.drop();
438
537
  serviceLocator.getStorageInstanceManager().removeFromCache(this);
439
538
  }
@@ -453,7 +552,7 @@ export class Dataset {
453
552
  * @param [options] Storage manager options.
454
553
  */
455
554
  static async open(identifier, options = {}) {
456
- checkStorageAccess();
555
+ tryCancel();
457
556
  ow(options, ow.object.exactShape({
458
557
  configuration: ow.optional.object.instanceOf(Configuration),
459
558
  storageBackend: ow.optional.object,
@@ -8,6 +8,6 @@ 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';
package/storages/index.js CHANGED
@@ -6,6 +6,6 @@ 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';
@@ -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.