@crawlee/core 4.0.0-beta.23 → 4.0.0-beta.230

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 (265) hide show
  1. package/README.md +15 -15
  2. package/byte-utils.d.ts +17 -0
  3. package/byte-utils.js +42 -0
  4. package/configuration.d.ts +77 -235
  5. package/configuration.js +157 -227
  6. package/debug.d.ts +36 -0
  7. package/debug.js +70 -0
  8. package/errors.d.ts +45 -18
  9. package/errors.js +58 -27
  10. package/events/event-manager.d.ts +78 -0
  11. package/events/{event_manager.js → event-manager.js} +12 -13
  12. package/events/index.d.ts +3 -3
  13. package/events/index.js +2 -3
  14. package/events/local-event-manager.d.ts +32 -0
  15. package/events/local-event-manager.js +101 -0
  16. package/events/system-info.d.ts +38 -0
  17. package/events/system-info.js +1 -0
  18. package/index.d.ts +9 -12
  19. package/index.js +10 -11
  20. package/internal.d.ts +8 -0
  21. package/internal.js +9 -0
  22. package/iterables.d.ts +79 -0
  23. package/iterables.js +134 -0
  24. package/log.d.ts +77 -1
  25. package/log.js +134 -1
  26. package/memory-storage/consts.d.ts +4 -0
  27. package/memory-storage/consts.js +4 -0
  28. package/memory-storage/index.d.ts +1 -0
  29. package/memory-storage/index.js +1 -0
  30. package/memory-storage/memory-storage.d.ts +43 -0
  31. package/memory-storage/memory-storage.js +158 -0
  32. package/memory-storage/resource-clients/common/base-client.d.ts +4 -0
  33. package/memory-storage/resource-clients/common/base-client.js +6 -0
  34. package/memory-storage/resource-clients/dataset.d.ts +35 -0
  35. package/memory-storage/resource-clients/dataset.js +105 -0
  36. package/memory-storage/resource-clients/key-value-store.d.ts +54 -0
  37. package/memory-storage/resource-clients/key-value-store.js +179 -0
  38. package/memory-storage/resource-clients/request-queue.d.ts +55 -0
  39. package/memory-storage/resource-clients/request-queue.js +413 -0
  40. package/memory-storage/utils.d.ts +16 -0
  41. package/memory-storage/utils.js +41 -0
  42. package/owned-or-injected.d.ts +58 -0
  43. package/owned-or-injected.js +98 -0
  44. package/package.json +20 -20
  45. package/proxy-configuration.d.ts +118 -0
  46. package/proxy-configuration.js +141 -0
  47. package/recoverable-state.d.ts +177 -0
  48. package/recoverable-state.js +268 -0
  49. package/request.d.ts +48 -93
  50. package/request.js +112 -230
  51. package/serialization.d.ts +0 -1
  52. package/serialization.js +18 -18
  53. package/service-locator.d.ts +168 -0
  54. package/service-locator.js +255 -0
  55. package/storages/batched-adds.d.ts +37 -0
  56. package/storages/batched-adds.js +73 -0
  57. package/storages/dataset.d.ts +105 -58
  58. package/storages/dataset.js +292 -150
  59. package/storages/index.d.ts +11 -12
  60. package/storages/index.js +7 -12
  61. package/storages/key-value-store-codec.d.ts +32 -0
  62. package/storages/key-value-store-codec.js +108 -0
  63. package/storages/{key_value_store.d.ts → key-value-store.d.ts} +169 -72
  64. package/storages/key-value-store.js +767 -0
  65. package/storages/request-dedup-cache.d.ts +20 -0
  66. package/storages/request-dedup-cache.js +48 -0
  67. package/storages/{request_list.d.ts → request-list.d.ts} +33 -173
  68. package/storages/{request_list.js → request-list.js} +276 -255
  69. package/storages/request-loader.d.ts +127 -0
  70. package/storages/request-loader.js +36 -0
  71. package/storages/request-manager-tandem.d.ts +82 -0
  72. package/storages/request-manager-tandem.js +200 -0
  73. package/storages/request-manager.d.ts +119 -0
  74. package/storages/request-manager.js +1 -0
  75. package/storages/{request_provider.d.ts → request-queue.d.ts} +128 -159
  76. package/storages/request-queue.js +870 -0
  77. package/storages/storage-instance-manager.d.ts +86 -0
  78. package/storages/storage-instance-manager.js +256 -0
  79. package/storages/storage-stats.d.ts +48 -0
  80. package/storages/storage-stats.js +29 -0
  81. package/storages/transaction.d.ts +270 -0
  82. package/storages/transaction.js +296 -0
  83. package/storages/utils.d.ts +59 -11
  84. package/storages/utils.js +76 -16
  85. package/system-info/cpu-info.d.ts +67 -0
  86. package/system-info/cpu-info.js +216 -0
  87. package/system-info/memory-info.d.ts +31 -0
  88. package/system-info/memory-info.js +115 -0
  89. package/system-info/ps-tree.d.ts +17 -0
  90. package/system-info/ps-tree.js +144 -0
  91. package/system-info/runtime.d.ts +14 -0
  92. package/system-info/runtime.js +80 -0
  93. package/typedefs.d.ts +0 -6
  94. package/typedefs.js +0 -1
  95. package/url.d.ts +9 -0
  96. package/url.js +11 -0
  97. package/validators.d.ts +23 -18
  98. package/validators.js +14 -18
  99. package/autoscaling/autoscaled_pool.d.ts +0 -300
  100. package/autoscaling/autoscaled_pool.d.ts.map +0 -1
  101. package/autoscaling/autoscaled_pool.js +0 -533
  102. package/autoscaling/autoscaled_pool.js.map +0 -1
  103. package/autoscaling/index.d.ts +0 -4
  104. package/autoscaling/index.d.ts.map +0 -1
  105. package/autoscaling/index.js +0 -4
  106. package/autoscaling/index.js.map +0 -1
  107. package/autoscaling/snapshotter.d.ts +0 -189
  108. package/autoscaling/snapshotter.d.ts.map +0 -1
  109. package/autoscaling/snapshotter.js +0 -285
  110. package/autoscaling/snapshotter.js.map +0 -1
  111. package/autoscaling/system_status.d.ts +0 -181
  112. package/autoscaling/system_status.d.ts.map +0 -1
  113. package/autoscaling/system_status.js +0 -169
  114. package/autoscaling/system_status.js.map +0 -1
  115. package/configuration.d.ts.map +0 -1
  116. package/configuration.js.map +0 -1
  117. package/cookie_utils.d.ts +0 -43
  118. package/cookie_utils.d.ts.map +0 -1
  119. package/cookie_utils.js +0 -111
  120. package/cookie_utils.js.map +0 -1
  121. package/crawlers/context_pipeline.d.ts +0 -61
  122. package/crawlers/context_pipeline.d.ts.map +0 -1
  123. package/crawlers/context_pipeline.js +0 -99
  124. package/crawlers/context_pipeline.js.map +0 -1
  125. package/crawlers/crawler_commons.d.ts +0 -191
  126. package/crawlers/crawler_commons.d.ts.map +0 -1
  127. package/crawlers/crawler_commons.js +0 -108
  128. package/crawlers/crawler_commons.js.map +0 -1
  129. package/crawlers/crawler_utils.d.ts +0 -10
  130. package/crawlers/crawler_utils.d.ts.map +0 -1
  131. package/crawlers/crawler_utils.js +0 -12
  132. package/crawlers/crawler_utils.js.map +0 -1
  133. package/crawlers/error_snapshotter.d.ts +0 -60
  134. package/crawlers/error_snapshotter.d.ts.map +0 -1
  135. package/crawlers/error_snapshotter.js +0 -118
  136. package/crawlers/error_snapshotter.js.map +0 -1
  137. package/crawlers/error_tracker.d.ts +0 -55
  138. package/crawlers/error_tracker.d.ts.map +0 -1
  139. package/crawlers/error_tracker.js +0 -309
  140. package/crawlers/error_tracker.js.map +0 -1
  141. package/crawlers/index.d.ts +0 -7
  142. package/crawlers/index.d.ts.map +0 -1
  143. package/crawlers/index.js +0 -7
  144. package/crawlers/index.js.map +0 -1
  145. package/crawlers/internals/types.d.ts +0 -8
  146. package/crawlers/internals/types.d.ts.map +0 -1
  147. package/crawlers/internals/types.js +0 -2
  148. package/crawlers/internals/types.js.map +0 -1
  149. package/crawlers/statistics.d.ts +0 -213
  150. package/crawlers/statistics.d.ts.map +0 -1
  151. package/crawlers/statistics.js +0 -349
  152. package/crawlers/statistics.js.map +0 -1
  153. package/enqueue_links/enqueue_links.d.ts +0 -249
  154. package/enqueue_links/enqueue_links.d.ts.map +0 -1
  155. package/enqueue_links/enqueue_links.js +0 -269
  156. package/enqueue_links/enqueue_links.js.map +0 -1
  157. package/enqueue_links/index.d.ts +0 -3
  158. package/enqueue_links/index.d.ts.map +0 -1
  159. package/enqueue_links/index.js +0 -3
  160. package/enqueue_links/index.js.map +0 -1
  161. package/enqueue_links/shared.d.ts +0 -73
  162. package/enqueue_links/shared.d.ts.map +0 -1
  163. package/enqueue_links/shared.js +0 -212
  164. package/enqueue_links/shared.js.map +0 -1
  165. package/errors.d.ts.map +0 -1
  166. package/errors.js.map +0 -1
  167. package/events/event_manager.d.ts +0 -52
  168. package/events/event_manager.d.ts.map +0 -1
  169. package/events/event_manager.js.map +0 -1
  170. package/events/index.d.ts.map +0 -1
  171. package/events/index.js.map +0 -1
  172. package/events/local_event_manager.d.ts +0 -27
  173. package/events/local_event_manager.d.ts.map +0 -1
  174. package/events/local_event_manager.js +0 -75
  175. package/events/local_event_manager.js.map +0 -1
  176. package/index.d.ts.map +0 -1
  177. package/index.js.map +0 -1
  178. package/log.d.ts.map +0 -1
  179. package/log.js.map +0 -1
  180. package/proxy_configuration.d.ts +0 -169
  181. package/proxy_configuration.d.ts.map +0 -1
  182. package/proxy_configuration.js +0 -247
  183. package/proxy_configuration.js.map +0 -1
  184. package/recoverable_state.d.ts +0 -123
  185. package/recoverable_state.d.ts.map +0 -1
  186. package/recoverable_state.js +0 -140
  187. package/recoverable_state.js.map +0 -1
  188. package/request.d.ts.map +0 -1
  189. package/request.js.map +0 -1
  190. package/router.d.ts +0 -134
  191. package/router.d.ts.map +0 -1
  192. package/router.js +0 -164
  193. package/router.js.map +0 -1
  194. package/serialization.d.ts.map +0 -1
  195. package/serialization.js.map +0 -1
  196. package/session_pool/consts.d.ts +0 -4
  197. package/session_pool/consts.d.ts.map +0 -1
  198. package/session_pool/consts.js +0 -4
  199. package/session_pool/consts.js.map +0 -1
  200. package/session_pool/errors.d.ts +0 -8
  201. package/session_pool/errors.d.ts.map +0 -1
  202. package/session_pool/errors.js +0 -12
  203. package/session_pool/errors.js.map +0 -1
  204. package/session_pool/events.d.ts +0 -3
  205. package/session_pool/events.d.ts.map +0 -1
  206. package/session_pool/events.js +0 -3
  207. package/session_pool/events.js.map +0 -1
  208. package/session_pool/index.d.ts +0 -6
  209. package/session_pool/index.d.ts.map +0 -1
  210. package/session_pool/index.js +0 -6
  211. package/session_pool/index.js.map +0 -1
  212. package/session_pool/session.d.ts +0 -186
  213. package/session_pool/session.d.ts.map +0 -1
  214. package/session_pool/session.js +0 -278
  215. package/session_pool/session.js.map +0 -1
  216. package/session_pool/session_pool.d.ts +0 -258
  217. package/session_pool/session_pool.d.ts.map +0 -1
  218. package/session_pool/session_pool.js +0 -387
  219. package/session_pool/session_pool.js.map +0 -1
  220. package/storages/access_checking.d.ts +0 -13
  221. package/storages/access_checking.d.ts.map +0 -1
  222. package/storages/access_checking.js +0 -14
  223. package/storages/access_checking.js.map +0 -1
  224. package/storages/dataset.d.ts.map +0 -1
  225. package/storages/dataset.js.map +0 -1
  226. package/storages/index.d.ts.map +0 -1
  227. package/storages/index.js.map +0 -1
  228. package/storages/key_value_store.d.ts.map +0 -1
  229. package/storages/key_value_store.js +0 -512
  230. package/storages/key_value_store.js.map +0 -1
  231. package/storages/request_list.d.ts.map +0 -1
  232. package/storages/request_list.js.map +0 -1
  233. package/storages/request_list_adapter.d.ts +0 -58
  234. package/storages/request_list_adapter.d.ts.map +0 -1
  235. package/storages/request_list_adapter.js +0 -81
  236. package/storages/request_list_adapter.js.map +0 -1
  237. package/storages/request_manager_tandem.d.ts +0 -68
  238. package/storages/request_manager_tandem.d.ts.map +0 -1
  239. package/storages/request_manager_tandem.js +0 -124
  240. package/storages/request_manager_tandem.js.map +0 -1
  241. package/storages/request_provider.d.ts.map +0 -1
  242. package/storages/request_provider.js +0 -591
  243. package/storages/request_provider.js.map +0 -1
  244. package/storages/request_queue.d.ts +0 -128
  245. package/storages/request_queue.d.ts.map +0 -1
  246. package/storages/request_queue.js +0 -320
  247. package/storages/request_queue.js.map +0 -1
  248. package/storages/request_queue_v2.d.ts +0 -87
  249. package/storages/request_queue_v2.d.ts.map +0 -1
  250. package/storages/request_queue_v2.js +0 -438
  251. package/storages/request_queue_v2.js.map +0 -1
  252. package/storages/sitemap_request_list.d.ts +0 -250
  253. package/storages/sitemap_request_list.d.ts.map +0 -1
  254. package/storages/sitemap_request_list.js +0 -431
  255. package/storages/sitemap_request_list.js.map +0 -1
  256. package/storages/storage_manager.d.ts +0 -62
  257. package/storages/storage_manager.d.ts.map +0 -1
  258. package/storages/storage_manager.js +0 -105
  259. package/storages/storage_manager.js.map +0 -1
  260. package/storages/utils.d.ts.map +0 -1
  261. package/storages/utils.js.map +0 -1
  262. package/typedefs.d.ts.map +0 -1
  263. package/typedefs.js.map +0 -1
  264. package/validators.d.ts.map +0 -1
  265. package/validators.js.map +0 -1
@@ -0,0 +1,767 @@
1
+ import { z } from 'zod';
2
+ import { KEY_VALUE_STORE_KEY_REGEX } from '@apify/consts';
3
+ import { tryCancel } from '@apify/timeout';
4
+ import { Configuration } from '../configuration.js';
5
+ import { serviceLocator } from '../service-locator.js';
6
+ import { parseArgument, schemas, validators } from '../validators.js';
7
+ import { activeStorageTransaction, operationRejectedInTransaction, rejectOperationInTransaction, snapshotValue, withDirectStorageAccess, } from './transaction.js';
8
+ import { parseValue, serializeValue } from './key-value-store-codec.js';
9
+ import { StorageStatsTracker } from './storage-stats.js';
10
+ import { resolveStorageIdentifier } from './storage-instance-manager.js';
11
+ import { createDualIterable, purgeDefaultStorages } from './utils.js';
12
+ import { isBuffer, isStream } from '../byte-utils.js';
13
+ /** @internal */
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
+ });
29
+ /**
30
+ * The `KeyValueStore` class represents a key-value store, a simple data storage that is used
31
+ * for saving and reading data records or files. Each data record is
32
+ * represented by a unique key and associated with a MIME content type. Key-value stores are ideal
33
+ * for saving screenshots, crawler inputs and outputs, web pages, PDFs or to persist the state of crawlers.
34
+ *
35
+ * Do not instantiate this class directly, use the
36
+ * {@link KeyValueStore.open} function instead.
37
+ *
38
+ * Each crawler run is associated with a default key-value store, which is created exclusively
39
+ * for the run. By convention, the crawler input and output are stored into the
40
+ * default key-value store under the `INPUT` and `OUTPUT` key, respectively.
41
+ * Typically, input and output are JSON files, although it can be any other format.
42
+ * To access the default key-value store directly, you can use the
43
+ * {@link KeyValueStore.getValue} and {@link KeyValueStore.setValue} convenience functions.
44
+ *
45
+ * `KeyValueStore` stores its data on a local disk.
46
+ *
47
+ * If the `CRAWLEE_STORAGE_DIR` environment variable is set, the data is stored in
48
+ * the local directory in the following files:
49
+ * ```
50
+ * {CRAWLEE_STORAGE_DIR}/key_value_stores/{STORE_ID}/{INDEX}.{EXT}
51
+ * ```
52
+ * Note that `{STORE_ID}` is the name or ID of the key-value store. The default key-value store has ID: `default`.
53
+ * The `{KEY}` is the key of the record and `{EXT}` corresponds to the MIME content type of the data value.
54
+ *
55
+ * **Example usage:**
56
+ *
57
+ * ```javascript
58
+ * // Get crawler input from the default key-value store.
59
+ * const input = await KeyValueStore.getValue('INPUT');
60
+ * // Get some value from the default key-value store.
61
+ * const otherValue = await KeyValueStore.getValue('my-key');
62
+ *
63
+ * // Write crawler output to the default key-value store.
64
+ * await KeyValueStore.setValue('OUTPUT', { myResult: 123 });
65
+ *
66
+ * // Open a named key-value store
67
+ * const store = await KeyValueStore.open('some-name');
68
+ *
69
+ * // Write a record. JavaScript object is automatically converted to JSON,
70
+ * // strings and binary buffers are stored as they are
71
+ * await store.setValue('some-key', { foo: 'bar' });
72
+ *
73
+ * // Read a record. Note that JSON is automatically parsed to a JavaScript object,
74
+ * // text data returned as a string and other data is returned as binary buffer
75
+ * const value = await store.getValue('some-key');
76
+ *
77
+ * // Drop (delete) the store
78
+ * await store.drop();
79
+ * ```
80
+ * @category Result Stores
81
+ */
82
+ export class KeyValueStore {
83
+ configuration;
84
+ id;
85
+ name;
86
+ backend;
87
+ #persistStateEventStarted = false;
88
+ /** Cache for persistent (auto-saved) values. When we try to set such value, the cache will be updated automatically. */
89
+ #cache = new Map();
90
+ #statsTracker = new StorageStatsTracker({
91
+ readCount: 0,
92
+ writeCount: 0,
93
+ deleteCount: 0,
94
+ listCount: 0,
95
+ });
96
+ /**
97
+ * @internal
98
+ */
99
+ constructor(options, configuration = Configuration.getGlobalConfiguration()) {
100
+ this.configuration = configuration;
101
+ this.id = options.metadata.id;
102
+ this.name = options.metadata.name;
103
+ this.backend = options.backend;
104
+ }
105
+ /**
106
+ * Backend-independent usage counters tracked for this key-value store (read / write / delete /
107
+ * list operations issued to the underlying storage backend). Counted per backend call.
108
+ */
109
+ get stats() {
110
+ return this.#statsTracker.current;
111
+ }
112
+ /**
113
+ * Gets a value from the key-value store.
114
+ *
115
+ * The function returns a `Promise` that resolves to the record value,
116
+ * whose JavaScript type depends on the MIME content type of the record.
117
+ * Records with the `application/json`
118
+ * content type are automatically parsed and returned as a JavaScript object.
119
+ * Similarly, records with `text/plain` content types are returned as a string.
120
+ * For all other content types, the value is returned as a raw
121
+ * [`Buffer`](https://nodejs.org/api/buffer.html) instance.
122
+ *
123
+ * If the record does not exist, the function resolves to `null`.
124
+ *
125
+ * To save or delete a value in the key-value store, use the
126
+ * {@link KeyValueStore.setValue} function.
127
+ *
128
+ * **Example usage:**
129
+ *
130
+ * ```javascript
131
+ * const store = await KeyValueStore.open();
132
+ * const buffer = await store.getValue('screenshot1.png');
133
+ * ```
134
+ * @param key
135
+ * Unique key of the record. It can be at most 256 characters long and only consist
136
+ * of the following characters: `a`-`z`, `A`-`Z`, `0`-`9` and `!-_.'()`
137
+ * @param defaultValue
138
+ * Fallback that will be returned if no value if present in the storage.
139
+ * @returns
140
+ * Returns a promise that resolves to an object, string
141
+ * or [`Buffer`](https://nodejs.org/api/buffer.html), depending
142
+ * on the MIME content type of the record, or `null` if the key is missing from the store.
143
+ */
144
+ async getValue(key, defaultValue) {
145
+ tryCancel();
146
+ parseArgument(key, keySchema);
147
+ const record = await this.#readRecord(key);
148
+ // A missing record falls back to the default; a record that parses to a falsy value (including
149
+ // a stored literal `null`) is returned verbatim, so callers can tell "stored null" from "absent".
150
+ if (!record) {
151
+ return defaultValue ?? null;
152
+ }
153
+ // Storage backends are byte transports — the value is raw bytes; the frontend parses it here.
154
+ return parseValue(record.value, record.contentType ?? null);
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
+ }
202
+ /**
203
+ * Reads a record from the key-value store without parsing the value.
204
+ *
205
+ * Use this when you need the raw bytes and the content type — for example, to run your own
206
+ * parser (`simdjson`, a custom XML library, etc.) or to forward the bytes verbatim.
207
+ *
208
+ * There is no symmetric `setRecord` method, because {@link KeyValueStore.setValue} already
209
+ * passes a `Buffer` (or `string` / `Stream`) through unchanged when an explicit `contentType`
210
+ * is provided. To write pre-serialized bytes, call
211
+ * `setValue(key, buffer, { contentType: 'application/json; charset=utf-8' })`.
212
+ *
213
+ * Returns `null` if the record does not exist.
214
+ *
215
+ * **Example usage:**
216
+ * ```javascript
217
+ * const store = await KeyValueStore.open();
218
+ * const record = await store.getRecord('huge.json');
219
+ * if (record) {
220
+ * const data = simdjson.parse(record.value);
221
+ * }
222
+ * ```
223
+ *
224
+ * @param key
225
+ * Unique key of the record. It can be at most 256 characters long and only consist
226
+ * of the following characters: `a`-`z`, `A`-`Z`, `0`-`9` and `!-_.'()`
227
+ */
228
+ async getRecord(key) {
229
+ tryCancel();
230
+ parseArgument(key, keySchema);
231
+ return this.#readRecord(key);
232
+ }
233
+ /**
234
+ * Tests whether a record with the given key exists in the key-value store without retrieving its value.
235
+ *
236
+ * @param key The queried record key.
237
+ * @returns `true` if the record exists, `false` if it does not.
238
+ */
239
+ async recordExists(key) {
240
+ tryCancel();
241
+ parseArgument(key, keySchema);
242
+ const entry = this.#bufferedJournalEntries()?.get(key);
243
+ if (entry) {
244
+ return entry.value !== null;
245
+ }
246
+ return this.backend.recordExists(key);
247
+ }
248
+ async getAutoSavedValue(key, defaultValue = {}) {
249
+ tryCancel();
250
+ if (this.#cache.has(key)) {
251
+ return this.#cache.get(key);
252
+ }
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));
256
+ // The await above could have run in parallel with another call to this function. If the other call finished more quickly,
257
+ // the value will in cache at this point, and returning the new fetched value would introduce two different instances of
258
+ // the auto-saved object, and only the latter one would be persisted.
259
+ // Therefore we re-check the cache here, and if such race condition happened, we drop the fetched value and return the cached one.
260
+ if (this.#cache.has(key)) {
261
+ return this.#cache.get(key);
262
+ }
263
+ this.#cache.set(key, value);
264
+ this.#ensurePersistStateEvent();
265
+ return value;
266
+ }
267
+ #ensurePersistStateEvent() {
268
+ if (this.#persistStateEventStarted) {
269
+ return;
270
+ }
271
+ serviceLocator.getEventManager().on('persistState', async () => {
272
+ const promises = [];
273
+ for (const [key, value] of this.#cache) {
274
+ promises.push(this.setValue(key, value).catch((error) => serviceLocator.getLogger().warning(`Failed to persist the state value to ${key}`, { error })));
275
+ }
276
+ await Promise.all(promises);
277
+ });
278
+ this.#persistStateEventStarted = true;
279
+ }
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)) {
284
+ const results = [];
285
+ for (const item of page) {
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);
289
+ if (record) {
290
+ const parsed = parseValue(record.value, record.contentType ?? null);
291
+ results.push(mapRecord(item.key, parsed));
292
+ }
293
+ }
294
+ yield results;
295
+ }
296
+ }
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
+ }
316
+ let exclusiveStartKey;
317
+ while (true) {
318
+ this.#statsTracker.add('listCount');
319
+ const { items, isTruncated, nextExclusiveStartKey } = await this.backend.listKeys({
320
+ ...options,
321
+ exclusiveStartKey,
322
+ limit,
323
+ });
324
+ yield shadowedKeys.size > 0 ? items.filter((item) => !shadowedKeys.has(item.key)) : items;
325
+ if (!isTruncated)
326
+ break;
327
+ // Paginate from the raw backend cursor - it may reject a key it did not hand out.
328
+ exclusiveStartKey = nextExclusiveStartKey;
329
+ }
330
+ }
331
+ /**
332
+ * Saves or deletes a record in the key-value store.
333
+ * The function returns a promise that resolves once the record has been saved or deleted.
334
+ *
335
+ * **Example usage:**
336
+ *
337
+ * ```javascript
338
+ * const store = await KeyValueStore.open();
339
+ * await store.setValue('OUTPUT', { foo: 'bar' });
340
+ * ```
341
+ *
342
+ * Beware that the key can be at most 256 characters long and only contain the following characters: `a-zA-Z0-9!-_.'()`
343
+ *
344
+ * By default, `value` is converted to JSON and stored with the
345
+ * `application/json; charset=utf-8` MIME content type.
346
+ * To store the value with another content type, pass it in the options as follows:
347
+ * ```javascript
348
+ * const store = await KeyValueStore.open('my-text-store');
349
+ * await store.setValue('RESULTS', 'my text data', { contentType: 'text/plain' });
350
+ * ```
351
+ * If you set custom content type, `value` must be either a string or
352
+ * [`Buffer`](https://nodejs.org/api/buffer.html), otherwise an error will be thrown.
353
+ *
354
+ * If `value` is `null`, the record is deleted instead. Note that the `setValue()` function succeeds
355
+ * regardless whether the record existed or not.
356
+ *
357
+ * To retrieve a value from the key-value store, use the
358
+ * {@link KeyValueStore.getValue} function.
359
+ *
360
+ * **IMPORTANT:** Always make sure to use the `await` keyword when calling `setValue()`,
361
+ * otherwise the crawler process might finish before the value is stored!
362
+ *
363
+ * @param key
364
+ * Unique key of the record. It can be at most 256 characters long and only consist
365
+ * of the following characters: `a`-`z`, `A`-`Z`, `0`-`9` and `!-_.'()`
366
+ * @param value
367
+ * Record data, which can be one of the following values:
368
+ * - If `null`, the record in the key-value store is deleted.
369
+ * - If no `options.contentType` is specified, `value` can be any JavaScript object and it will be stringified to JSON.
370
+ * - If `options.contentType` is set, `value` is taken as is and it must be a `String` or [`Buffer`](https://nodejs.org/api/buffer.html).
371
+ * For any other value an error will be thrown.
372
+ * @param [options] Record options.
373
+ */
374
+ async setValue(key, value, options = {}) {
375
+ const transaction = activeStorageTransaction();
376
+ parseArgument(key, setValueKeySchema);
377
+ if (options.contentType && !(typeof value === 'string' || isBuffer(value) || isStream(value))) {
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;
407
+ }
408
+ // If we try to set the value of a cached state to a different reference, we need to update the cache accordingly.
409
+ const cachedValue = this.#cache.get(key);
410
+ if (cachedValue && cachedValue !== value) {
411
+ if (value === null) {
412
+ // Cached state can be only object, so a propagation of `null` means removing all its properties.
413
+ Object.keys(cachedValue).forEach((k) => this.#cache.delete(k));
414
+ }
415
+ else if (typeof value === 'object') {
416
+ // We need to remove the keys that are no longer present in the new value.
417
+ Object.keys(cachedValue)
418
+ .filter((k) => !(k in value))
419
+ .forEach((k) => this.#cache.delete(k));
420
+ // And update the existing ones + add new ones.
421
+ Object.assign(cachedValue, value);
422
+ }
423
+ }
424
+ // In this case delete the record.
425
+ if (value === null) {
426
+ this.#statsTracker.add('deleteCount');
427
+ return this.backend.deleteValue(key);
428
+ }
429
+ const serialized = serializeValue(value, optionsCopy.contentType);
430
+ this.#statsTracker.add('writeCount');
431
+ return this.backend.setValue({
432
+ key,
433
+ value: serialized.value,
434
+ contentType: serialized.contentType,
435
+ });
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
+ }
450
+ /**
451
+ * Removes the key-value store either from the Apify cloud storage or from the local directory,
452
+ * depending on the mode of operation.
453
+ */
454
+ async drop() {
455
+ rejectOperationInTransaction('KeyValueStore.drop()');
456
+ await this.backend.drop();
457
+ serviceLocator.getStorageInstanceManager().removeFromCache(this);
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
+ }
469
+ /** @internal */
470
+ clearCache() {
471
+ rejectOperationInTransaction('KeyValueStore.clearCache()');
472
+ this.#cache.clear();
473
+ }
474
+ /**
475
+ * Iterates over key-value store keys, yielding each in turn to an `iteratee` function.
476
+ * Each invocation of `iteratee` is called with three arguments: `(key, index, info)`, where `key`
477
+ * is the record key, `index` is a zero-based index of the key in the current iteration
478
+ * and `info` is an object that contains a single property `size`
479
+ * indicating size of the record in bytes.
480
+ *
481
+ * If the `iteratee` function returns a Promise then it is awaited before the next call.
482
+ * If it throws an error, the iteration is aborted and the `forEachKey` function throws the error.
483
+ *
484
+ * **Example usage**
485
+ * ```javascript
486
+ * const keyValueStore = await KeyValueStore.open();
487
+ * await keyValueStore.forEachKey(async (key, index, info) => {
488
+ * console.log(`Key at ${index}: ${key} has size ${info.size}`);
489
+ * });
490
+ * ```
491
+ *
492
+ * @param iteratee A function that is called for every key in the key-value store.
493
+ * @param [options] All `forEachKey()` parameters.
494
+ */
495
+ async forEachKey(iteratee, options = {}) {
496
+ tryCancel();
497
+ parseArgument(iteratee, schemas.anyFunction);
498
+ const parsedOptions = parseArgument(options, iteratorOptionsSchema);
499
+ let index = 0;
500
+ for await (const page of this.#fetchKeyPages(parsedOptions)) {
501
+ for (const item of page) {
502
+ await iteratee(item.key, index++, { size: item.size });
503
+ }
504
+ }
505
+ }
506
+ /**
507
+ * Returns key-value store keys.
508
+ *
509
+ * When awaited (`await store.keys()`), returns all keys as a flat `string[]` array.
510
+ * When used as an async iterable (`for await...of`), iterates over all keys across pages
511
+ * without loading everything into memory at once.
512
+ *
513
+ * **Example usage:**
514
+ * ```javascript
515
+ * const keyValueStore = await KeyValueStore.open();
516
+ *
517
+ * // Iterate over all keys (memory-efficient for large stores)
518
+ * for await (const key of keyValueStore.keys()) {
519
+ * console.log(key);
520
+ * }
521
+ *
522
+ * // Or fetch all keys at once
523
+ * const allKeys = await keyValueStore.keys();
524
+ * console.log(allKeys);
525
+ * ```
526
+ *
527
+ * @param options Options for the iteration.
528
+ */
529
+ keys(options = {}) {
530
+ tryCancel();
531
+ return createDualIterable({
532
+ createPages: () => this.#fetchKeyPages(options),
533
+ extractItems: (page) => page.map((item) => item.key),
534
+ });
535
+ }
536
+ /**
537
+ * Returns key-value store values.
538
+ *
539
+ * When awaited (`await store.values()`), returns all values as a flat `T[]` array.
540
+ * When used as an async iterable (`for await...of`), iterates over all values across pages
541
+ * without loading everything into memory at once.
542
+ *
543
+ * **Example usage:**
544
+ * ```javascript
545
+ * const keyValueStore = await KeyValueStore.open();
546
+ *
547
+ * // Iterate over all values (memory-efficient for large stores)
548
+ * for await (const value of keyValueStore.values()) {
549
+ * console.log(value);
550
+ * }
551
+ *
552
+ * // Or fetch all values at once
553
+ * const allValues = await keyValueStore.values();
554
+ * console.log(allValues);
555
+ * ```
556
+ *
557
+ * @param options Options for the iteration.
558
+ */
559
+ values(options = {}) {
560
+ tryCancel();
561
+ return createDualIterable({
562
+ createPages: () => this.#fetchKeyValuePages(options, (_key, value) => value),
563
+ extractItems: (page) => page,
564
+ });
565
+ }
566
+ /**
567
+ * Returns key-value store entries (key-value pairs).
568
+ *
569
+ * When awaited (`await store.entries()`), returns all entries as a flat `[key, value][]` array.
570
+ * When used as an async iterable (`for await...of`), iterates over all entries across pages
571
+ * without loading everything into memory at once.
572
+ *
573
+ * **Example usage:**
574
+ * ```javascript
575
+ * const keyValueStore = await KeyValueStore.open();
576
+ *
577
+ * // Iterate over all entries (memory-efficient for large stores)
578
+ * for await (const [key, value] of keyValueStore.entries()) {
579
+ * console.log(`${key}: ${value}`);
580
+ * }
581
+ *
582
+ * // Or fetch all entries at once
583
+ * const allEntries = await keyValueStore.entries();
584
+ * console.log(allEntries);
585
+ * ```
586
+ *
587
+ * @param options Options for the iteration.
588
+ */
589
+ entries(options = {}) {
590
+ tryCancel();
591
+ return createDualIterable({
592
+ createPages: () => this.#fetchKeyValuePages(options, (key, value) => [key, value]),
593
+ extractItems: (page) => page,
594
+ });
595
+ }
596
+ /**
597
+ * Default async iterator for the key-value store, iterating over entries (key-value pairs).
598
+ * Allows using the store directly in a `for await...of` loop.
599
+ *
600
+ * **Example usage:**
601
+ * ```javascript
602
+ * const keyValueStore = await KeyValueStore.open();
603
+ * for await (const [key, value] of keyValueStore) {
604
+ * console.log(`${key}: ${value}`);
605
+ * }
606
+ * ```
607
+ */
608
+ async *[Symbol.asyncIterator]() {
609
+ yield* this.entries();
610
+ }
611
+ /**
612
+ * Returns a file URL for the given key.
613
+ *
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).
617
+ *
618
+ * @param key The key of the record to generate the public URL for.
619
+ */
620
+ async getPublicUrl(key) {
621
+ return this.backend.getPublicUrl(key);
622
+ }
623
+ /**
624
+ * Opens a key-value store and returns a promise resolving to an instance of the {@link KeyValueStore} class.
625
+ *
626
+ * Key-value stores are used to store records or files, along with their MIME content type.
627
+ * The records are stored and retrieved using a unique key.
628
+ * The actual data is stored either on a local filesystem or in the Apify cloud.
629
+ *
630
+ * For more details and code examples, see the {@link KeyValueStore} class.
631
+ *
632
+ * @param [identifier]
633
+ * ID or name of the key-value store to be opened. If a string is provided, it will first be
634
+ * looked up as an ID; if no such storage exists, it will be treated as a name.
635
+ * If `null` or `undefined`, the function returns the default key-value store associated with the crawler run.
636
+ * @param [options] Storage manager options.
637
+ */
638
+ static async open(identifier, options = {}) {
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 });
644
+ const resolved = await resolveStorageIdentifier(identifier, storageBackend, 'KeyValueStore');
645
+ return serviceLocator.getStorageInstanceManager().openStorage(this, {
646
+ ...resolved,
647
+ backendOpener: () => storageBackend.createKeyValueStoreBackend(resolved),
648
+ backendCacheKey: storageBackend.getStorageBackendCacheKey?.() ?? storageBackend.constructor.name,
649
+ });
650
+ }
651
+ /**
652
+ * Gets a value from the default {@link KeyValueStore} associated with the current crawler run.
653
+ *
654
+ * This is just a convenient shortcut for {@link KeyValueStore.getValue}.
655
+ * For example, calling the following code:
656
+ * ```javascript
657
+ * const value = await KeyValueStore.getValue('my-key');
658
+ * ```
659
+ *
660
+ * is equivalent to:
661
+ * ```javascript
662
+ * const store = await KeyValueStore.open();
663
+ * const value = await store.getValue('my-key');
664
+ * ```
665
+ *
666
+ * To store the value to the default key-value store, you can use the {@link KeyValueStore.setValue} function.
667
+ *
668
+ * For more information, see {@link KeyValueStore.open}
669
+ * and {@link KeyValueStore.getValue}.
670
+ *
671
+ * @param key Unique record key.
672
+ * @param defaultValue Fallback that will be returned if no value if present in the storage.
673
+ * @returns
674
+ * Returns a promise that resolves to an object, string
675
+ * or [`Buffer`](https://nodejs.org/api/buffer.html), depending
676
+ * on the MIME content type of the record, or `null`
677
+ * if the record is missing.
678
+ * @ignore
679
+ */
680
+ static async getValue(key, defaultValue) {
681
+ const store = await this.open();
682
+ return store.getValue(key, defaultValue);
683
+ }
684
+ /**
685
+ * Reads a record from the default {@link KeyValueStore} associated with the current crawler run
686
+ * without parsing the value.
687
+ *
688
+ * This is just a convenient shortcut for {@link KeyValueStore.getRecord}. Returns `null` if the
689
+ * record does not exist.
690
+ *
691
+ * @param key
692
+ * Unique key of the record. It can be at most 256 characters long and only consist
693
+ * of the following characters: `a`-`z`, `A`-`Z`, `0`-`9` and `!-_.'()`
694
+ * @ignore
695
+ */
696
+ static async getRecord(key) {
697
+ const store = await this.open();
698
+ return store.getRecord(key);
699
+ }
700
+ /**
701
+ * Tests whether a record with the given key exists in the default {@link KeyValueStore} associated with the current crawler run.
702
+ * @param key The queried record key.
703
+ * @returns `true` if the record exists, `false` if it does not.
704
+ */
705
+ static async recordExists(key) {
706
+ const store = await this.open();
707
+ return store.recordExists(key);
708
+ }
709
+ static async getAutoSavedValue(key, defaultValue = {}) {
710
+ const store = await this.open();
711
+ return store.getAutoSavedValue(key, defaultValue);
712
+ }
713
+ /**
714
+ * Stores or deletes a value in the default {@link KeyValueStore} associated with the current crawler run.
715
+ *
716
+ * This is just a convenient shortcut for {@link KeyValueStore.setValue}.
717
+ * For example, calling the following code:
718
+ * ```javascript
719
+ * await KeyValueStore.setValue('OUTPUT', { foo: "bar" });
720
+ * ```
721
+ *
722
+ * is equivalent to:
723
+ * ```javascript
724
+ * const store = await KeyValueStore.open();
725
+ * await store.setValue('OUTPUT', { foo: "bar" });
726
+ * ```
727
+ *
728
+ * To get a value from the default key-value store, you can use the {@link KeyValueStore.getValue} function.
729
+ *
730
+ * For more information, see {@link KeyValueStore.open}
731
+ * and {@link KeyValueStore.getValue}.
732
+ *
733
+ * @param key
734
+ * Unique record key.
735
+ * @param value
736
+ * Record data, which can be one of the following values:
737
+ * - If `null`, the record in the key-value store is deleted.
738
+ * - If no `options.contentType` is specified, `value` can be any JavaScript object, and it will be stringified to JSON.
739
+ * - If `options.contentType` is set, `value` is taken as is, and it must be a `String` or [`Buffer`](https://nodejs.org/api/buffer.html).
740
+ * For any other value an error will be thrown.
741
+ * @param [options]
742
+ * @ignore
743
+ */
744
+ static async setValue(key, value, options = {}) {
745
+ const store = await this.open();
746
+ return store.setValue(key, value, options);
747
+ }
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);
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
+ };
767
+ }