@crawlee/core 4.0.0-beta.99 → 4.0.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/autoscaling/autoscaled_pool.d.ts +3 -21
- package/autoscaling/autoscaled_pool.js +105 -97
- package/autoscaling/concurrency_system.d.ts +7 -22
- package/autoscaling/concurrency_system.js +112 -100
- package/autoscaling/cpu_load_signal.d.ts +1 -2
- package/autoscaling/cpu_load_signal.js +11 -10
- package/autoscaling/event_loop_load_signal.d.ts +1 -4
- package/autoscaling/event_loop_load_signal.js +18 -18
- package/autoscaling/index.d.ts +1 -1
- package/autoscaling/index.js +1 -1
- package/autoscaling/load_signal.d.ts +8 -7
- package/autoscaling/load_signal.js +14 -12
- package/autoscaling/memory_load_signal.d.ts +3 -12
- package/autoscaling/memory_load_signal.js +41 -41
- package/autoscaling/snapshotter.d.ts +7 -10
- package/autoscaling/snapshotter.js +18 -18
- package/autoscaling/{client_load_signal.d.ts → storage_backend_load_signal.d.ts} +14 -17
- package/autoscaling/{client_load_signal.js → storage_backend_load_signal.js} +27 -27
- package/autoscaling/system_status.d.ts +9 -11
- package/autoscaling/system_status.js +13 -13
- package/configuration.d.ts +16 -16
- package/configuration.js +6 -6
- package/crawlers/context_pipeline.js +6 -6
- package/crawlers/crawler_commons.d.ts +14 -112
- package/crawlers/crawler_commons.js +1 -107
- package/crawlers/error_snapshotter.d.ts +2 -4
- package/crawlers/error_snapshotter.js +5 -5
- package/crawlers/index.d.ts +1 -1
- package/crawlers/index.js +0 -1
- package/crawlers/statistics.d.ts +164 -45
- package/crawlers/statistics.js +339 -153
- package/debug.js +4 -4
- package/enqueue_links/enqueue_links.d.ts +51 -159
- package/enqueue_links/enqueue_links.js +38 -231
- package/enqueue_links/shared.d.ts +34 -24
- package/enqueue_links/shared.js +54 -36
- package/errors.d.ts +43 -12
- package/errors.js +48 -9
- package/events/event_manager.d.ts +2 -2
- package/events/event_manager.js +7 -6
- package/events/local_event_manager.d.ts +1 -1
- package/events/local_event_manager.js +5 -5
- package/http.d.ts +9 -0
- package/http.js +28 -0
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/log.js +5 -1
- package/memory-storage/memory-storage.d.ts +3 -11
- package/memory-storage/memory-storage.js +42 -48
- package/memory-storage/resource-clients/dataset.d.ts +1 -1
- package/memory-storage/resource-clients/dataset.js +8 -13
- package/memory-storage/resource-clients/key-value-store.d.ts +1 -1
- package/memory-storage/resource-clients/key-value-store.js +36 -38
- package/memory-storage/resource-clients/request-queue.d.ts +4 -23
- package/memory-storage/resource-clients/request-queue.js +68 -80
- package/owned_or_injected.d.ts +1 -3
- package/owned_or_injected.js +17 -17
- package/package.json +8 -9
- package/proxy_configuration.d.ts +1 -3
- package/proxy_configuration.js +18 -14
- package/recoverable_state.d.ts +80 -45
- package/recoverable_state.js +163 -75
- package/request.d.ts +3 -4
- package/request.js +57 -44
- package/router.d.ts +6 -9
- package/router.js +23 -23
- package/serialization.js +14 -13
- package/service_locator.d.ts +19 -10
- package/service_locator.js +57 -48
- package/session_pool/session.d.ts +3 -14
- package/session_pool/session.js +76 -74
- package/session_pool/session_pool.d.ts +6 -12
- package/session_pool/session_pool.js +85 -77
- package/storages/batched_adds.d.ts +37 -0
- package/storages/batched_adds.js +73 -0
- package/storages/dataset.d.ts +12 -1
- package/storages/dataset.js +133 -32
- package/storages/index.d.ts +2 -1
- package/storages/index.js +2 -1
- package/storages/key_value_store.d.ts +19 -4
- package/storages/key_value_store.js +204 -75
- package/storages/request_dedup_cache.d.ts +1 -2
- package/storages/request_dedup_cache.js +9 -9
- package/storages/request_list.d.ts +4 -23
- package/storages/request_list.js +103 -96
- package/storages/request_loader.d.ts +5 -0
- package/storages/request_manager_tandem.d.ts +1 -10
- package/storages/request_manager_tandem.js +27 -27
- package/storages/request_queue.d.ts +21 -18
- package/storages/request_queue.js +318 -177
- package/storages/sitemap_request_loader.d.ts +22 -70
- package/storages/sitemap_request_loader.js +122 -116
- package/storages/storage_instance_manager.d.ts +1 -2
- package/storages/storage_instance_manager.js +17 -17
- package/storages/storage_stats.d.ts +1 -1
- package/storages/storage_stats.js +4 -4
- package/storages/throttling_request_manager.d.ts +239 -0
- package/storages/throttling_request_manager.js +646 -0
- package/storages/transaction.d.ts +252 -0
- package/storages/transaction.js +251 -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 +22 -25
- package/validators.js +13 -25
- package/storages/access_checking.d.ts +0 -12
- package/storages/access_checking.js +0 -17
|
@@ -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
|
|
@@ -70,11 +86,12 @@ export class KeyValueStore {
|
|
|
70
86
|
configuration;
|
|
71
87
|
id;
|
|
72
88
|
name;
|
|
89
|
+
// kept as TS-private: key_value_store tests spy on the backend directly
|
|
73
90
|
backend;
|
|
74
|
-
persistStateEventStarted = false;
|
|
91
|
+
#persistStateEventStarted = false;
|
|
75
92
|
/** 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({
|
|
93
|
+
#cache = new Map();
|
|
94
|
+
#statsTracker = new StorageStatsTracker({
|
|
78
95
|
readCount: 0,
|
|
79
96
|
writeCount: 0,
|
|
80
97
|
deleteCount: 0,
|
|
@@ -94,7 +111,7 @@ export class KeyValueStore {
|
|
|
94
111
|
* list operations issued to the underlying storage backend). Counted per backend call.
|
|
95
112
|
*/
|
|
96
113
|
get stats() {
|
|
97
|
-
return this
|
|
114
|
+
return this.#statsTracker.current;
|
|
98
115
|
}
|
|
99
116
|
/**
|
|
100
117
|
* Gets a value from the key-value store.
|
|
@@ -129,10 +146,9 @@ export class KeyValueStore {
|
|
|
129
146
|
* on the MIME content type of the record, or `null` if the key is missing from the store.
|
|
130
147
|
*/
|
|
131
148
|
async getValue(key, defaultValue) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
this.
|
|
135
|
-
const record = await this.backend.getValue(key);
|
|
149
|
+
tryCancel();
|
|
150
|
+
parseArgument(key, keySchema);
|
|
151
|
+
const record = await this.readRecord(key);
|
|
136
152
|
// A missing record falls back to the default; a record that parses to a falsy value (including
|
|
137
153
|
// a stored literal `null`) is returned verbatim, so callers can tell "stored null" from "absent".
|
|
138
154
|
if (!record) {
|
|
@@ -141,6 +157,52 @@ export class KeyValueStore {
|
|
|
141
157
|
// Storage backends are byte transports — the value is raw bytes; the frontend parses it here.
|
|
142
158
|
return parseValue(record.value, record.contentType ?? null);
|
|
143
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* The active transaction's last buffered write per key for this store, derived from its journal.
|
|
162
|
+
* An entry with a `null` value is a tombstone (an in-transaction deletion).
|
|
163
|
+
*/
|
|
164
|
+
bufferedJournalEntries() {
|
|
165
|
+
const transaction = activeStorageTransaction();
|
|
166
|
+
if (!transaction)
|
|
167
|
+
return undefined;
|
|
168
|
+
const lastWritePerKey = new Map();
|
|
169
|
+
for (const entry of transaction.journal) {
|
|
170
|
+
if (entry.type === 'keyValueStore' && entry.participant === this) {
|
|
171
|
+
lastWritePerKey.set(entry.key, entry);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return lastWritePerKey;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* The single transaction-aware record read shared by `getValue`, `getRecord`, `recordExists` and the
|
|
178
|
+
* listing paths: buffered key → serialized through the standard codec (same fidelity as a real
|
|
179
|
+
* round-trip); tombstoned key → `null`; otherwise the backend.
|
|
180
|
+
*
|
|
181
|
+
* The per-key buffered lookup requires the whole journal to be reduced to a last-write-per-key map,
|
|
182
|
+
* which is O(journal). Single-record callers let it default (rebuilt per call); the listing paths,
|
|
183
|
+
* which read many keys, pass a map built once so the read stays O(1) per key instead of O(journal).
|
|
184
|
+
*/
|
|
185
|
+
async readRecord(key, buffered = this.bufferedJournalEntries()) {
|
|
186
|
+
const entry = buffered?.get(key);
|
|
187
|
+
if (entry) {
|
|
188
|
+
if (entry.value === null) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
const serialized = serializeValue(entry.value, entry.options?.contentType);
|
|
192
|
+
return {
|
|
193
|
+
value: normalizeSerializedValue(serialized.value),
|
|
194
|
+
contentType: serialized.contentType ?? null,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
this.#statsTracker.add('readCount');
|
|
198
|
+
const record = await this.backend.getValue(key);
|
|
199
|
+
if (!record)
|
|
200
|
+
return null;
|
|
201
|
+
return {
|
|
202
|
+
value: record.value,
|
|
203
|
+
contentType: record.contentType ?? null,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
144
206
|
/**
|
|
145
207
|
* Reads a record from the key-value store without parsing the value.
|
|
146
208
|
*
|
|
@@ -168,16 +230,9 @@ export class KeyValueStore {
|
|
|
168
230
|
* of the following characters: `a`-`z`, `A`-`Z`, `0`-`9` and `!-_.'()`
|
|
169
231
|
*/
|
|
170
232
|
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
|
-
};
|
|
233
|
+
tryCancel();
|
|
234
|
+
parseArgument(key, keySchema);
|
|
235
|
+
return this.readRecord(key);
|
|
181
236
|
}
|
|
182
237
|
/**
|
|
183
238
|
* Tests whether a record with the given key exists in the key-value store without retrieving its value.
|
|
@@ -186,46 +241,55 @@ export class KeyValueStore {
|
|
|
186
241
|
* @returns `true` if the record exists, `false` if it does not.
|
|
187
242
|
*/
|
|
188
243
|
async recordExists(key) {
|
|
189
|
-
|
|
190
|
-
|
|
244
|
+
tryCancel();
|
|
245
|
+
parseArgument(key, keySchema);
|
|
246
|
+
const entry = this.bufferedJournalEntries()?.get(key);
|
|
247
|
+
if (entry) {
|
|
248
|
+
return entry.value !== null;
|
|
249
|
+
}
|
|
191
250
|
return this.backend.recordExists(key);
|
|
192
251
|
}
|
|
193
252
|
async getAutoSavedValue(key, defaultValue = {}) {
|
|
194
|
-
|
|
195
|
-
if (this
|
|
196
|
-
return this
|
|
253
|
+
tryCancel();
|
|
254
|
+
if (this.#cache.has(key)) {
|
|
255
|
+
return this.#cache.get(key);
|
|
197
256
|
}
|
|
198
|
-
|
|
257
|
+
// Auto-saved state is deliberately *not* transactional. The direct read bypasses any active
|
|
258
|
+
// transaction - a buffered value seeded into this shared cache would survive a rollback forever.
|
|
259
|
+
const value = await withDirectStorageAccess(async () => this.getValue(key, defaultValue));
|
|
199
260
|
// The await above could have run in parallel with another call to this function. If the other call finished more quickly,
|
|
200
261
|
// the value will in cache at this point, and returning the new fetched value would introduce two different instances of
|
|
201
262
|
// the auto-saved object, and only the latter one would be persisted.
|
|
202
263
|
// 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
|
|
264
|
+
if (this.#cache.has(key)) {
|
|
265
|
+
return this.#cache.get(key);
|
|
205
266
|
}
|
|
206
|
-
this
|
|
267
|
+
this.#cache.set(key, value);
|
|
207
268
|
this.ensurePersistStateEvent();
|
|
208
269
|
return value;
|
|
209
270
|
}
|
|
210
271
|
ensurePersistStateEvent() {
|
|
211
|
-
if (this
|
|
272
|
+
if (this.#persistStateEventStarted) {
|
|
212
273
|
return;
|
|
213
274
|
}
|
|
214
275
|
serviceLocator.getEventManager().on('persistState', async () => {
|
|
215
276
|
const promises = [];
|
|
216
|
-
for (const [key, value] of this
|
|
277
|
+
for (const [key, value] of this.#cache) {
|
|
217
278
|
promises.push(this.setValue(key, value).catch((error) => serviceLocator.getLogger().warning(`Failed to persist the state value to ${key}`, { error })));
|
|
218
279
|
}
|
|
219
280
|
await Promise.all(promises);
|
|
220
281
|
});
|
|
221
|
-
this
|
|
282
|
+
this.#persistStateEventStarted = true;
|
|
222
283
|
}
|
|
223
284
|
async *fetchKeyValuePages(options, mapRecord) {
|
|
224
|
-
for
|
|
285
|
+
// Reduce the journal once for the whole iteration, not once per key inside `readRecord`.
|
|
286
|
+
const buffered = this.bufferedJournalEntries();
|
|
287
|
+
for await (const page of this.fetchKeyPages(options, buffered)) {
|
|
225
288
|
const results = [];
|
|
226
289
|
for (const item of page) {
|
|
227
|
-
|
|
228
|
-
|
|
290
|
+
// The shared transaction-aware read, so a key that exists only in the transaction resolves
|
|
291
|
+
// here instead of being dropped (`values()` would disagree with `keys()` on length).
|
|
292
|
+
const record = await this.readRecord(item.key, buffered);
|
|
229
293
|
if (record) {
|
|
230
294
|
const parsed = parseValue(record.value, record.contentType ?? null);
|
|
231
295
|
results.push(mapRecord(item.key, parsed));
|
|
@@ -234,18 +298,37 @@ export class KeyValueStore {
|
|
|
234
298
|
yield results;
|
|
235
299
|
}
|
|
236
300
|
}
|
|
237
|
-
async *fetchKeyPages(options, limit = KVS_KEYS_DEFAULT_LIMIT) {
|
|
301
|
+
async *fetchKeyPages(options, buffered = this.bufferedJournalEntries(), limit = KVS_KEYS_DEFAULT_LIMIT) {
|
|
302
|
+
// Buffered keys are emitted first, then the real pages with any buffered (or tombstoned) key
|
|
303
|
+
// skipped - a merge-join is not an option, since `listKeys` promises no sort order.
|
|
304
|
+
const shadowedKeys = new Set();
|
|
305
|
+
if (buffered) {
|
|
306
|
+
const bufferedItems = [];
|
|
307
|
+
for (const [key, entry] of buffered) {
|
|
308
|
+
shadowedKeys.add(key);
|
|
309
|
+
if (entry.value === null)
|
|
310
|
+
continue;
|
|
311
|
+
if (options.prefix !== undefined && !key.startsWith(options.prefix))
|
|
312
|
+
continue;
|
|
313
|
+
bufferedItems.push(bufferedKeyItemData(key, entry));
|
|
314
|
+
}
|
|
315
|
+
if (bufferedItems.length > 0) {
|
|
316
|
+
bufferedItems.sort((a, b) => (a.key < b.key ? -1 : 1));
|
|
317
|
+
yield bufferedItems;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
238
320
|
let exclusiveStartKey;
|
|
239
321
|
while (true) {
|
|
240
|
-
this
|
|
322
|
+
this.#statsTracker.add('listCount');
|
|
241
323
|
const { items, isTruncated, nextExclusiveStartKey } = await this.backend.listKeys({
|
|
242
324
|
...options,
|
|
243
325
|
exclusiveStartKey,
|
|
244
326
|
limit,
|
|
245
327
|
});
|
|
246
|
-
yield items;
|
|
328
|
+
yield shadowedKeys.size > 0 ? items.filter((item) => !shadowedKeys.has(item.key)) : items;
|
|
247
329
|
if (!isTruncated)
|
|
248
330
|
break;
|
|
331
|
+
// Paginate from the raw backend cursor - it may reject a key it did not hand out.
|
|
249
332
|
exclusiveStartKey = nextExclusiveStartKey;
|
|
250
333
|
}
|
|
251
334
|
}
|
|
@@ -293,62 +376,94 @@ export class KeyValueStore {
|
|
|
293
376
|
* @param [options] Record options.
|
|
294
377
|
*/
|
|
295
378
|
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
|
-
})));
|
|
379
|
+
const transaction = activeStorageTransaction();
|
|
380
|
+
parseArgument(key, setValueKeySchema);
|
|
302
381
|
if (options.contentType && !(typeof value === 'string' || isBuffer(value) || isStream(value))) {
|
|
303
|
-
throw new
|
|
382
|
+
throw new Error('The "value" parameter must be a String, Buffer, ArrayBuffer, TypedArray, or Stream when "options.contentType" is specified.');
|
|
383
|
+
}
|
|
384
|
+
// The parse result is a fresh copy, so we never update what user passed.
|
|
385
|
+
const optionsCopy = parseArgument(options, recordOptionsSchema);
|
|
386
|
+
// The whole transaction branch sits *above* the auto-saved cache update below, so a buffered
|
|
387
|
+
// write touches nothing outside the journal. That cache is shared, process-lifetime frontend
|
|
388
|
+
// state, so mutating it here would survive a rollback and later be persisted by `persistState`.
|
|
389
|
+
// The commit replay re-enters this method with no active transaction and updates it then.
|
|
390
|
+
if (transaction) {
|
|
391
|
+
if (isStream(value)) {
|
|
392
|
+
// A stream cannot serve both a read-your-own-writes read and the commit replay. The
|
|
393
|
+
// transaction is known-active here, so throw directly rather than via the conditional guard.
|
|
394
|
+
throw operationRejectedInTransaction(`KeyValueStore.setValue() with a stream value (key "${key}")`, 'a stream can only be consumed once, so it cannot be buffered until commit.');
|
|
395
|
+
}
|
|
396
|
+
// Validation only, result discarded: the journal snapshot (`structuredClone`) accepts values
|
|
397
|
+
// JSON cannot, which would otherwise only throw at a later read or at commit.
|
|
398
|
+
if (value !== null) {
|
|
399
|
+
serializeValue(value, optionsCopy.contentType);
|
|
400
|
+
}
|
|
401
|
+
// One snapshot serves both the reads and the commit replay; `null` is a tombstone.
|
|
402
|
+
transaction.recordJournalEntry({
|
|
403
|
+
type: 'keyValueStore',
|
|
404
|
+
participant: this,
|
|
405
|
+
storageId: this.id,
|
|
406
|
+
key,
|
|
407
|
+
value: value === null ? null : snapshotValue(value),
|
|
408
|
+
options: optionsCopy,
|
|
409
|
+
});
|
|
410
|
+
return;
|
|
304
411
|
}
|
|
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
412
|
// 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
|
|
413
|
+
const cachedValue = this.#cache.get(key);
|
|
312
414
|
if (cachedValue && cachedValue !== value) {
|
|
313
415
|
if (value === null) {
|
|
314
416
|
// Cached state can be only object, so a propagation of `null` means removing all its properties.
|
|
315
|
-
Object.keys(cachedValue).forEach((k) => this
|
|
417
|
+
Object.keys(cachedValue).forEach((k) => this.#cache.delete(k));
|
|
316
418
|
}
|
|
317
419
|
else if (typeof value === 'object') {
|
|
318
420
|
// We need to remove the keys that are no longer present in the new value.
|
|
319
421
|
Object.keys(cachedValue)
|
|
320
422
|
.filter((k) => !(k in value))
|
|
321
|
-
.forEach((k) => this
|
|
423
|
+
.forEach((k) => this.#cache.delete(k));
|
|
322
424
|
// And update the existing ones + add new ones.
|
|
323
425
|
Object.assign(cachedValue, value);
|
|
324
426
|
}
|
|
325
427
|
}
|
|
326
428
|
// In this case delete the record.
|
|
327
429
|
if (value === null) {
|
|
328
|
-
this
|
|
430
|
+
this.#statsTracker.add('deleteCount');
|
|
329
431
|
return this.backend.deleteValue(key);
|
|
330
432
|
}
|
|
331
433
|
const serialized = serializeValue(value, optionsCopy.contentType);
|
|
332
|
-
this
|
|
434
|
+
this.#statsTracker.add('writeCount');
|
|
333
435
|
return this.backend.setValue({
|
|
334
436
|
key,
|
|
335
437
|
value: serialized.value,
|
|
336
438
|
contentType: serialized.contentType,
|
|
337
439
|
});
|
|
338
440
|
}
|
|
441
|
+
/** @internal */
|
|
442
|
+
async commitJournalEntries(entries) {
|
|
443
|
+
// One `setValue` per key, last write wins - idempotent under retry.
|
|
444
|
+
const lastWritePerKey = new Map();
|
|
445
|
+
for (const entry of entries) {
|
|
446
|
+
if (entry.type === 'keyValueStore') {
|
|
447
|
+
lastWritePerKey.set(entry.key, { value: entry.value, options: entry.options });
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
for (const [key, { value, options }] of lastWritePerKey) {
|
|
451
|
+
await this.setValue(key, value, options);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
339
454
|
/**
|
|
340
455
|
* Removes the key-value store either from the Apify cloud storage or from the local directory,
|
|
341
456
|
* depending on the mode of operation.
|
|
342
457
|
*/
|
|
343
458
|
async drop() {
|
|
344
|
-
|
|
459
|
+
rejectOperationInTransaction('KeyValueStore.drop()');
|
|
345
460
|
await this.backend.drop();
|
|
346
461
|
serviceLocator.getStorageInstanceManager().removeFromCache(this);
|
|
347
462
|
}
|
|
348
463
|
/** @internal */
|
|
349
464
|
clearCache() {
|
|
350
|
-
|
|
351
|
-
this
|
|
465
|
+
rejectOperationInTransaction('KeyValueStore.clearCache()');
|
|
466
|
+
this.#cache.clear();
|
|
352
467
|
}
|
|
353
468
|
/**
|
|
354
469
|
* Iterates over key-value store keys, yielding each in turn to an `iteratee` function.
|
|
@@ -372,13 +487,11 @@ export class KeyValueStore {
|
|
|
372
487
|
* @param [options] All `forEachKey()` parameters.
|
|
373
488
|
*/
|
|
374
489
|
async forEachKey(iteratee, options = {}) {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
prefix: ow.optional.string,
|
|
379
|
-
}));
|
|
490
|
+
tryCancel();
|
|
491
|
+
parseArgument(iteratee, schemas.anyFunction);
|
|
492
|
+
const parsedOptions = parseArgument(options, iteratorOptionsSchema);
|
|
380
493
|
let index = 0;
|
|
381
|
-
for await (const page of this.fetchKeyPages(
|
|
494
|
+
for await (const page of this.fetchKeyPages(parsedOptions)) {
|
|
382
495
|
for (const item of page) {
|
|
383
496
|
await iteratee(item.key, index++, { size: item.size });
|
|
384
497
|
}
|
|
@@ -408,7 +521,7 @@ export class KeyValueStore {
|
|
|
408
521
|
* @param options Options for the iteration.
|
|
409
522
|
*/
|
|
410
523
|
keys(options = {}) {
|
|
411
|
-
|
|
524
|
+
tryCancel();
|
|
412
525
|
return createDualIterable({
|
|
413
526
|
createPages: () => this.fetchKeyPages(options),
|
|
414
527
|
extractItems: (page) => page.map((item) => item.key),
|
|
@@ -438,7 +551,7 @@ export class KeyValueStore {
|
|
|
438
551
|
* @param options Options for the iteration.
|
|
439
552
|
*/
|
|
440
553
|
values(options = {}) {
|
|
441
|
-
|
|
554
|
+
tryCancel();
|
|
442
555
|
return createDualIterable({
|
|
443
556
|
createPages: () => this.fetchKeyValuePages(options, (_key, value) => value),
|
|
444
557
|
extractItems: (page) => page,
|
|
@@ -468,7 +581,7 @@ export class KeyValueStore {
|
|
|
468
581
|
* @param options Options for the iteration.
|
|
469
582
|
*/
|
|
470
583
|
entries(options = {}) {
|
|
471
|
-
|
|
584
|
+
tryCancel();
|
|
472
585
|
return createDualIterable({
|
|
473
586
|
createPages: () => this.fetchKeyValuePages(options, (key, value) => [key, value]),
|
|
474
587
|
extractItems: (page) => page,
|
|
@@ -515,14 +628,11 @@ export class KeyValueStore {
|
|
|
515
628
|
* @param [options] Storage manager options.
|
|
516
629
|
*/
|
|
517
630
|
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 });
|
|
631
|
+
tryCancel();
|
|
632
|
+
const parsedOptions = parseArgument(options, openOptionsSchema);
|
|
633
|
+
const configuration = parsedOptions.configuration ?? Configuration.getGlobalConfiguration();
|
|
634
|
+
const storageBackend = parsedOptions.storageBackend ?? serviceLocator.getStorageBackend();
|
|
635
|
+
await purgeDefaultStorages({ onlyPurgeOnce: true, storageBackend, configuration });
|
|
526
636
|
const resolved = await resolveStorageIdentifier(identifier, storageBackend, 'KeyValueStore');
|
|
527
637
|
return serviceLocator.getStorageInstanceManager().openStorage(this, {
|
|
528
638
|
...resolved,
|
|
@@ -652,3 +762,22 @@ export class KeyValueStore {
|
|
|
652
762
|
return store.getValue(store.configuration.inputKey);
|
|
653
763
|
}
|
|
654
764
|
}
|
|
765
|
+
/** Normalizes a codec-serialized value into the `Buffer | ArrayBuffer` shape raw record reads promise. */
|
|
766
|
+
function normalizeSerializedValue(value) {
|
|
767
|
+
if (typeof value === 'string') {
|
|
768
|
+
return Buffer.from(value);
|
|
769
|
+
}
|
|
770
|
+
if (ArrayBuffer.isView(value)) {
|
|
771
|
+
return Buffer.isBuffer(value) ? value : Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
772
|
+
}
|
|
773
|
+
return value;
|
|
774
|
+
}
|
|
775
|
+
/** Computes the key listing item (serialized byte size and content type) of a buffered entry. */
|
|
776
|
+
function bufferedKeyItemData(key, entry) {
|
|
777
|
+
const serialized = serializeValue(entry.value, entry.options?.contentType);
|
|
778
|
+
return {
|
|
779
|
+
key,
|
|
780
|
+
size: normalizeSerializedValue(serialized.value).byteLength,
|
|
781
|
+
contentType: serialized.contentType,
|
|
782
|
+
};
|
|
783
|
+
}
|
|
@@ -12,9 +12,8 @@
|
|
|
12
12
|
* @internal
|
|
13
13
|
*/
|
|
14
14
|
export declare class RequestDeduplicationCache {
|
|
15
|
+
#private;
|
|
15
16
|
private readonly size;
|
|
16
|
-
private keys;
|
|
17
|
-
private ids;
|
|
18
17
|
constructor(size?: number);
|
|
19
18
|
get(cacheKey: string): string | null;
|
|
20
19
|
add(cacheKey: string, requestId: string): void;
|
|
@@ -13,26 +13,26 @@
|
|
|
13
13
|
*/
|
|
14
14
|
export class RequestDeduplicationCache {
|
|
15
15
|
size;
|
|
16
|
-
keys;
|
|
17
|
-
ids;
|
|
16
|
+
#keys;
|
|
17
|
+
#ids;
|
|
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
20
|
this.size = size;
|
|
21
|
-
this
|
|
22
|
-
this
|
|
21
|
+
this.#keys = new Array(size);
|
|
22
|
+
this.#ids = new Array(size);
|
|
23
23
|
}
|
|
24
24
|
get(cacheKey) {
|
|
25
25
|
const index = this.indexOf(cacheKey);
|
|
26
|
-
return this
|
|
26
|
+
return this.#keys[index] === cacheKey ? this.#ids[index] : null;
|
|
27
27
|
}
|
|
28
28
|
add(cacheKey, requestId) {
|
|
29
29
|
const index = this.indexOf(cacheKey);
|
|
30
|
-
this
|
|
31
|
-
this
|
|
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
38
|
indexOf(cacheKey) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { BaseHttpClient
|
|
1
|
+
import type { BaseHttpClient } from '@crawlee/http-client';
|
|
2
|
+
import type { Dictionary } from '@crawlee/types';
|
|
2
3
|
import type { Configuration } from '../configuration.js';
|
|
3
4
|
import type { IProxyConfiguration } from '../proxy_configuration.js';
|
|
4
5
|
import { Request, type RequestOptions, type Source } from '../request.js';
|
|
@@ -216,28 +217,18 @@ export interface RequestListOptions {
|
|
|
216
217
|
* @category Sources
|
|
217
218
|
*/
|
|
218
219
|
export declare class RequestList implements IRequestLoader {
|
|
219
|
-
private
|
|
220
|
+
#private;
|
|
220
221
|
/**
|
|
221
222
|
* Array of all requests from all sources, in the order as they appeared in sources.
|
|
222
223
|
* All requests in the array have distinct uniqueKey!
|
|
223
224
|
* @internal
|
|
224
225
|
*/
|
|
225
226
|
readonly requests: (Request | RequestOptions)[];
|
|
226
|
-
/** Index to the next item in requests array to fetch. All previous requests are either handled or in progress. */
|
|
227
|
-
private nextIndex;
|
|
228
|
-
/** Dictionary, key is Request.uniqueKey, value is corresponding index in the requests array. */
|
|
229
|
-
private uniqueKeyToIndex;
|
|
230
227
|
/**
|
|
231
228
|
* Set of `uniqueKey`s of requests that were returned by fetchNextRequest().
|
|
232
229
|
* @internal
|
|
233
230
|
*/
|
|
234
231
|
inProgress: Set<string>;
|
|
235
|
-
/**
|
|
236
|
-
* `uniqueKey`s of requests that were in progress when the state was last persisted and thus need to be
|
|
237
|
-
* re-crawled after a restart. They are served before advancing through the rest of the sources.
|
|
238
|
-
* @internal
|
|
239
|
-
*/
|
|
240
|
-
private requestsToRetry;
|
|
241
232
|
/**
|
|
242
233
|
* Starts as true because until we handle the first request, the list is effectively persisted by doing nothing.
|
|
243
234
|
* @internal
|
|
@@ -248,17 +239,7 @@ export declare class RequestList implements IRequestLoader {
|
|
|
248
239
|
* @internal
|
|
249
240
|
*/
|
|
250
241
|
areRequestsPersisted: boolean;
|
|
251
|
-
private isLoading;
|
|
252
|
-
private isInitialized;
|
|
253
|
-
private persistStateKey?;
|
|
254
|
-
private persistRequestsKey?;
|
|
255
|
-
private initialState?;
|
|
256
|
-
private store?;
|
|
257
|
-
private keepDuplicateUrls;
|
|
258
242
|
private sources;
|
|
259
|
-
private sourcesFunction?;
|
|
260
|
-
private proxyConfiguration?;
|
|
261
|
-
private httpClient?;
|
|
262
243
|
/**
|
|
263
244
|
* To create new instance of `RequestList` we need to use `RequestList.open()` factory method.
|
|
264
245
|
* @param options All `RequestList` configuration options
|
|
@@ -437,7 +418,7 @@ export declare class RequestList implements IRequestLoader {
|
|
|
437
418
|
/**
|
|
438
419
|
* @internal wraps public utility for mocking purposes
|
|
439
420
|
*/
|
|
440
|
-
private
|
|
421
|
+
private downloadListOfUrls;
|
|
441
422
|
}
|
|
442
423
|
/**
|
|
443
424
|
* Represents state of a {@link RequestList}. It can be used to resume a {@link RequestList} which has been previously processed.
|