@crawlee/core 4.0.0-beta.104 → 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.
- 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/package.json +5 -5
- package/storages/dataset.d.ts +11 -0
- package/storages/dataset.js +118 -19
- package/storages/index.d.ts +1 -1
- package/storages/index.js +1 -1
- package/storages/key_value_store.d.ts +18 -0
- package/storages/key_value_store.js +154 -29
- package/storages/request_queue.d.ts +19 -0
- package/storages/request_queue.js +222 -21
- package/storages/transaction.d.ts +254 -0
- package/storages/transaction.js +251 -0
- package/storages/access_checking.d.ts +0 -12
- package/storages/access_checking.js +0 -17
|
@@ -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
|
|
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
|
-
|
|
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 {};
|
package/crawlers/index.d.ts
CHANGED
package/crawlers/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crawlee/core",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
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.
|
|
56
|
-
"@crawlee/types": "4.0.0-beta.
|
|
57
|
-
"@crawlee/utils": "4.0.0-beta.
|
|
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": "
|
|
81
|
+
"gitHead": "26073a822c7699ac487931383a39192bc3daae7a"
|
|
82
82
|
}
|
package/storages/dataset.d.ts
CHANGED
|
@@ -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.
|
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';
|
|
@@ -121,13 +122,24 @@ 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
|
}
|
|
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.
|
|
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';
|
|
@@ -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.
|