@crawlee/core 4.0.0-beta.190 → 4.0.0-beta.192

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crawlee/core",
3
- "version": "4.0.0-beta.190",
3
+ "version": "4.0.0-beta.192",
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.190",
57
- "@crawlee/http-client": "4.0.0-beta.190",
58
- "@crawlee/types": "4.0.0-beta.190",
59
- "@crawlee/utils": "4.0.0-beta.190",
56
+ "@crawlee/fs-storage": "4.0.0-beta.192",
57
+ "@crawlee/http-client": "4.0.0-beta.192",
58
+ "@crawlee/types": "4.0.0-beta.192",
59
+ "@crawlee/utils": "4.0.0-beta.192",
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": "0a6af5ff4704225ddb009a7dd58bdeaad48ebb14"
79
+ "gitHead": "729823422c7168238d8b5fcf38dcb798cbf0af0b"
80
80
  }
@@ -48,9 +48,9 @@ export interface RecoverableStatePersistenceOptions {
48
48
  persistenceTimeoutMillis?: number;
49
49
  }
50
50
  /**
51
- * Options for configuring the RecoverableState
51
+ * The fields of {@link RecoverableStateOptions}, without the constraint tying `contentType` to the conversions.
52
52
  */
53
- export interface RecoverableStateOptions<TStateModel = Record<string, unknown>, TPersistedState = TStateModel> extends RecoverableStatePersistenceOptions {
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
  *
@@ -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 () => keyValueStore.setValue(this.#persistStateKey, serializedState), 'Persisting the state');
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
- const storedState = await this.#withTimeout(async () => keyValueStore.getValue(this.#persistStateKey), 'Loading the persisted state');
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
  }