@crawlee/core 4.0.0-beta.103 → 4.0.0-beta.105

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.
@@ -1,12 +1,11 @@
1
1
  import type { Dictionary, HttpRequestOptions, ISession, ProxyInfo, SendRequestOptions } from '@crawlee/types';
2
2
  import type { ReadonlyDeep, SetRequired } from 'type-fest';
3
- import type { Configuration } from '../configuration.js';
4
3
  import type { EnqueueLinksOptions } from '../enqueue_links/enqueue_links.js';
5
4
  import type { CrawleeLogger } from '../log.js';
6
5
  import type { Request, RequestOptions, Source } from '../request.js';
7
6
  import type { StorageIdentifier } from '../storages/storage_instance_manager.js';
8
7
  import type { Dataset } from '../storages/dataset.js';
9
- import { KeyValueStore, type RecordOptions } from '../storages/key_value_store.js';
8
+ import type { KeyValueStore } from '../storages/key_value_store.js';
10
9
  import type { RequestQueueOperationOptions } from '../storages/request_queue.js';
11
10
  /** @internal */
12
11
  export type IsAny<T> = 0 extends 1 & T ? true : false;
@@ -176,6 +175,11 @@ export interface CrawlingContext<UserData extends Dictionary = Dictionary> exten
176
175
  sendRequest: (requestOverrides?: Partial<HttpRequestOptions>, optionsOverrides?: SendRequestOptions) => Promise<Response>;
177
176
  /**
178
177
  * Register a function to be called at the very end of the request handling process. This is useful for resources that should be accessible to error handlers, for instance.
178
+ *
179
+ * The callback runs *outside* the request's storage transaction, so storage writes made here are
180
+ * applied immediately and are **not** rolled back when the request fails. In
181
+ * {@link AdaptivePlaywrightCrawler} it also runs once per request handler attempt, so a write
182
+ * here can land more than once for a single request. Push results from the request handler itself.
179
183
  */
180
184
  registerDeferredCleanup(cleanup: () => Promise<unknown>): void;
181
185
  /**
@@ -198,58 +202,4 @@ export interface CrawlingContext<UserData extends Dictionary = Dictionary> exten
198
202
  */
199
203
  extendTimeout(secs: number): void;
200
204
  }
201
- /**
202
- * A partial implementation of {@link RestrictedCrawlingContext} that stores parameters of calls to context methods for later inspection.
203
- *
204
- * @experimental
205
- */
206
- export declare class RequestHandlerResult {
207
- private configuration;
208
- private crawleeStateKey;
209
- private _keyValueStoreChanges;
210
- private pushDataCalls;
211
- private addRequestsCalls;
212
- constructor(configuration: Configuration, crawleeStateKey: string);
213
- /**
214
- * A record of calls to {@link RestrictedCrawlingContext.pushData}, {@link RestrictedCrawlingContext.addRequests}, {@link RestrictedCrawlingContext.enqueueLinks} made by a request handler.
215
- */
216
- get calls(): ReadonlyDeep<{
217
- pushData: Parameters<RestrictedCrawlingContext['pushData']>[];
218
- addRequests: Parameters<RestrictedCrawlingContext['addRequests']>[];
219
- }>;
220
- /**
221
- * A record of changes made to key-value stores by a request handler.
222
- */
223
- get keyValueStoreChanges(): ReadonlyDeep<Record<string, Record<string, {
224
- changedValue: unknown;
225
- options?: RecordOptions;
226
- }>>>;
227
- /**
228
- * Items added to datasets by a request handler.
229
- */
230
- get datasetItems(): ReadonlyDeep<{
231
- item: Dictionary;
232
- datasetIdentifier?: string | StorageIdentifier;
233
- }[]>;
234
- /**
235
- * URLs enqueued to the request queue by a request handler, either via {@link RestrictedCrawlingContext.addRequests} or {@link RestrictedCrawlingContext.enqueueLinks}
236
- */
237
- get enqueuedUrls(): ReadonlyDeep<{
238
- url: string;
239
- label?: string;
240
- }[]>;
241
- /**
242
- * URL lists enqueued to the request queue by a request handler via {@link RestrictedCrawlingContext.addRequests} using the `requestsFromUrl` option.
243
- */
244
- get enqueuedUrlLists(): ReadonlyDeep<{
245
- listUrl: string;
246
- label?: string;
247
- }[]>;
248
- pushData: RestrictedCrawlingContext['pushData'];
249
- addRequests: RestrictedCrawlingContext['addRequests'];
250
- useState: RestrictedCrawlingContext['useState'];
251
- getKeyValueStore: RestrictedCrawlingContext['getKeyValueStore'];
252
- private getKeyValueStoreChangedValue;
253
- private setKeyValueStoreChangedValue;
254
- }
255
205
  export {};
@@ -1,107 +1 @@
1
- import { KeyValueStore } from '../storages/key_value_store.js';
2
- /**
3
- * A partial implementation of {@link RestrictedCrawlingContext} that stores parameters of calls to context methods for later inspection.
4
- *
5
- * @experimental
6
- */
7
- export class RequestHandlerResult {
8
- configuration;
9
- crawleeStateKey;
10
- _keyValueStoreChanges = {};
11
- pushDataCalls = [];
12
- addRequestsCalls = [];
13
- constructor(configuration, crawleeStateKey) {
14
- this.configuration = configuration;
15
- this.crawleeStateKey = crawleeStateKey;
16
- }
17
- /**
18
- * A record of calls to {@link RestrictedCrawlingContext.pushData}, {@link RestrictedCrawlingContext.addRequests}, {@link RestrictedCrawlingContext.enqueueLinks} made by a request handler.
19
- */
20
- get calls() {
21
- return {
22
- pushData: this.pushDataCalls,
23
- addRequests: this.addRequestsCalls,
24
- };
25
- }
26
- /**
27
- * A record of changes made to key-value stores by a request handler.
28
- */
29
- get keyValueStoreChanges() {
30
- return this._keyValueStoreChanges;
31
- }
32
- /**
33
- * Items added to datasets by a request handler.
34
- */
35
- get datasetItems() {
36
- return this.pushDataCalls.flatMap(([data, datasetIdentifier]) => (Array.isArray(data) ? data : [data]).map((item) => ({ item, datasetIdentifier })));
37
- }
38
- /**
39
- * URLs enqueued to the request queue by a request handler, either via {@link RestrictedCrawlingContext.addRequests} or {@link RestrictedCrawlingContext.enqueueLinks}
40
- */
41
- get enqueuedUrls() {
42
- const result = [];
43
- for (const [requests] of this.addRequestsCalls) {
44
- for (const request of requests) {
45
- if (typeof request === 'object' &&
46
- (!('requestsFromUrl' in request) || request.requestsFromUrl !== undefined) &&
47
- request.url !== undefined) {
48
- result.push({ url: request.url, label: request.label });
49
- }
50
- else if (typeof request === 'string') {
51
- result.push({ url: request });
52
- }
53
- }
54
- }
55
- return result;
56
- }
57
- /**
58
- * URL lists enqueued to the request queue by a request handler via {@link RestrictedCrawlingContext.addRequests} using the `requestsFromUrl` option.
59
- */
60
- get enqueuedUrlLists() {
61
- const result = [];
62
- for (const [requests] of this.addRequestsCalls) {
63
- for (const request of requests) {
64
- if (typeof request === 'object' &&
65
- 'requestsFromUrl' in request &&
66
- request.requestsFromUrl !== undefined) {
67
- result.push({ listUrl: request.requestsFromUrl, label: request.label });
68
- }
69
- }
70
- }
71
- return result;
72
- }
73
- pushData = async (data, datasetIdOrName) => {
74
- this.pushDataCalls.push([data, datasetIdOrName]);
75
- };
76
- addRequests = async (requests, options = {}) => {
77
- this.addRequestsCalls.push([requests, options]);
78
- };
79
- useState = async (defaultValue) => {
80
- const store = await this.getKeyValueStore(undefined);
81
- return await store.getAutoSavedValue(this.crawleeStateKey, defaultValue);
82
- };
83
- getKeyValueStore = async (identifier) => {
84
- const store = await KeyValueStore.open(identifier, { configuration: this.configuration });
85
- const storeId = store.id;
86
- return {
87
- id: storeId ?? this.configuration.defaultKeyValueStoreId,
88
- name: store.name,
89
- getValue: async (key) => this.getKeyValueStoreChangedValue(storeId, key) ?? (await store.getValue(key)),
90
- setValue: async (key, value, options) => {
91
- this.setKeyValueStoreChangedValue(storeId, key, value, options);
92
- },
93
- getAutoSavedValue: store.getAutoSavedValue.bind(store),
94
- getPublicUrl: store.getPublicUrl.bind(store),
95
- };
96
- };
97
- getKeyValueStoreChangedValue = (storeKey, key) => {
98
- const id = storeKey ?? this.configuration.defaultKeyValueStoreId;
99
- this._keyValueStoreChanges[id] ??= {};
100
- return this.keyValueStoreChanges[id][key]?.changedValue ?? null;
101
- };
102
- setKeyValueStoreChangedValue = (storeKey, key, changedValue, options) => {
103
- const id = storeKey ?? this.configuration.defaultKeyValueStoreId;
104
- this._keyValueStoreChanges[id] ??= {};
105
- this._keyValueStoreChanges[id][key] = { changedValue, options };
106
- };
107
- }
1
+ export {};
@@ -1,5 +1,5 @@
1
1
  export * from './context_pipeline.js';
2
- export * from './crawler_commons.js';
2
+ export type * from './crawler_commons.js';
3
3
  export * from './statistics.js';
4
4
  export * from './error_tracker.js';
5
5
  export * from './error_snapshotter.js';
package/crawlers/index.js CHANGED
@@ -1,5 +1,4 @@
1
1
  export * from './context_pipeline.js';
2
- export * from './crawler_commons.js';
3
2
  export * from './statistics.js';
4
3
  export * from './error_tracker.js';
5
4
  export * from './error_snapshotter.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crawlee/core",
3
- "version": "4.0.0-beta.103",
3
+ "version": "4.0.0-beta.105",
4
4
  "description": "The scalable web crawling and scraping library for JavaScript/Node.js. Enables development of data extraction and web automation jobs (not only) with headless Chrome and Puppeteer.",
5
5
  "engines": {
6
6
  "node": ">=22.0.0"
@@ -52,9 +52,9 @@
52
52
  "@apify/log": "^2.5.18",
53
53
  "@apify/timeout": "^0.4.4",
54
54
  "@apify/utilities": "^2.15.5",
55
- "@crawlee/fs-storage": "4.0.0-beta.103",
56
- "@crawlee/types": "4.0.0-beta.103",
57
- "@crawlee/utils": "4.0.0-beta.103",
55
+ "@crawlee/fs-storage": "4.0.0-beta.105",
56
+ "@crawlee/types": "4.0.0-beta.105",
57
+ "@crawlee/utils": "4.0.0-beta.105",
58
58
  "@sapphire/async-queue": "^1.5.5",
59
59
  "@sapphire/shapeshift": "^4.0.0",
60
60
  "@vladfrangu/async_event_emitter": "^2.4.6",
@@ -78,5 +78,5 @@
78
78
  }
79
79
  }
80
80
  },
81
- "gitHead": "fe5d0ae11067683e27a2d17b4edd226ccf112cf5"
81
+ "gitHead": "26073a822c7699ac487931383a39192bc3daae7a"
82
82
  }
@@ -1,6 +1,7 @@
1
1
  import type { Awaitable, DatasetBackend, DatasetInfo, Dictionary } from '@crawlee/types';
2
2
  import { Configuration } from '../configuration.js';
3
3
  import type { CrawleeLogger } from '../log.js';
4
+ import type { JournalEntry } from './transaction.js';
4
5
  import type { DatasetStats } from './storage_stats.js';
5
6
  import type { StorageOpenOptions } from './utils.js';
6
7
  import type { StorageIdentifier } from './storage_instance_manager.js';
@@ -170,6 +171,16 @@ export declare class Dataset<Data extends Dictionary = Dictionary> {
170
171
  * Returns {@link DatasetContent} object holding the items in the dataset based on the provided parameters.
171
172
  */
172
173
  getData(options?: DatasetDataOptions): Promise<DatasetContent<Data>>;
174
+ /**
175
+ * The single transaction-aware page read all dataset read paths go through — both `getData()` and
176
+ * the private `fetchPages()`. Returns the real page concatenated with the current transaction's
177
+ * buffered items, with `offset` / `limit` / `desc` windowing applied across the concatenation.
178
+ */
179
+ private readPage;
180
+ /** The active transaction's buffered writes to this dataset, derived from its journal. */
181
+ private bufferedJournalEntries;
182
+ /** @internal */
183
+ commitJournalEntries(entries: JournalEntry[]): Promise<void>;
173
184
  /**
174
185
  * Returns all the data from the dataset. This will iterate through the whole dataset
175
186
  * via the `listItems()` client method, which gives you only paginated results.
@@ -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 { checkStorageAccess } from './access_checking.js';
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';
@@ -121,13 +122,24 @@ export class Dataset {
121
122
  * The objects must be serializable to JSON.
122
123
  */
123
124
  async pushData(data) {
124
- checkStorageAccess();
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
  }
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
+ }
131
143
  this.statsTracker.add('writeCount');
132
144
  await this.backend.pushData(items);
133
145
  }
@@ -135,10 +147,8 @@ export class Dataset {
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.statsTracker.add('readCount');
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
- checkStorageAccess();
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
- checkStorageAccess();
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
- checkStorageAccess();
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
- checkStorageAccess();
256
- return this.backend.getMetadata();
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
- checkStorageAccess();
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
- checkStorageAccess();
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
- checkStorageAccess();
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.statsTracker.add('readCount');
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
- checkStorageAccess();
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
- checkStorageAccess();
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
- checkStorageAccess();
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
- checkStorageAccess();
555
+ tryCancel();
457
556
  ow(options, ow.object.exactShape({
458
557
  configuration: ow.optional.object.instanceOf(Configuration),
459
558
  storageBackend: ow.optional.object,
@@ -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 './access_checking.js';
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 './access_checking.js';
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';
@@ -141,6 +142,21 @@ export declare class KeyValueStore {
141
142
  * on the MIME content type of the record, or the default value if the key is missing from the store.
142
143
  */
143
144
  getValue<T = unknown>(key: string, defaultValue: T): Promise<T>;
145
+ /**
146
+ * The active transaction's last buffered write per key for this store, derived from its journal.
147
+ * An entry with a `null` value is a tombstone (an in-transaction deletion).
148
+ */
149
+ private bufferedJournalEntries;
150
+ /**
151
+ * The single transaction-aware record read shared by `getValue`, `getRecord`, `recordExists` and the
152
+ * listing paths: buffered key → serialized through the standard codec (same fidelity as a real
153
+ * round-trip); tombstoned key → `null`; otherwise the backend.
154
+ *
155
+ * The per-key buffered lookup requires the whole journal to be reduced to a last-write-per-key map,
156
+ * which is O(journal). Single-record callers let it default (rebuilt per call); the listing paths,
157
+ * which read many keys, pass a map built once so the read stays O(1) per key instead of O(journal).
158
+ */
159
+ private readRecord;
144
160
  /**
145
161
  * Reads a record from the key-value store without parsing the value.
146
162
  *
@@ -223,6 +239,8 @@ export declare class KeyValueStore {
223
239
  * @param [options] Record options.
224
240
  */
225
241
  setValue<T>(key: string, value: T | null, options?: RecordOptions): Promise<void>;
242
+ /** @internal */
243
+ commitJournalEntries(entries: JournalEntry[]): Promise<void>;
226
244
  /**
227
245
  * Removes the key-value store either from the Apify cloud storage or from the local directory,
228
246
  * depending on the mode of operation.