@crawlee/core 4.0.0-beta.99 → 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 (133) hide show
  1. package/README.md +1 -1
  2. package/configuration.d.ts +16 -47
  3. package/configuration.js +13 -25
  4. package/debug.js +4 -4
  5. package/errors.d.ts +28 -38
  6. package/errors.js +33 -47
  7. package/events/event_manager.d.ts +2 -2
  8. package/events/event_manager.js +7 -6
  9. package/events/index.d.ts +1 -0
  10. package/events/local_event_manager.d.ts +1 -8
  11. package/events/local_event_manager.js +13 -13
  12. package/events/system_info.d.ts +38 -0
  13. package/index.d.ts +2 -8
  14. package/index.js +4 -8
  15. package/internal.d.ts +8 -0
  16. package/internal.js +9 -0
  17. package/log.d.ts +10 -11
  18. package/log.js +52 -20
  19. package/memory-storage/memory-storage.d.ts +15 -18
  20. package/memory-storage/memory-storage.js +80 -58
  21. package/memory-storage/resource-clients/dataset.d.ts +1 -6
  22. package/memory-storage/resource-clients/dataset.js +23 -31
  23. package/memory-storage/resource-clients/key-value-store.d.ts +1 -10
  24. package/memory-storage/resource-clients/key-value-store.js +43 -67
  25. package/memory-storage/resource-clients/request-queue.d.ts +1 -42
  26. package/memory-storage/resource-clients/request-queue.js +109 -117
  27. package/owned_or_injected.d.ts +1 -3
  28. package/owned_or_injected.js +17 -17
  29. package/package.json +17 -20
  30. package/proxy_configuration.d.ts +21 -26
  31. package/proxy_configuration.js +35 -25
  32. package/recoverable_state.d.ts +104 -47
  33. package/recoverable_state.js +199 -74
  34. package/request.d.ts +20 -107
  35. package/request.js +78 -244
  36. package/serialization.js +17 -16
  37. package/service_locator.d.ts +22 -10
  38. package/service_locator.js +59 -48
  39. package/storages/batched_adds.d.ts +37 -0
  40. package/storages/batched_adds.js +73 -0
  41. package/storages/dataset.d.ts +13 -8
  42. package/storages/dataset.js +149 -40
  43. package/storages/index.d.ts +4 -4
  44. package/storages/index.js +2 -4
  45. package/storages/key_value_store.d.ts +16 -35
  46. package/storages/key_value_store.js +223 -110
  47. package/storages/key_value_store_codec.js +6 -11
  48. package/storages/request_dedup_cache.d.ts +1 -4
  49. package/storages/request_dedup_cache.js +15 -15
  50. package/storages/request_list.d.ts +9 -104
  51. package/storages/request_list.js +236 -233
  52. package/storages/request_loader.d.ts +49 -18
  53. package/storages/request_loader.js +36 -1
  54. package/storages/request_manager.d.ts +86 -0
  55. package/storages/request_manager_tandem.d.ts +14 -38
  56. package/storages/request_manager_tandem.js +67 -64
  57. package/storages/request_queue.d.ts +23 -50
  58. package/storages/request_queue.js +371 -226
  59. package/storages/storage_instance_manager.d.ts +2 -4
  60. package/storages/storage_instance_manager.js +21 -21
  61. package/storages/storage_stats.d.ts +1 -1
  62. package/storages/storage_stats.js +4 -4
  63. package/storages/transaction.d.ts +270 -0
  64. package/storages/transaction.js +296 -0
  65. package/storages/utils.d.ts +6 -3
  66. package/storages/utils.js +11 -2
  67. package/system-info/runtime.js +7 -7
  68. package/url.d.ts +9 -0
  69. package/url.js +11 -0
  70. package/validators.d.ts +23 -25
  71. package/validators.js +14 -25
  72. package/autoscaling/autoscaled_pool.d.ts +0 -213
  73. package/autoscaling/autoscaled_pool.js +0 -378
  74. package/autoscaling/client_load_signal.d.ts +0 -59
  75. package/autoscaling/client_load_signal.js +0 -73
  76. package/autoscaling/concurrency_system.d.ts +0 -283
  77. package/autoscaling/concurrency_system.js +0 -350
  78. package/autoscaling/cpu_load_signal.d.ts +0 -44
  79. package/autoscaling/cpu_load_signal.js +0 -46
  80. package/autoscaling/event_loop_load_signal.d.ts +0 -54
  81. package/autoscaling/event_loop_load_signal.js +0 -60
  82. package/autoscaling/index.d.ts +0 -9
  83. package/autoscaling/index.js +0 -9
  84. package/autoscaling/load_signal.d.ts +0 -99
  85. package/autoscaling/load_signal.js +0 -103
  86. package/autoscaling/memory_load_signal.d.ts +0 -56
  87. package/autoscaling/memory_load_signal.js +0 -106
  88. package/autoscaling/snapshotter.d.ts +0 -87
  89. package/autoscaling/snapshotter.js +0 -67
  90. package/autoscaling/system_status.d.ts +0 -161
  91. package/autoscaling/system_status.js +0 -139
  92. package/autoscaling/weighted_avg.d.ts +0 -5
  93. package/autoscaling/weighted_avg.js +0 -14
  94. package/cookie_utils.d.ts +0 -44
  95. package/cookie_utils.js +0 -122
  96. package/crawlers/context_pipeline.d.ts +0 -70
  97. package/crawlers/context_pipeline.js +0 -122
  98. package/crawlers/crawler_commons.d.ts +0 -257
  99. package/crawlers/crawler_commons.js +0 -107
  100. package/crawlers/error_snapshotter.d.ts +0 -59
  101. package/crawlers/error_snapshotter.js +0 -117
  102. package/crawlers/error_tracker.d.ts +0 -54
  103. package/crawlers/error_tracker.js +0 -308
  104. package/crawlers/index.d.ts +0 -5
  105. package/crawlers/index.js +0 -5
  106. package/crawlers/internals/types.d.ts +0 -7
  107. package/crawlers/statistics.d.ts +0 -209
  108. package/crawlers/statistics.js +0 -350
  109. package/enqueue_links/enqueue_links.d.ts +0 -264
  110. package/enqueue_links/enqueue_links.js +0 -271
  111. package/enqueue_links/index.d.ts +0 -2
  112. package/enqueue_links/index.js +0 -2
  113. package/enqueue_links/shared.d.ts +0 -83
  114. package/enqueue_links/shared.js +0 -221
  115. package/router.d.ts +0 -309
  116. package/router.js +0 -309
  117. package/session_pool/consts.d.ts +0 -3
  118. package/session_pool/consts.js +0 -3
  119. package/session_pool/errors.d.ts +0 -7
  120. package/session_pool/errors.js +0 -11
  121. package/session_pool/fingerprint.d.ts +0 -9
  122. package/session_pool/fingerprint.js +0 -30
  123. package/session_pool/index.d.ts +0 -4
  124. package/session_pool/index.js +0 -4
  125. package/session_pool/session.d.ts +0 -161
  126. package/session_pool/session.js +0 -218
  127. package/session_pool/session_pool.d.ts +0 -246
  128. package/session_pool/session_pool.js +0 -386
  129. package/storages/access_checking.d.ts +0 -12
  130. package/storages/access_checking.js +0 -17
  131. package/storages/sitemap_request_loader.d.ts +0 -249
  132. package/storages/sitemap_request_loader.js +0 -432
  133. /package/{crawlers/internals/types.js → events/system_info.js} +0 -0
@@ -1,8 +1,10 @@
1
- import ow, { ArgumentError } from 'ow';
1
+ import { z } from 'zod';
2
2
  import { KEY_VALUE_STORE_KEY_REGEX } from '@apify/consts';
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 { parseArgument, schemas, validators } from '../validators.js';
7
+ import { activeStorageTransaction, operationRejectedInTransaction, rejectOperationInTransaction, snapshotValue, withDirectStorageAccess, } from './transaction.js';
6
8
  import { parseValue, serializeValue } from './key_value_store_codec.js';
7
9
  import { StorageStatsTracker } from './storage_stats.js';
8
10
  import { resolveStorageIdentifier } from './storage_instance_manager.js';
@@ -10,6 +12,20 @@ import { createDualIterable, purgeDefaultStorages } from './utils.js';
10
12
  import { isBuffer, isStream } from '../byte_utils.js';
11
13
  /** @internal */
12
14
  const KVS_KEYS_DEFAULT_LIMIT = 1000;
15
+ const keySchema = z.string().nonempty();
16
+ const setValueKeySchema = z.string().nonempty().regex(KEY_VALUE_STORE_KEY_REGEX, {
17
+ message: `The "key" argument must be at most 256 characters long and only contain the following characters: a-zA-Z0-9!-_.'()`,
18
+ });
19
+ const recordOptionsSchema = z.strictObject({
20
+ contentType: z.string().nonempty().optional(),
21
+ });
22
+ const iteratorOptionsSchema = z.strictObject({
23
+ prefix: z.string().optional(),
24
+ });
25
+ const openOptionsSchema = z.strictObject({
26
+ configuration: z.instanceof(Configuration).optional(),
27
+ storageBackend: validators.storageBackend.optional(),
28
+ });
13
29
  /**
14
30
  * The `KeyValueStore` class represents a key-value store, a simple data storage that is used
15
31
  * for saving and reading data records or files. Each data record is
@@ -26,8 +42,6 @@ const KVS_KEYS_DEFAULT_LIMIT = 1000;
26
42
  * To access the default key-value store directly, you can use the
27
43
  * {@link KeyValueStore.getValue} and {@link KeyValueStore.setValue} convenience functions.
28
44
  *
29
- * To access the input, you can also use the {@link KeyValueStore.getInput} convenience function.
30
- *
31
45
  * `KeyValueStore` stores its data on a local disk.
32
46
  *
33
47
  * If the `CRAWLEE_STORAGE_DIR` environment variable is set, the data is stored in
@@ -35,15 +49,14 @@ const KVS_KEYS_DEFAULT_LIMIT = 1000;
35
49
  * ```
36
50
  * {CRAWLEE_STORAGE_DIR}/key_value_stores/{STORE_ID}/{INDEX}.{EXT}
37
51
  * ```
38
- * Note that `{STORE_ID}` is the name or ID of the key-value store. The default key-value store has ID: `default`,
39
- * unless you override it by setting the `CRAWLEE_DEFAULT_KEY_VALUE_STORE_ID` environment variable.
52
+ * Note that `{STORE_ID}` is the name or ID of the key-value store. The default key-value store has ID: `default`.
40
53
  * The `{KEY}` is the key of the record and `{EXT}` corresponds to the MIME content type of the data value.
41
54
  *
42
55
  * **Example usage:**
43
56
  *
44
57
  * ```javascript
45
58
  * // Get crawler input from the default key-value store.
46
- * const input = await KeyValueStore.getInput();
59
+ * const input = await KeyValueStore.getValue('INPUT');
47
60
  * // Get some value from the default key-value store.
48
61
  * const otherValue = await KeyValueStore.getValue('my-key');
49
62
  *
@@ -71,10 +84,10 @@ export class KeyValueStore {
71
84
  id;
72
85
  name;
73
86
  backend;
74
- persistStateEventStarted = false;
87
+ #persistStateEventStarted = false;
75
88
  /** Cache for persistent (auto-saved) values. When we try to set such value, the cache will be updated automatically. */
76
- cache = new Map();
77
- statsTracker = new StorageStatsTracker({
89
+ #cache = new Map();
90
+ #statsTracker = new StorageStatsTracker({
78
91
  readCount: 0,
79
92
  writeCount: 0,
80
93
  deleteCount: 0,
@@ -94,7 +107,7 @@ export class KeyValueStore {
94
107
  * list operations issued to the underlying storage backend). Counted per backend call.
95
108
  */
96
109
  get stats() {
97
- return this.statsTracker.current;
110
+ return this.#statsTracker.current;
98
111
  }
99
112
  /**
100
113
  * Gets a value from the key-value store.
@@ -129,10 +142,9 @@ export class KeyValueStore {
129
142
  * on the MIME content type of the record, or `null` if the key is missing from the store.
130
143
  */
131
144
  async getValue(key, defaultValue) {
132
- checkStorageAccess();
133
- ow(key, ow.string.nonEmpty);
134
- this.statsTracker.add('readCount');
135
- const record = await this.backend.getValue(key);
145
+ tryCancel();
146
+ parseArgument(key, keySchema);
147
+ const record = await this.#readRecord(key);
136
148
  // A missing record falls back to the default; a record that parses to a falsy value (including
137
149
  // a stored literal `null`) is returned verbatim, so callers can tell "stored null" from "absent".
138
150
  if (!record) {
@@ -141,6 +153,52 @@ export class KeyValueStore {
141
153
  // Storage backends are byte transports — the value is raw bytes; the frontend parses it here.
142
154
  return parseValue(record.value, record.contentType ?? null);
143
155
  }
156
+ /**
157
+ * The active transaction's last buffered write per key for this store, derived from its journal.
158
+ * An entry with a `null` value is a tombstone (an in-transaction deletion).
159
+ */
160
+ #bufferedJournalEntries() {
161
+ const transaction = activeStorageTransaction();
162
+ if (!transaction)
163
+ return undefined;
164
+ const lastWritePerKey = new Map();
165
+ for (const entry of transaction.journal) {
166
+ if (entry.type === 'keyValueStore' && entry.participant === this) {
167
+ lastWritePerKey.set(entry.key, entry);
168
+ }
169
+ }
170
+ return lastWritePerKey;
171
+ }
172
+ /**
173
+ * The single transaction-aware record read shared by `getValue`, `getRecord`, `recordExists` and the
174
+ * listing paths: buffered key → serialized through the standard codec (same fidelity as a real
175
+ * round-trip); tombstoned key → `null`; otherwise the backend.
176
+ *
177
+ * The per-key buffered lookup requires the whole journal to be reduced to a last-write-per-key map,
178
+ * which is O(journal). Single-record callers let it default (rebuilt per call); the listing paths,
179
+ * which read many keys, pass a map built once so the read stays O(1) per key instead of O(journal).
180
+ */
181
+ async #readRecord(key, buffered = this.#bufferedJournalEntries()) {
182
+ const entry = buffered?.get(key);
183
+ if (entry) {
184
+ if (entry.value === null) {
185
+ return null;
186
+ }
187
+ const serialized = serializeValue(entry.value, entry.options?.contentType);
188
+ return {
189
+ value: normalizeSerializedValue(serialized.value),
190
+ contentType: serialized.contentType ?? null,
191
+ };
192
+ }
193
+ this.#statsTracker.add('readCount');
194
+ const record = await this.backend.getValue(key);
195
+ if (!record)
196
+ return null;
197
+ return {
198
+ value: record.value,
199
+ contentType: record.contentType ?? null,
200
+ };
201
+ }
144
202
  /**
145
203
  * Reads a record from the key-value store without parsing the value.
146
204
  *
@@ -168,16 +226,9 @@ export class KeyValueStore {
168
226
  * of the following characters: `a`-`z`, `A`-`Z`, `0`-`9` and `!-_.'()`
169
227
  */
170
228
  async getRecord(key) {
171
- checkStorageAccess();
172
- ow(key, ow.string.nonEmpty);
173
- this.statsTracker.add('readCount');
174
- const record = await this.backend.getValue(key);
175
- if (!record)
176
- return null;
177
- return {
178
- value: record.value,
179
- contentType: record.contentType ?? null,
180
- };
229
+ tryCancel();
230
+ parseArgument(key, keySchema);
231
+ return this.#readRecord(key);
181
232
  }
182
233
  /**
183
234
  * Tests whether a record with the given key exists in the key-value store without retrieving its value.
@@ -186,46 +237,55 @@ export class KeyValueStore {
186
237
  * @returns `true` if the record exists, `false` if it does not.
187
238
  */
188
239
  async recordExists(key) {
189
- checkStorageAccess();
190
- ow(key, ow.string.nonEmpty);
240
+ tryCancel();
241
+ parseArgument(key, keySchema);
242
+ const entry = this.#bufferedJournalEntries()?.get(key);
243
+ if (entry) {
244
+ return entry.value !== null;
245
+ }
191
246
  return this.backend.recordExists(key);
192
247
  }
193
248
  async getAutoSavedValue(key, defaultValue = {}) {
194
- checkStorageAccess();
195
- if (this.cache.has(key)) {
196
- return this.cache.get(key);
249
+ tryCancel();
250
+ if (this.#cache.has(key)) {
251
+ return this.#cache.get(key);
197
252
  }
198
- const value = await this.getValue(key, defaultValue);
253
+ // Auto-saved state is deliberately *not* transactional. The direct read bypasses any active
254
+ // transaction - a buffered value seeded into this shared cache would survive a rollback forever.
255
+ const value = await withDirectStorageAccess(async () => this.getValue(key, defaultValue));
199
256
  // The await above could have run in parallel with another call to this function. If the other call finished more quickly,
200
257
  // the value will in cache at this point, and returning the new fetched value would introduce two different instances of
201
258
  // the auto-saved object, and only the latter one would be persisted.
202
259
  // Therefore we re-check the cache here, and if such race condition happened, we drop the fetched value and return the cached one.
203
- if (this.cache.has(key)) {
204
- return this.cache.get(key);
260
+ if (this.#cache.has(key)) {
261
+ return this.#cache.get(key);
205
262
  }
206
- this.cache.set(key, value);
207
- this.ensurePersistStateEvent();
263
+ this.#cache.set(key, value);
264
+ this.#ensurePersistStateEvent();
208
265
  return value;
209
266
  }
210
- ensurePersistStateEvent() {
211
- if (this.persistStateEventStarted) {
267
+ #ensurePersistStateEvent() {
268
+ if (this.#persistStateEventStarted) {
212
269
  return;
213
270
  }
214
271
  serviceLocator.getEventManager().on('persistState', async () => {
215
272
  const promises = [];
216
- for (const [key, value] of this.cache) {
273
+ for (const [key, value] of this.#cache) {
217
274
  promises.push(this.setValue(key, value).catch((error) => serviceLocator.getLogger().warning(`Failed to persist the state value to ${key}`, { error })));
218
275
  }
219
276
  await Promise.all(promises);
220
277
  });
221
- this.persistStateEventStarted = true;
278
+ this.#persistStateEventStarted = true;
222
279
  }
223
- async *fetchKeyValuePages(options, mapRecord) {
224
- for await (const page of this.fetchKeyPages(options)) {
280
+ async *#fetchKeyValuePages(options, mapRecord) {
281
+ // Reduce the journal once for the whole iteration, not once per key inside `readRecord`.
282
+ const buffered = this.#bufferedJournalEntries();
283
+ for await (const page of this.#fetchKeyPages(options, buffered)) {
225
284
  const results = [];
226
285
  for (const item of page) {
227
- this.statsTracker.add('readCount');
228
- const record = await this.backend.getValue(item.key);
286
+ // The shared transaction-aware read, so a key that exists only in the transaction resolves
287
+ // here instead of being dropped (`values()` would disagree with `keys()` on length).
288
+ const record = await this.#readRecord(item.key, buffered);
229
289
  if (record) {
230
290
  const parsed = parseValue(record.value, record.contentType ?? null);
231
291
  results.push(mapRecord(item.key, parsed));
@@ -234,18 +294,37 @@ export class KeyValueStore {
234
294
  yield results;
235
295
  }
236
296
  }
237
- async *fetchKeyPages(options, limit = KVS_KEYS_DEFAULT_LIMIT) {
297
+ async *#fetchKeyPages(options, buffered = this.#bufferedJournalEntries(), limit = KVS_KEYS_DEFAULT_LIMIT) {
298
+ // Buffered keys are emitted first, then the real pages with any buffered (or tombstoned) key
299
+ // skipped - a merge-join is not an option, since `listKeys` promises no sort order.
300
+ const shadowedKeys = new Set();
301
+ if (buffered) {
302
+ const bufferedItems = [];
303
+ for (const [key, entry] of buffered) {
304
+ shadowedKeys.add(key);
305
+ if (entry.value === null)
306
+ continue;
307
+ if (options.prefix !== undefined && !key.startsWith(options.prefix))
308
+ continue;
309
+ bufferedItems.push(bufferedKeyItemData(key, entry));
310
+ }
311
+ if (bufferedItems.length > 0) {
312
+ bufferedItems.sort((a, b) => (a.key < b.key ? -1 : 1));
313
+ yield bufferedItems;
314
+ }
315
+ }
238
316
  let exclusiveStartKey;
239
317
  while (true) {
240
- this.statsTracker.add('listCount');
318
+ this.#statsTracker.add('listCount');
241
319
  const { items, isTruncated, nextExclusiveStartKey } = await this.backend.listKeys({
242
320
  ...options,
243
321
  exclusiveStartKey,
244
322
  limit,
245
323
  });
246
- yield items;
324
+ yield shadowedKeys.size > 0 ? items.filter((item) => !shadowedKeys.has(item.key)) : items;
247
325
  if (!isTruncated)
248
326
  break;
327
+ // Paginate from the raw backend cursor - it may reject a key it did not hand out.
249
328
  exclusiveStartKey = nextExclusiveStartKey;
250
329
  }
251
330
  }
@@ -293,62 +372,104 @@ export class KeyValueStore {
293
372
  * @param [options] Record options.
294
373
  */
295
374
  async setValue(key, value, options = {}) {
296
- checkStorageAccess();
297
- ow(key, 'key', ow.string.nonEmpty);
298
- ow(key, ow.string.validate((k) => ({
299
- validator: ow.isValid(k, ow.string.matches(KEY_VALUE_STORE_KEY_REGEX)),
300
- message: `The "key" argument "${key}" must be at most 256 characters long and only contain the following characters: a-zA-Z0-9!-_.'()`,
301
- })));
375
+ const transaction = activeStorageTransaction();
376
+ parseArgument(key, setValueKeySchema);
302
377
  if (options.contentType && !(typeof value === 'string' || isBuffer(value) || isStream(value))) {
303
- throw new ArgumentError('The "value" parameter must be a String, Buffer, ArrayBuffer, TypedArray, or Stream when "options.contentType" is specified.', this.setValue);
378
+ throw new Error('The "value" parameter must be a String, Buffer, ArrayBuffer, TypedArray, or Stream when "options.contentType" is specified.');
379
+ }
380
+ // The parse result is a fresh copy, so we never update what user passed.
381
+ const optionsCopy = parseArgument(options, recordOptionsSchema);
382
+ // The whole transaction branch sits *above* the auto-saved cache update below, so a buffered
383
+ // write touches nothing outside the journal. That cache is shared, process-lifetime frontend
384
+ // state, so mutating it here would survive a rollback and later be persisted by `persistState`.
385
+ // The commit replay re-enters this method with no active transaction and updates it then.
386
+ if (transaction) {
387
+ if (isStream(value)) {
388
+ // A stream cannot serve both a read-your-own-writes read and the commit replay. The
389
+ // transaction is known-active here, so throw directly rather than via the conditional guard.
390
+ throw operationRejectedInTransaction(`KeyValueStore.setValue() with a stream value (key "${key}")`, 'a stream can only be consumed once, so it cannot be buffered until commit.');
391
+ }
392
+ // Validation only, result discarded: the journal snapshot (`structuredClone`) accepts values
393
+ // JSON cannot, which would otherwise only throw at a later read or at commit.
394
+ if (value !== null) {
395
+ serializeValue(value, optionsCopy.contentType);
396
+ }
397
+ // One snapshot serves both the reads and the commit replay; `null` is a tombstone.
398
+ transaction.recordJournalEntry({
399
+ type: 'keyValueStore',
400
+ participant: this,
401
+ storageId: this.id,
402
+ key,
403
+ value: value === null ? null : snapshotValue(value),
404
+ options: optionsCopy,
405
+ });
406
+ return;
304
407
  }
305
- ow(options, ow.object.exactShape({
306
- contentType: ow.optional.string.nonEmpty,
307
- }));
308
- // Make copy of options, don't update what user passed.
309
- const optionsCopy = { ...options };
310
408
  // If we try to set the value of a cached state to a different reference, we need to update the cache accordingly.
311
- const cachedValue = this.cache.get(key);
409
+ const cachedValue = this.#cache.get(key);
312
410
  if (cachedValue && cachedValue !== value) {
313
411
  if (value === null) {
314
412
  // Cached state can be only object, so a propagation of `null` means removing all its properties.
315
- Object.keys(cachedValue).forEach((k) => this.cache.delete(k));
413
+ Object.keys(cachedValue).forEach((k) => this.#cache.delete(k));
316
414
  }
317
415
  else if (typeof value === 'object') {
318
416
  // We need to remove the keys that are no longer present in the new value.
319
417
  Object.keys(cachedValue)
320
418
  .filter((k) => !(k in value))
321
- .forEach((k) => this.cache.delete(k));
419
+ .forEach((k) => this.#cache.delete(k));
322
420
  // And update the existing ones + add new ones.
323
421
  Object.assign(cachedValue, value);
324
422
  }
325
423
  }
326
424
  // In this case delete the record.
327
425
  if (value === null) {
328
- this.statsTracker.add('deleteCount');
426
+ this.#statsTracker.add('deleteCount');
329
427
  return this.backend.deleteValue(key);
330
428
  }
331
429
  const serialized = serializeValue(value, optionsCopy.contentType);
332
- this.statsTracker.add('writeCount');
430
+ this.#statsTracker.add('writeCount');
333
431
  return this.backend.setValue({
334
432
  key,
335
433
  value: serialized.value,
336
434
  contentType: serialized.contentType,
337
435
  });
338
436
  }
437
+ /** @internal */
438
+ async commitJournalEntries(entries) {
439
+ // One `setValue` per key, last write wins - idempotent under retry.
440
+ const lastWritePerKey = new Map();
441
+ for (const entry of entries) {
442
+ if (entry.type === 'keyValueStore') {
443
+ lastWritePerKey.set(entry.key, { value: entry.value, options: entry.options });
444
+ }
445
+ }
446
+ for (const [key, { value, options }] of lastWritePerKey) {
447
+ await this.setValue(key, value, options);
448
+ }
449
+ }
339
450
  /**
340
451
  * Removes the key-value store either from the Apify cloud storage or from the local directory,
341
452
  * depending on the mode of operation.
342
453
  */
343
454
  async drop() {
344
- checkStorageAccess();
455
+ rejectOperationInTransaction('KeyValueStore.drop()');
345
456
  await this.backend.drop();
346
457
  serviceLocator.getStorageInstanceManager().removeFromCache(this);
347
458
  }
459
+ /**
460
+ * Removes all records from the store but keeps the store itself, along with its
461
+ * {@link KeyValueStore.id|`id`} and {@link KeyValueStore.name|`name`}.
462
+ */
463
+ async purge() {
464
+ rejectOperationInTransaction('KeyValueStore.purge()');
465
+ await this.backend.purge();
466
+ // The auto-saved values this cache holds are no longer in the store.
467
+ this.#cache.clear();
468
+ }
348
469
  /** @internal */
349
470
  clearCache() {
350
- checkStorageAccess();
351
- this.cache.clear();
471
+ rejectOperationInTransaction('KeyValueStore.clearCache()');
472
+ this.#cache.clear();
352
473
  }
353
474
  /**
354
475
  * Iterates over key-value store keys, yielding each in turn to an `iteratee` function.
@@ -372,13 +493,11 @@ export class KeyValueStore {
372
493
  * @param [options] All `forEachKey()` parameters.
373
494
  */
374
495
  async forEachKey(iteratee, options = {}) {
375
- checkStorageAccess();
376
- ow(iteratee, ow.function);
377
- ow(options, ow.object.exactShape({
378
- prefix: ow.optional.string,
379
- }));
496
+ tryCancel();
497
+ parseArgument(iteratee, schemas.anyFunction);
498
+ const parsedOptions = parseArgument(options, iteratorOptionsSchema);
380
499
  let index = 0;
381
- for await (const page of this.fetchKeyPages(options)) {
500
+ for await (const page of this.#fetchKeyPages(parsedOptions)) {
382
501
  for (const item of page) {
383
502
  await iteratee(item.key, index++, { size: item.size });
384
503
  }
@@ -408,9 +527,9 @@ export class KeyValueStore {
408
527
  * @param options Options for the iteration.
409
528
  */
410
529
  keys(options = {}) {
411
- checkStorageAccess();
530
+ tryCancel();
412
531
  return createDualIterable({
413
- createPages: () => this.fetchKeyPages(options),
532
+ createPages: () => this.#fetchKeyPages(options),
414
533
  extractItems: (page) => page.map((item) => item.key),
415
534
  });
416
535
  }
@@ -438,9 +557,9 @@ export class KeyValueStore {
438
557
  * @param options Options for the iteration.
439
558
  */
440
559
  values(options = {}) {
441
- checkStorageAccess();
560
+ tryCancel();
442
561
  return createDualIterable({
443
- createPages: () => this.fetchKeyValuePages(options, (_key, value) => value),
562
+ createPages: () => this.#fetchKeyValuePages(options, (_key, value) => value),
444
563
  extractItems: (page) => page,
445
564
  });
446
565
  }
@@ -468,9 +587,9 @@ export class KeyValueStore {
468
587
  * @param options Options for the iteration.
469
588
  */
470
589
  entries(options = {}) {
471
- checkStorageAccess();
590
+ tryCancel();
472
591
  return createDualIterable({
473
- createPages: () => this.fetchKeyValuePages(options, (key, value) => [key, value]),
592
+ createPages: () => this.#fetchKeyValuePages(options, (key, value) => [key, value]),
474
593
  extractItems: (page) => page,
475
594
  });
476
595
  }
@@ -492,7 +611,9 @@ export class KeyValueStore {
492
611
  /**
493
612
  * Returns a file URL for the given key.
494
613
  *
495
- * If the record does not exist or has no associated file path (i.e., it is not stored as a file), returns `undefined`.
614
+ * The URL is derived from the key, so it is also returned for a record that does not exist (yet) —
615
+ * including one written earlier in an uncommitted storage transaction. Returns `undefined` only when
616
+ * the storage has no file URLs at all (e.g. the in-memory storage).
496
617
  *
497
618
  * @param key The key of the record to generate the public URL for.
498
619
  */
@@ -515,14 +636,11 @@ export class KeyValueStore {
515
636
  * @param [options] Storage manager options.
516
637
  */
517
638
  static async open(identifier, options = {}) {
518
- checkStorageAccess();
519
- ow(options, ow.object.exactShape({
520
- configuration: ow.optional.object.instanceOf(Configuration),
521
- storageBackend: ow.optional.object,
522
- }));
523
- options.configuration ??= Configuration.getGlobalConfiguration();
524
- const storageBackend = options.storageBackend ?? serviceLocator.getStorageBackend();
525
- await purgeDefaultStorages({ onlyPurgeOnce: true, storageBackend, configuration: options.configuration });
639
+ tryCancel();
640
+ const parsedOptions = parseArgument(options, openOptionsSchema);
641
+ const configuration = parsedOptions.configuration ?? Configuration.getGlobalConfiguration();
642
+ const storageBackend = parsedOptions.storageBackend ?? serviceLocator.getStorageBackend();
643
+ await purgeDefaultStorages({ onlyPurgeOnce: true, storageBackend, configuration });
526
644
  const resolved = await resolveStorageIdentifier(identifier, storageBackend, 'KeyValueStore');
527
645
  return serviceLocator.getStorageInstanceManager().openStorage(this, {
528
646
  ...resolved,
@@ -627,28 +745,23 @@ export class KeyValueStore {
627
745
  const store = await this.open();
628
746
  return store.setValue(key, value, options);
629
747
  }
630
- /**
631
- * Gets the crawler input value from the default {@link KeyValueStore} associated with the current crawler run.
632
- *
633
- * The input is read from the default {@link KeyValueStore} under the configured input key
634
- * (`CRAWLEE_INPUT_KEY`, default `INPUT`).
635
- *
636
- * Note that the `getInput()` function does not cache the value read from the key-value store.
637
- * If you need to use the input multiple times in your crawler,
638
- * it is far more efficient to read it once and store it locally.
639
- *
640
- * For more information, see {@link KeyValueStore.open}
641
- * and {@link KeyValueStore.getValue}.
642
- *
643
- * @returns
644
- * Returns a promise that resolves to an object, string
645
- * or [`Buffer`](https://nodejs.org/api/buffer.html), depending
646
- * on the MIME content type of the record, or `null`
647
- * if the record is missing.
648
- * @ignore
649
- */
650
- static async getInput() {
651
- const store = await this.open();
652
- return store.getValue(store.configuration.inputKey);
748
+ }
749
+ /** Normalizes a codec-serialized value into the `Buffer | ArrayBuffer` shape raw record reads promise. */
750
+ function normalizeSerializedValue(value) {
751
+ if (typeof value === 'string') {
752
+ return Buffer.from(value);
753
+ }
754
+ if (ArrayBuffer.isView(value)) {
755
+ return Buffer.isBuffer(value) ? value : Buffer.from(value.buffer, value.byteOffset, value.byteLength);
653
756
  }
757
+ return value;
758
+ }
759
+ /** Computes the key listing item (serialized byte size and content type) of a buffered entry. */
760
+ function bufferedKeyItemData(key, entry) {
761
+ const serialized = serializeValue(entry.value, entry.options?.contentType);
762
+ return {
763
+ key,
764
+ size: normalizeSerializedValue(serialized.value).byteLength,
765
+ contentType: serialized.contentType,
766
+ };
654
767
  }
@@ -1,4 +1,4 @@
1
- import contentTypeParser from 'content-type';
1
+ import { isTypeValid, parse as parseContentType } from 'content-type';
2
2
  import JSON5 from 'json5';
3
3
  import { jsonStringifyExtended } from '@apify/utilities';
4
4
  import { isBuffer, isStream } from '../byte_utils.js';
@@ -67,17 +67,12 @@ export function parseValue(body, contentTypeHeader) {
67
67
  // No content type at all → we have no basis for interpretation; hand back the raw value.
68
68
  if (contentTypeHeader === null)
69
69
  return body;
70
- let contentType;
71
- let charset;
72
- try {
73
- const result = contentTypeParser.parse(contentTypeHeader);
74
- contentType = result.type;
75
- charset = result.parameters.charset;
76
- }
77
- catch {
78
- // Unparseable header → keep the original value rather than a mangled string.
70
+ const result = parseContentType(contentTypeHeader);
71
+ // Unparseable header → keep the original value rather than a mangled string.
72
+ if (!isTypeValid(result.type))
79
73
  return body;
80
- }
74
+ const contentType = result.type;
75
+ const charset = result.parameters.charset;
81
76
  // If we can't successfully interpret it, we return the original value rather than mangling it.
82
77
  if (!areDataStringifiable(contentType, charset))
83
78
  return body;
@@ -12,12 +12,9 @@
12
12
  * @internal
13
13
  */
14
14
  export declare class RequestDeduplicationCache {
15
- private readonly size;
16
- private keys;
17
- private ids;
15
+ #private;
18
16
  constructor(size?: number);
19
17
  get(cacheKey: string): string | null;
20
18
  add(cacheKey: string, requestId: string): void;
21
19
  clear(): void;
22
- private indexOf;
23
20
  }
@@ -12,37 +12,37 @@
12
12
  * @internal
13
13
  */
14
14
  export class RequestDeduplicationCache {
15
- size;
16
- keys;
17
- ids;
15
+ #keys;
16
+ #ids;
17
+ #size;
18
18
  // The slot count is the same for every queue, so it's a fixed default rather than a per-consumer option.
19
19
  constructor(size = 1_000_000) {
20
- this.size = size;
21
- this.keys = new Array(size);
22
- this.ids = new Array(size);
20
+ this.#size = size;
21
+ this.#keys = new Array(size);
22
+ this.#ids = new Array(size);
23
23
  }
24
24
  get(cacheKey) {
25
- const index = this.indexOf(cacheKey);
26
- return this.keys[index] === cacheKey ? this.ids[index] : null;
25
+ const index = this.#indexOf(cacheKey);
26
+ return this.#keys[index] === cacheKey ? this.#ids[index] : null;
27
27
  }
28
28
  add(cacheKey, requestId) {
29
- const index = this.indexOf(cacheKey);
30
- this.keys[index] = cacheKey;
31
- this.ids[index] = requestId;
29
+ const index = this.#indexOf(cacheKey);
30
+ this.#keys[index] = cacheKey;
31
+ this.#ids[index] = requestId;
32
32
  }
33
33
  clear() {
34
- this.keys = new Array(this.size);
35
- this.ids = new Array(this.size);
34
+ this.#keys = new Array(this.#size);
35
+ this.#ids = new Array(this.#size);
36
36
  }
37
37
  // A cheap FNV-1a hash of the cache key — avoids pulling in a dedicated hashing dependency.
38
- indexOf(cacheKey) {
38
+ #indexOf(cacheKey) {
39
39
  /* eslint-disable no-bitwise */
40
40
  let hash = 0x811c9dc5;
41
41
  for (let i = 0; i < cacheKey.length; i++) {
42
42
  hash ^= cacheKey.charCodeAt(i);
43
43
  hash = Math.imul(hash, 0x01000193);
44
44
  }
45
- return (hash >>> 0) % this.size;
45
+ return (hash >>> 0) % this.#size;
46
46
  /* eslint-enable no-bitwise */
47
47
  }
48
48
  }