@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.
- package/README.md +1 -1
- package/configuration.d.ts +16 -47
- package/configuration.js +13 -25
- package/debug.js +4 -4
- package/errors.d.ts +28 -38
- package/errors.js +33 -47
- package/events/event_manager.d.ts +2 -2
- package/events/event_manager.js +7 -6
- package/events/index.d.ts +1 -0
- package/events/local_event_manager.d.ts +1 -8
- package/events/local_event_manager.js +13 -13
- package/events/system_info.d.ts +38 -0
- package/index.d.ts +2 -8
- package/index.js +4 -8
- package/internal.d.ts +8 -0
- package/internal.js +9 -0
- package/log.d.ts +10 -11
- package/log.js +52 -20
- package/memory-storage/memory-storage.d.ts +15 -18
- package/memory-storage/memory-storage.js +80 -58
- package/memory-storage/resource-clients/dataset.d.ts +1 -6
- package/memory-storage/resource-clients/dataset.js +23 -31
- package/memory-storage/resource-clients/key-value-store.d.ts +1 -10
- package/memory-storage/resource-clients/key-value-store.js +43 -67
- package/memory-storage/resource-clients/request-queue.d.ts +1 -42
- package/memory-storage/resource-clients/request-queue.js +109 -117
- package/owned_or_injected.d.ts +1 -3
- package/owned_or_injected.js +17 -17
- package/package.json +17 -20
- package/proxy_configuration.d.ts +21 -26
- package/proxy_configuration.js +35 -25
- package/recoverable_state.d.ts +104 -47
- package/recoverable_state.js +199 -74
- package/request.d.ts +20 -107
- package/request.js +78 -244
- package/serialization.js +17 -16
- package/service_locator.d.ts +22 -10
- package/service_locator.js +59 -48
- package/storages/batched_adds.d.ts +37 -0
- package/storages/batched_adds.js +73 -0
- package/storages/dataset.d.ts +13 -8
- package/storages/dataset.js +149 -40
- package/storages/index.d.ts +4 -4
- package/storages/index.js +2 -4
- package/storages/key_value_store.d.ts +16 -35
- package/storages/key_value_store.js +223 -110
- package/storages/key_value_store_codec.js +6 -11
- package/storages/request_dedup_cache.d.ts +1 -4
- package/storages/request_dedup_cache.js +15 -15
- package/storages/request_list.d.ts +9 -104
- package/storages/request_list.js +236 -233
- package/storages/request_loader.d.ts +49 -18
- package/storages/request_loader.js +36 -1
- package/storages/request_manager.d.ts +86 -0
- package/storages/request_manager_tandem.d.ts +14 -38
- package/storages/request_manager_tandem.js +67 -64
- package/storages/request_queue.d.ts +23 -50
- package/storages/request_queue.js +371 -226
- package/storages/storage_instance_manager.d.ts +2 -4
- package/storages/storage_instance_manager.js +21 -21
- package/storages/storage_stats.d.ts +1 -1
- package/storages/storage_stats.js +4 -4
- package/storages/transaction.d.ts +270 -0
- package/storages/transaction.js +296 -0
- package/storages/utils.d.ts +6 -3
- package/storages/utils.js +11 -2
- package/system-info/runtime.js +7 -7
- package/url.d.ts +9 -0
- package/url.js +11 -0
- package/validators.d.ts +23 -25
- package/validators.js +14 -25
- package/autoscaling/autoscaled_pool.d.ts +0 -213
- package/autoscaling/autoscaled_pool.js +0 -378
- package/autoscaling/client_load_signal.d.ts +0 -59
- package/autoscaling/client_load_signal.js +0 -73
- package/autoscaling/concurrency_system.d.ts +0 -283
- package/autoscaling/concurrency_system.js +0 -350
- package/autoscaling/cpu_load_signal.d.ts +0 -44
- package/autoscaling/cpu_load_signal.js +0 -46
- package/autoscaling/event_loop_load_signal.d.ts +0 -54
- package/autoscaling/event_loop_load_signal.js +0 -60
- package/autoscaling/index.d.ts +0 -9
- package/autoscaling/index.js +0 -9
- package/autoscaling/load_signal.d.ts +0 -99
- package/autoscaling/load_signal.js +0 -103
- package/autoscaling/memory_load_signal.d.ts +0 -56
- package/autoscaling/memory_load_signal.js +0 -106
- package/autoscaling/snapshotter.d.ts +0 -87
- package/autoscaling/snapshotter.js +0 -67
- package/autoscaling/system_status.d.ts +0 -161
- package/autoscaling/system_status.js +0 -139
- package/autoscaling/weighted_avg.d.ts +0 -5
- package/autoscaling/weighted_avg.js +0 -14
- package/cookie_utils.d.ts +0 -44
- package/cookie_utils.js +0 -122
- package/crawlers/context_pipeline.d.ts +0 -70
- package/crawlers/context_pipeline.js +0 -122
- package/crawlers/crawler_commons.d.ts +0 -257
- package/crawlers/crawler_commons.js +0 -107
- package/crawlers/error_snapshotter.d.ts +0 -59
- package/crawlers/error_snapshotter.js +0 -117
- package/crawlers/error_tracker.d.ts +0 -54
- package/crawlers/error_tracker.js +0 -308
- package/crawlers/index.d.ts +0 -5
- package/crawlers/index.js +0 -5
- package/crawlers/internals/types.d.ts +0 -7
- package/crawlers/statistics.d.ts +0 -209
- package/crawlers/statistics.js +0 -350
- package/enqueue_links/enqueue_links.d.ts +0 -264
- package/enqueue_links/enqueue_links.js +0 -271
- package/enqueue_links/index.d.ts +0 -2
- package/enqueue_links/index.js +0 -2
- package/enqueue_links/shared.d.ts +0 -83
- package/enqueue_links/shared.js +0 -221
- package/router.d.ts +0 -309
- package/router.js +0 -309
- package/session_pool/consts.d.ts +0 -3
- package/session_pool/consts.js +0 -3
- package/session_pool/errors.d.ts +0 -7
- package/session_pool/errors.js +0 -11
- package/session_pool/fingerprint.d.ts +0 -9
- package/session_pool/fingerprint.js +0 -30
- package/session_pool/index.d.ts +0 -4
- package/session_pool/index.js +0 -4
- package/session_pool/session.d.ts +0 -161
- package/session_pool/session.js +0 -218
- package/session_pool/session_pool.d.ts +0 -246
- package/session_pool/session_pool.js +0 -386
- package/storages/access_checking.d.ts +0 -12
- package/storages/access_checking.js +0 -17
- package/storages/sitemap_request_loader.d.ts +0 -249
- package/storages/sitemap_request_loader.js +0 -432
- /package/{crawlers/internals/types.js → events/system_info.js} +0 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import
|
|
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 {
|
|
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.
|
|
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
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
this
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
this
|
|
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
|
-
|
|
190
|
-
|
|
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
|
-
|
|
195
|
-
if (this
|
|
196
|
-
return this
|
|
249
|
+
tryCancel();
|
|
250
|
+
if (this.#cache.has(key)) {
|
|
251
|
+
return this.#cache.get(key);
|
|
197
252
|
}
|
|
198
|
-
|
|
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
|
|
204
|
-
return this
|
|
260
|
+
if (this.#cache.has(key)) {
|
|
261
|
+
return this.#cache.get(key);
|
|
205
262
|
}
|
|
206
|
-
this
|
|
207
|
-
this
|
|
263
|
+
this.#cache.set(key, value);
|
|
264
|
+
this.#ensurePersistStateEvent();
|
|
208
265
|
return value;
|
|
209
266
|
}
|
|
210
|
-
ensurePersistStateEvent() {
|
|
211
|
-
if (this
|
|
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
|
|
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
|
|
278
|
+
this.#persistStateEventStarted = true;
|
|
222
279
|
}
|
|
223
|
-
async
|
|
224
|
-
for
|
|
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
|
-
|
|
228
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
297
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
426
|
+
this.#statsTracker.add('deleteCount');
|
|
329
427
|
return this.backend.deleteValue(key);
|
|
330
428
|
}
|
|
331
429
|
const serialized = serializeValue(value, optionsCopy.contentType);
|
|
332
|
-
this
|
|
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
|
-
|
|
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
|
-
|
|
351
|
-
this
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
|
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
|
-
|
|
530
|
+
tryCancel();
|
|
412
531
|
return createDualIterable({
|
|
413
|
-
createPages: () => this
|
|
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
|
-
|
|
560
|
+
tryCancel();
|
|
442
561
|
return createDualIterable({
|
|
443
|
-
createPages: () => this
|
|
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
|
-
|
|
590
|
+
tryCancel();
|
|
472
591
|
return createDualIterable({
|
|
473
|
-
createPages: () => this
|
|
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
|
-
*
|
|
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
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
21
|
-
this
|
|
22
|
-
this
|
|
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
|
|
26
|
-
return this
|
|
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
|
|
30
|
-
this
|
|
31
|
-
this
|
|
29
|
+
const index = this.#indexOf(cacheKey);
|
|
30
|
+
this.#keys[index] = cacheKey;
|
|
31
|
+
this.#ids[index] = requestId;
|
|
32
32
|
}
|
|
33
33
|
clear() {
|
|
34
|
-
this
|
|
35
|
-
this
|
|
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
|
|
45
|
+
return (hash >>> 0) % this.#size;
|
|
46
46
|
/* eslint-enable no-bitwise */
|
|
47
47
|
}
|
|
48
48
|
}
|