@crawlee/core 4.0.0-beta.190 → 4.0.0-beta.191
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/package.json +6 -6
- package/recoverable_state.d.ts +22 -2
- package/recoverable_state.js +24 -2
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.191",
|
|
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"
|
|
@@ -53,10 +53,10 @@
|
|
|
53
53
|
"@apify/log": "^3.0.1",
|
|
54
54
|
"@apify/timeout": "^1.0.1",
|
|
55
55
|
"@apify/utilities": "^3.0.1",
|
|
56
|
-
"@crawlee/fs-storage": "4.0.0-beta.
|
|
57
|
-
"@crawlee/http-client": "4.0.0-beta.
|
|
58
|
-
"@crawlee/types": "4.0.0-beta.
|
|
59
|
-
"@crawlee/utils": "4.0.0-beta.
|
|
56
|
+
"@crawlee/fs-storage": "4.0.0-beta.191",
|
|
57
|
+
"@crawlee/http-client": "4.0.0-beta.191",
|
|
58
|
+
"@crawlee/types": "4.0.0-beta.191",
|
|
59
|
+
"@crawlee/utils": "4.0.0-beta.191",
|
|
60
60
|
"@sapphire/async-queue": "^1.5.5",
|
|
61
61
|
"@standard-schema/spec": "^1.0.0",
|
|
62
62
|
"@vladfrangu/async_event_emitter": "^2.4.6",
|
|
@@ -76,5 +76,5 @@
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
},
|
|
79
|
-
"gitHead": "
|
|
79
|
+
"gitHead": "efc4b5504e9aa6d529cf0eb218152a3bfad6ebf1"
|
|
80
80
|
}
|
package/recoverable_state.d.ts
CHANGED
|
@@ -48,9 +48,9 @@ export interface RecoverableStatePersistenceOptions {
|
|
|
48
48
|
persistenceTimeoutMillis?: number;
|
|
49
49
|
}
|
|
50
50
|
/**
|
|
51
|
-
*
|
|
51
|
+
* The fields of {@link RecoverableStateOptions}, without the constraint tying `contentType` to the conversions.
|
|
52
52
|
*/
|
|
53
|
-
export interface
|
|
53
|
+
export interface RecoverableStateBaseOptions<TStateModel = Record<string, unknown>, TPersistedState = TStateModel> extends RecoverableStatePersistenceOptions {
|
|
54
54
|
/**
|
|
55
55
|
* The state used when no persisted state is found, and the state {@link RecoverableState.reset} restores.
|
|
56
56
|
*
|
|
@@ -69,14 +69,34 @@ export interface RecoverableStateOptions<TStateModel = Record<string, unknown>,
|
|
|
69
69
|
/**
|
|
70
70
|
* Optional conversion of the state to a plain JSON-serializable value before it is persisted.
|
|
71
71
|
* If not provided, the state is persisted as is.
|
|
72
|
+
*
|
|
73
|
+
* With {@link RecoverableStateBaseOptions.contentType} set, it has to produce what
|
|
74
|
+
* {@link KeyValueStore.setValue} accepts alongside an explicit content type - a `string`, a `Buffer` or a
|
|
75
|
+
* stream.
|
|
72
76
|
*/
|
|
73
77
|
serialize?: StateConversion<TStateModel, TPersistedState>;
|
|
74
78
|
/**
|
|
75
79
|
* Optional conversion of a persisted value back to the state model, and the place to validate a record before
|
|
76
80
|
* trusting it. If not provided, the persisted value is used as is.
|
|
81
|
+
*
|
|
82
|
+
* With {@link RecoverableStateBaseOptions.contentType} set, it receives a `Readable` of the record bytes
|
|
83
|
+
* instead of a parsed value.
|
|
77
84
|
*/
|
|
78
85
|
deserialize?: StateConversion<TPersistedState, TStateModel>;
|
|
86
|
+
/**
|
|
87
|
+
* Content type of the persisted record. Setting it hands the record encoding over to
|
|
88
|
+
* {@link RecoverableStateBaseOptions.serialize} and {@link RecoverableStateBaseOptions.deserialize}, both of
|
|
89
|
+
* which are then required - the default JSON codec is bypassed in both directions. Meant for a state too large
|
|
90
|
+
* for `JSON.stringify`, which `serialize` can then stream out instead.
|
|
91
|
+
*/
|
|
92
|
+
contentType?: string;
|
|
79
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Options for configuring the RecoverableState
|
|
96
|
+
*/
|
|
97
|
+
export type RecoverableStateOptions<TStateModel = Record<string, unknown>, TPersistedState = TStateModel> = RecoverableStateBaseOptions<TStateModel, TPersistedState> & ({
|
|
98
|
+
contentType?: undefined;
|
|
99
|
+
} | Required<Pick<RecoverableStateBaseOptions<TStateModel, TPersistedState>, 'serialize' | 'deserialize' | 'contentType'>>);
|
|
80
100
|
/**
|
|
81
101
|
* A class for managing persistent recoverable state using a plain JavaScript object.
|
|
82
102
|
*
|
package/recoverable_state.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
1
2
|
import { addTimeoutToPromise, storage as timeoutStorage } from '@apify/timeout';
|
|
2
3
|
import { StateValidationError } from './errors.js';
|
|
3
4
|
import { EventType } from './events/event_manager.js';
|
|
@@ -47,6 +48,7 @@ export class RecoverableState {
|
|
|
47
48
|
#log;
|
|
48
49
|
#serialize;
|
|
49
50
|
#deserialize;
|
|
51
|
+
#contentType;
|
|
50
52
|
#persistStateQuietly;
|
|
51
53
|
/**
|
|
52
54
|
* Initialize a new recoverable state object.
|
|
@@ -65,6 +67,10 @@ export class RecoverableState {
|
|
|
65
67
|
this.#configuration = options.configuration;
|
|
66
68
|
this.#keyValueStore = options.keyValueStore ?? null;
|
|
67
69
|
this.#log = options.logger ?? serviceLocator.getLogger().child({ prefix: 'RecoverableState' });
|
|
70
|
+
this.#contentType = options.contentType;
|
|
71
|
+
if (this.#contentType !== undefined && (options.serialize === undefined || options.deserialize === undefined)) {
|
|
72
|
+
throw new Error(`A 'contentType' for the state persisted under key '${this.#persistStateKey}' requires both 'serialize' and 'deserialize' - the record is no longer JSON the default codec can handle.`);
|
|
73
|
+
}
|
|
68
74
|
this.#serialize = this.#toConversion(options.serialize);
|
|
69
75
|
this.#deserialize = this.#toConversion(options.deserialize);
|
|
70
76
|
// A pending open() may reject before anything here awaits it. Observe the rejection so it surfaces on
|
|
@@ -203,7 +209,11 @@ export class RecoverableState {
|
|
|
203
209
|
}
|
|
204
210
|
this.#log.debug(`Persisting state of the RecoverableState (eventData=${JSON.stringify(eventData)}).`);
|
|
205
211
|
const serializedState = await this.#serialize(this.currentValue);
|
|
206
|
-
await this.#withTimeout(async () =>
|
|
212
|
+
await this.#withTimeout(async () => this.#contentType === undefined
|
|
213
|
+
? keyValueStore.setValue(this.#persistStateKey, serializedState)
|
|
214
|
+
: keyValueStore.setValue(this.#persistStateKey, serializedState, {
|
|
215
|
+
contentType: this.#contentType,
|
|
216
|
+
}), 'Persisting the state');
|
|
207
217
|
}
|
|
208
218
|
/** Falls back to the default store when none was supplied - persistence has to write somewhere. */
|
|
209
219
|
async #openKeyValueStore() {
|
|
@@ -231,7 +241,19 @@ export class RecoverableState {
|
|
|
231
241
|
if (keyValueStore === null) {
|
|
232
242
|
return;
|
|
233
243
|
}
|
|
234
|
-
|
|
244
|
+
// With a content type, the record is whatever `serialize` produced, so the codec's parse is skipped and
|
|
245
|
+
// `deserialize` gets the bytes as a stream - the shape a streaming parser wants.
|
|
246
|
+
// TODO: read the record as a stream instead of buffering it and wrapping (https://github.com/apify/crawlee/issues/2929).
|
|
247
|
+
const storedState = await this.#withTimeout(async () => {
|
|
248
|
+
if (this.#contentType === undefined) {
|
|
249
|
+
return keyValueStore.getValue(this.#persistStateKey);
|
|
250
|
+
}
|
|
251
|
+
const record = await keyValueStore.getRecord(this.#persistStateKey);
|
|
252
|
+
if (record === null) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
return Readable.from(Buffer.isBuffer(record.value) ? record.value : Buffer.from(record.value));
|
|
256
|
+
}, 'Loading the persisted state');
|
|
235
257
|
if (storedState === null || storedState === undefined) {
|
|
236
258
|
return;
|
|
237
259
|
}
|