@crawlee/core 4.0.0-beta.104 → 4.0.0-beta.106
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/autoscaling/autoscaled_pool.d.ts +3 -21
- package/autoscaling/autoscaled_pool.js +85 -85
- package/autoscaling/client_load_signal.d.ts +1 -5
- package/autoscaling/client_load_signal.js +20 -20
- package/autoscaling/concurrency_system.d.ts +5 -20
- package/autoscaling/concurrency_system.js +81 -80
- package/autoscaling/cpu_load_signal.d.ts +1 -2
- package/autoscaling/cpu_load_signal.js +10 -10
- package/autoscaling/event_loop_load_signal.d.ts +1 -4
- package/autoscaling/event_loop_load_signal.js +18 -18
- package/autoscaling/load_signal.d.ts +1 -1
- package/autoscaling/load_signal.js +12 -11
- package/autoscaling/memory_load_signal.d.ts +3 -12
- package/autoscaling/memory_load_signal.js +40 -41
- package/autoscaling/snapshotter.d.ts +1 -4
- package/autoscaling/snapshotter.js +12 -12
- package/autoscaling/system_status.d.ts +1 -3
- package/autoscaling/system_status.js +11 -11
- package/configuration.d.ts +1 -1
- package/configuration.js +3 -3
- package/crawlers/context_pipeline.js +6 -6
- package/crawlers/crawler_commons.d.ts +6 -56
- package/crawlers/crawler_commons.js +1 -107
- package/crawlers/index.d.ts +1 -1
- package/crawlers/index.js +0 -1
- package/crawlers/statistics.d.ts +1 -8
- package/crawlers/statistics.js +45 -44
- package/events/event_manager.d.ts +1 -1
- package/events/event_manager.js +3 -3
- package/events/local_event_manager.d.ts +1 -1
- package/events/local_event_manager.js +3 -3
- package/log.js +5 -1
- package/memory-storage/memory-storage.d.ts +1 -5
- package/memory-storage/memory-storage.js +2 -2
- package/memory-storage/resource-clients/dataset.d.ts +1 -1
- package/memory-storage/resource-clients/dataset.js +6 -5
- package/memory-storage/resource-clients/key-value-store.d.ts +1 -1
- package/memory-storage/resource-clients/key-value-store.js +13 -12
- package/memory-storage/resource-clients/request-queue.d.ts +4 -23
- package/memory-storage/resource-clients/request-queue.js +59 -58
- package/owned_or_injected.d.ts +1 -3
- package/owned_or_injected.js +17 -17
- package/package.json +5 -5
- package/proxy_configuration.d.ts +1 -3
- package/proxy_configuration.js +8 -8
- package/recoverable_state.d.ts +1 -10
- package/recoverable_state.js +41 -41
- package/request.d.ts +1 -2
- package/request.js +10 -13
- package/router.d.ts +1 -4
- package/router.js +23 -23
- package/serialization.js +8 -9
- package/service_locator.d.ts +1 -10
- package/service_locator.js +48 -48
- package/session_pool/session.d.ts +1 -12
- package/session_pool/session.js +50 -50
- package/session_pool/session_pool.d.ts +2 -11
- package/session_pool/session_pool.js +59 -58
- package/storages/dataset.d.ts +12 -1
- package/storages/dataset.js +121 -22
- package/storages/index.d.ts +1 -1
- package/storages/index.js +1 -1
- package/storages/key_value_store.d.ts +19 -4
- package/storages/key_value_store.js +174 -48
- package/storages/request_dedup_cache.d.ts +1 -2
- package/storages/request_dedup_cache.js +9 -9
- package/storages/request_list.d.ts +2 -22
- package/storages/request_list.js +74 -73
- 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 +256 -53
- package/storages/sitemap_request_loader.d.ts +1 -44
- package/storages/sitemap_request_loader.js +87 -87
- 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/transaction.d.ts +252 -0
- package/storages/transaction.js +251 -0
- package/system-info/runtime.js +7 -7
- package/storages/access_checking.d.ts +0 -12
- package/storages/access_checking.js +0 -17
package/storages/dataset.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { stringify } from 'csv-stringify/sync';
|
|
2
2
|
import ow from 'ow';
|
|
3
|
+
import { tryCancel } from '@apify/timeout';
|
|
3
4
|
import { Configuration } from '../configuration.js';
|
|
4
5
|
import { serviceLocator } from '../service_locator.js';
|
|
5
|
-
import {
|
|
6
|
+
import { activeStorageTransaction, rejectOperationInTransaction, snapshotValue } from './transaction.js';
|
|
6
7
|
import { KeyValueStore } from './key_value_store.js';
|
|
7
8
|
import { StorageStatsTracker } from './storage_stats.js';
|
|
8
9
|
import { resolveStorageIdentifier } from './storage_instance_manager.js';
|
|
@@ -88,7 +89,7 @@ export class Dataset {
|
|
|
88
89
|
name;
|
|
89
90
|
backend;
|
|
90
91
|
log;
|
|
91
|
-
statsTracker = new StorageStatsTracker({
|
|
92
|
+
#statsTracker = new StorageStatsTracker({
|
|
92
93
|
readCount: 0,
|
|
93
94
|
writeCount: 0,
|
|
94
95
|
});
|
|
@@ -107,7 +108,7 @@ export class Dataset {
|
|
|
107
108
|
* the underlying storage backend). Counted per backend call.
|
|
108
109
|
*/
|
|
109
110
|
get stats() {
|
|
110
|
-
return this
|
|
111
|
+
return this.#statsTracker.current;
|
|
111
112
|
}
|
|
112
113
|
/**
|
|
113
114
|
* Stores an object or an array of objects to the dataset.
|
|
@@ -121,24 +122,33 @@ export class Dataset {
|
|
|
121
122
|
* The objects must be serializable to JSON.
|
|
122
123
|
*/
|
|
123
124
|
async pushData(data) {
|
|
124
|
-
|
|
125
|
+
const transaction = activeStorageTransaction();
|
|
125
126
|
ow(data, 'data', ow.object);
|
|
126
127
|
// Normalize to array and validate each item
|
|
127
128
|
const items = Array.isArray(data) ? data : [data];
|
|
128
129
|
for (let i = 0; i < items.length; i++) {
|
|
129
130
|
assertJsonSerializable(items[i], i);
|
|
130
131
|
}
|
|
131
|
-
|
|
132
|
+
if (transaction) {
|
|
133
|
+
// One snapshot serves both the reads and the commit replay, so the two cannot disagree.
|
|
134
|
+
transaction.recordJournalEntry({
|
|
135
|
+
type: 'dataset',
|
|
136
|
+
participant: this,
|
|
137
|
+
storageId: this.id,
|
|
138
|
+
items: snapshotValue(items),
|
|
139
|
+
recordedAt: new Date(),
|
|
140
|
+
});
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
this.#statsTracker.add('writeCount');
|
|
132
144
|
await this.backend.pushData(items);
|
|
133
145
|
}
|
|
134
146
|
/**
|
|
135
147
|
* Returns {@link DatasetContent} object holding the items in the dataset based on the provided parameters.
|
|
136
148
|
*/
|
|
137
149
|
async getData(options = {}) {
|
|
138
|
-
checkStorageAccess();
|
|
139
150
|
try {
|
|
140
|
-
this.
|
|
141
|
-
return await this.backend.getData(options);
|
|
151
|
+
return await this.readPage(options);
|
|
142
152
|
}
|
|
143
153
|
catch (e) {
|
|
144
154
|
const error = e;
|
|
@@ -148,12 +158,93 @@ export class Dataset {
|
|
|
148
158
|
throw e;
|
|
149
159
|
}
|
|
150
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* The single transaction-aware page read all dataset read paths go through — both `getData()` and
|
|
163
|
+
* the private `fetchPages()`. Returns the real page concatenated with the current transaction's
|
|
164
|
+
* buffered items, with `offset` / `limit` / `desc` windowing applied across the concatenation.
|
|
165
|
+
*/
|
|
166
|
+
async readPage(options) {
|
|
167
|
+
const buffered = this.bufferedJournalEntries()?.flatMap((entry) => entry.items);
|
|
168
|
+
// Every branch below hits the backend exactly once.
|
|
169
|
+
this.#statsTracker.add('readCount');
|
|
170
|
+
if (!buffered?.length) {
|
|
171
|
+
return this.backend.getData(options);
|
|
172
|
+
}
|
|
173
|
+
const { offset = 0, limit, desc = false } = options;
|
|
174
|
+
if (!desc) {
|
|
175
|
+
const realPage = await this.backend.getData(options);
|
|
176
|
+
// Buffered items sit past `realPage.total`, so the window bounds must come from that - not
|
|
177
|
+
// from the page's shortfall, which `skipEmpty` produces without exhausting the real items.
|
|
178
|
+
const bufferedStart = Math.max(0, offset - realPage.total);
|
|
179
|
+
const bufferedEnd = limit === undefined ? buffered.length : Math.max(0, offset + limit - realPage.total);
|
|
180
|
+
const items = [...realPage.items, ...buffered.slice(bufferedStart, bufferedEnd)];
|
|
181
|
+
return {
|
|
182
|
+
items,
|
|
183
|
+
total: realPage.total + buffered.length,
|
|
184
|
+
offset,
|
|
185
|
+
// A caller that passed no limit wants everything, so the backend-reported `limit` is
|
|
186
|
+
// passed through - backends are free to report a page size or a sentinel there.
|
|
187
|
+
limit: limit ?? realPage.limit,
|
|
188
|
+
count: items.length,
|
|
189
|
+
desc,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
// Descending order: the buffered items are the newest, so they come first, reversed.
|
|
193
|
+
const reversedBuffer = [...buffered].reverse();
|
|
194
|
+
const fromBuffer = limit === undefined ? reversedBuffer.slice(offset) : reversedBuffer.slice(offset, offset + limit);
|
|
195
|
+
const needed = limit === undefined ? Infinity : limit - fromBuffer.length;
|
|
196
|
+
if (needed <= 0) {
|
|
197
|
+
// The whole window is served from the buffer; only the real total is missing.
|
|
198
|
+
const { itemCount } = await this.backend.getMetadata();
|
|
199
|
+
return {
|
|
200
|
+
items: fromBuffer,
|
|
201
|
+
total: itemCount + buffered.length,
|
|
202
|
+
offset,
|
|
203
|
+
limit: limit,
|
|
204
|
+
count: fromBuffer.length,
|
|
205
|
+
desc,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
const realPage = await this.backend.getData({
|
|
209
|
+
...options,
|
|
210
|
+
offset: Math.max(0, offset - buffered.length),
|
|
211
|
+
...(limit === undefined ? {} : { limit: needed }),
|
|
212
|
+
});
|
|
213
|
+
return {
|
|
214
|
+
items: [...fromBuffer, ...realPage.items],
|
|
215
|
+
total: realPage.total + buffered.length,
|
|
216
|
+
offset,
|
|
217
|
+
limit: limit ?? realPage.limit,
|
|
218
|
+
count: fromBuffer.length + realPage.items.length,
|
|
219
|
+
desc,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
/** The active transaction's buffered writes to this dataset, derived from its journal. */
|
|
223
|
+
bufferedJournalEntries() {
|
|
224
|
+
const transaction = activeStorageTransaction();
|
|
225
|
+
return transaction?.journal.filter((entry) => entry.type === 'dataset' && entry.participant === this);
|
|
226
|
+
}
|
|
227
|
+
/** @internal */
|
|
228
|
+
async commitJournalEntries(entries) {
|
|
229
|
+
const items = [];
|
|
230
|
+
for (const entry of entries) {
|
|
231
|
+
if (entry.type === 'dataset') {
|
|
232
|
+
items.push(...entry.items);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// One backend call with all journaled items, in order - as close to atomic as the backend allows.
|
|
236
|
+
// Straight to the backend: the items were validated and snapshotted at write time.
|
|
237
|
+
if (items.length > 0) {
|
|
238
|
+
this.#statsTracker.add('writeCount');
|
|
239
|
+
await this.backend.pushData(items);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
151
242
|
/**
|
|
152
243
|
* Returns all the data from the dataset. This will iterate through the whole dataset
|
|
153
244
|
* via the `listItems()` client method, which gives you only paginated results.
|
|
154
245
|
*/
|
|
155
246
|
async export(options = {}) {
|
|
156
|
-
|
|
247
|
+
tryCancel();
|
|
157
248
|
const items = [];
|
|
158
249
|
for await (const page of this.fetchPages(options)) {
|
|
159
250
|
items.push(...page.items);
|
|
@@ -219,7 +310,7 @@ export class Dataset {
|
|
|
219
310
|
* @param [options] An optional options object where you can provide the dataset and target KVS name.
|
|
220
311
|
*/
|
|
221
312
|
static async exportToJSON(key, options) {
|
|
222
|
-
|
|
313
|
+
tryCancel();
|
|
223
314
|
const dataset = await this.open(options?.fromDataset);
|
|
224
315
|
await dataset.exportToJSON(key, options);
|
|
225
316
|
}
|
|
@@ -230,7 +321,7 @@ export class Dataset {
|
|
|
230
321
|
* @param [options] An optional options object where you can provide the dataset and target KVS name.
|
|
231
322
|
*/
|
|
232
323
|
static async exportToCSV(key, options) {
|
|
233
|
-
|
|
324
|
+
tryCancel();
|
|
234
325
|
const dataset = await this.open(options?.fromDataset);
|
|
235
326
|
await dataset.exportToCSV(key, options);
|
|
236
327
|
}
|
|
@@ -252,8 +343,17 @@ export class Dataset {
|
|
|
252
343
|
* @throws If the underlying storage no longer exists (e.g. it was deleted externally).
|
|
253
344
|
*/
|
|
254
345
|
async getInfo() {
|
|
255
|
-
|
|
256
|
-
|
|
346
|
+
const buffered = this.bufferedJournalEntries();
|
|
347
|
+
const metadata = await this.backend.getMetadata();
|
|
348
|
+
if (buffered?.length) {
|
|
349
|
+
const lastWriteAt = buffered[buffered.length - 1].recordedAt;
|
|
350
|
+
return {
|
|
351
|
+
...metadata,
|
|
352
|
+
itemCount: metadata.itemCount + buffered.reduce((sum, entry) => sum + entry.items.length, 0),
|
|
353
|
+
modifiedAt: metadata.modifiedAt > lastWriteAt ? metadata.modifiedAt : lastWriteAt,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
return metadata;
|
|
257
357
|
}
|
|
258
358
|
/**
|
|
259
359
|
* Iterates over dataset items, yielding each in turn to an `iteratee` function.
|
|
@@ -276,7 +376,7 @@ export class Dataset {
|
|
|
276
376
|
* @default 0
|
|
277
377
|
*/
|
|
278
378
|
async forEach(iteratee, options = {}, index = 0) {
|
|
279
|
-
|
|
379
|
+
tryCancel();
|
|
280
380
|
if (!options.offset)
|
|
281
381
|
options.offset = 0;
|
|
282
382
|
if (options.format && options.format !== 'json')
|
|
@@ -303,7 +403,7 @@ export class Dataset {
|
|
|
303
403
|
* @param [options] All `map()` parameters.
|
|
304
404
|
*/
|
|
305
405
|
async map(iteratee, options = {}) {
|
|
306
|
-
|
|
406
|
+
tryCancel();
|
|
307
407
|
const result = [];
|
|
308
408
|
await this.forEach(async (item, index) => {
|
|
309
409
|
const res = await iteratee(item, index);
|
|
@@ -312,7 +412,7 @@ export class Dataset {
|
|
|
312
412
|
return result;
|
|
313
413
|
}
|
|
314
414
|
async reduce(iteratee, memo, options = {}) {
|
|
315
|
-
|
|
415
|
+
tryCancel();
|
|
316
416
|
let currentMemo = memo;
|
|
317
417
|
const wrappedFunc = async (item, index) => {
|
|
318
418
|
if (index === 0 && currentMemo === undefined) {
|
|
@@ -344,8 +444,7 @@ export class Dataset {
|
|
|
344
444
|
const fetchLimit = totalLimit !== undefined ? Math.min(pageSize, totalLimit - yielded) : pageSize;
|
|
345
445
|
if (fetchLimit <= 0)
|
|
346
446
|
break;
|
|
347
|
-
this.
|
|
348
|
-
const page = await this.backend.getData({ ...options, offset, limit: fetchLimit });
|
|
447
|
+
const page = await this.readPage({ ...options, offset, limit: fetchLimit });
|
|
349
448
|
yield page;
|
|
350
449
|
yielded += page.items.length;
|
|
351
450
|
if (page.items.length < fetchLimit || offset + page.items.length >= page.total)
|
|
@@ -377,7 +476,7 @@ export class Dataset {
|
|
|
377
476
|
* @param options Options for the iteration.
|
|
378
477
|
*/
|
|
379
478
|
values(options = {}) {
|
|
380
|
-
|
|
479
|
+
tryCancel();
|
|
381
480
|
return createDualIterable({
|
|
382
481
|
createPages: () => this.fetchPages(options),
|
|
383
482
|
extractItems: (page) => page.items,
|
|
@@ -407,7 +506,7 @@ export class Dataset {
|
|
|
407
506
|
* @param options Options for the iteration.
|
|
408
507
|
*/
|
|
409
508
|
entries(options = {}) {
|
|
410
|
-
|
|
509
|
+
tryCancel();
|
|
411
510
|
return createDualIterable({
|
|
412
511
|
createPages: () => this.fetchEntryPages(options),
|
|
413
512
|
extractItems: (page) => page.items,
|
|
@@ -433,7 +532,7 @@ export class Dataset {
|
|
|
433
532
|
* depending on the mode of operation.
|
|
434
533
|
*/
|
|
435
534
|
async drop() {
|
|
436
|
-
|
|
535
|
+
rejectOperationInTransaction('Dataset.drop()');
|
|
437
536
|
await this.backend.drop();
|
|
438
537
|
serviceLocator.getStorageInstanceManager().removeFromCache(this);
|
|
439
538
|
}
|
|
@@ -453,7 +552,7 @@ export class Dataset {
|
|
|
453
552
|
* @param [options] Storage manager options.
|
|
454
553
|
*/
|
|
455
554
|
static async open(identifier, options = {}) {
|
|
456
|
-
|
|
555
|
+
tryCancel();
|
|
457
556
|
ow(options, ow.object.exactShape({
|
|
458
557
|
configuration: ow.optional.object.instanceOf(Configuration),
|
|
459
558
|
storageBackend: ow.optional.object,
|
package/storages/index.d.ts
CHANGED
|
@@ -8,6 +8,6 @@ export * from './request_queue.js';
|
|
|
8
8
|
export * from './storage_instance_manager.js';
|
|
9
9
|
export * from './storage_stats.js';
|
|
10
10
|
export * from './utils.js';
|
|
11
|
-
export * from './
|
|
11
|
+
export * from './transaction.js';
|
|
12
12
|
export * from './sitemap_request_loader.js';
|
|
13
13
|
export * from './request_manager_tandem.js';
|
package/storages/index.js
CHANGED
|
@@ -6,6 +6,6 @@ export * from './request_queue.js';
|
|
|
6
6
|
export * from './storage_instance_manager.js';
|
|
7
7
|
export * from './storage_stats.js';
|
|
8
8
|
export * from './utils.js';
|
|
9
|
-
export * from './
|
|
9
|
+
export * from './transaction.js';
|
|
10
10
|
export * from './sitemap_request_loader.js';
|
|
11
11
|
export * from './request_manager_tandem.js';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Awaitable, Dictionary, KeyValueStoreBackend, KeyValueStoreInfo } from '@crawlee/types';
|
|
2
2
|
import { Configuration } from '../configuration.js';
|
|
3
|
+
import type { JournalEntry } from './transaction.js';
|
|
3
4
|
import type { KeyValueStoreStats } from './storage_stats.js';
|
|
4
5
|
import type { StorageOpenOptions } from './utils.js';
|
|
5
6
|
import type { StorageIdentifier } from './storage_instance_manager.js';
|
|
@@ -60,14 +61,11 @@ import type { StorageIdentifier } from './storage_instance_manager.js';
|
|
|
60
61
|
* @category Result Stores
|
|
61
62
|
*/
|
|
62
63
|
export declare class KeyValueStore {
|
|
64
|
+
#private;
|
|
63
65
|
readonly configuration: Configuration;
|
|
64
66
|
readonly id: string;
|
|
65
67
|
readonly name?: string;
|
|
66
68
|
private readonly backend;
|
|
67
|
-
private persistStateEventStarted;
|
|
68
|
-
/** Cache for persistent (auto-saved) values. When we try to set such value, the cache will be updated automatically. */
|
|
69
|
-
private readonly cache;
|
|
70
|
-
private readonly statsTracker;
|
|
71
69
|
/**
|
|
72
70
|
* @internal
|
|
73
71
|
*/
|
|
@@ -141,6 +139,21 @@ export declare class KeyValueStore {
|
|
|
141
139
|
* on the MIME content type of the record, or the default value if the key is missing from the store.
|
|
142
140
|
*/
|
|
143
141
|
getValue<T = unknown>(key: string, defaultValue: T): Promise<T>;
|
|
142
|
+
/**
|
|
143
|
+
* The active transaction's last buffered write per key for this store, derived from its journal.
|
|
144
|
+
* An entry with a `null` value is a tombstone (an in-transaction deletion).
|
|
145
|
+
*/
|
|
146
|
+
private bufferedJournalEntries;
|
|
147
|
+
/**
|
|
148
|
+
* The single transaction-aware record read shared by `getValue`, `getRecord`, `recordExists` and the
|
|
149
|
+
* listing paths: buffered key → serialized through the standard codec (same fidelity as a real
|
|
150
|
+
* round-trip); tombstoned key → `null`; otherwise the backend.
|
|
151
|
+
*
|
|
152
|
+
* The per-key buffered lookup requires the whole journal to be reduced to a last-write-per-key map,
|
|
153
|
+
* which is O(journal). Single-record callers let it default (rebuilt per call); the listing paths,
|
|
154
|
+
* which read many keys, pass a map built once so the read stays O(1) per key instead of O(journal).
|
|
155
|
+
*/
|
|
156
|
+
private readRecord;
|
|
144
157
|
/**
|
|
145
158
|
* Reads a record from the key-value store without parsing the value.
|
|
146
159
|
*
|
|
@@ -223,6 +236,8 @@ export declare class KeyValueStore {
|
|
|
223
236
|
* @param [options] Record options.
|
|
224
237
|
*/
|
|
225
238
|
setValue<T>(key: string, value: T | null, options?: RecordOptions): Promise<void>;
|
|
239
|
+
/** @internal */
|
|
240
|
+
commitJournalEntries(entries: JournalEntry[]): Promise<void>;
|
|
226
241
|
/**
|
|
227
242
|
* Removes the key-value store either from the Apify cloud storage or from the local directory,
|
|
228
243
|
* depending on the mode of operation.
|