@platforma-sdk/model 1.53.0 → 1.53.2

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,112 +1,408 @@
1
1
  import { tryRegisterCallback } from './internal.js';
2
- import { createBlockStorage } from './block_storage.js';
2
+ import { DATA_MODEL_DEFAULT_VERSION, createBlockStorage } from './block_storage.js';
3
3
 
4
- /** Internal builder for chaining migrations */
5
- class DataModelBuilder {
4
+ /**
5
+ * Helper to define version keys with literal type inference and runtime validation.
6
+ * - Validates that all version values are unique
7
+ * - Validates that no version value is empty
8
+ * - Eliminates need for `as const` assertion
9
+ *
10
+ * @throws Error if duplicate or empty version values are found
11
+ *
12
+ * @example
13
+ * const Version = defineDataVersions({
14
+ * Initial: 'v1',
15
+ * AddedLabels: 'v2',
16
+ * });
17
+ *
18
+ * type VersionedData = {
19
+ * [Version.Initial]: DataV1;
20
+ * [Version.AddedLabels]: DataV2;
21
+ * };
22
+ */
23
+ function defineDataVersions(versions) {
24
+ const values = Object.values(versions);
25
+ const keys = Object.keys(versions);
26
+ const emptyKeys = keys.filter((key) => versions[key] === '');
27
+ if (emptyKeys.length > 0) {
28
+ throw new Error(`Version values must be non-empty strings (empty: ${emptyKeys.join(', ')})`);
29
+ }
30
+ const unique = new Set(values);
31
+ if (unique.size !== values.length) {
32
+ const duplicates = values.filter((v, i) => values.indexOf(v) !== i);
33
+ throw new Error(`Duplicate version values: ${[...new Set(duplicates)].join(', ')}`);
34
+ }
35
+ return versions;
36
+ }
37
+ /** Create a DataVersioned wrapper with correct shape */
38
+ function makeDataVersioned(version, data) {
39
+ return { version, data };
40
+ }
41
+ /** Thrown by recover() to signal unrecoverable data. */
42
+ class DataUnrecoverableError extends Error {
43
+ name = 'DataUnrecoverableError';
44
+ constructor(dataVersion) {
45
+ super(`Unknown version '${dataVersion}'`);
46
+ }
47
+ }
48
+ function isDataUnrecoverableError(error) {
49
+ return error instanceof Error && error.name === 'DataUnrecoverableError';
50
+ }
51
+ /**
52
+ * Default recover function for unknown versions.
53
+ * Use as fallback at the end of custom recover functions.
54
+ *
55
+ * @example
56
+ * .recover((version, data) => {
57
+ * if (version === 'legacy') {
58
+ * return transformLegacyData(data);
59
+ * }
60
+ * return defaultRecover(version, data);
61
+ * })
62
+ */
63
+ const defaultRecover = (version, _data) => {
64
+ throw new DataUnrecoverableError(version);
65
+ };
66
+ /** Symbol for internal builder creation method */
67
+ const FROM_BUILDER = Symbol('fromBuilder');
68
+ /**
69
+ * Final builder state after recover() is called.
70
+ * Only allows calling create() to finalize the DataModel.
71
+ *
72
+ * @typeParam VersionedData - Map of version keys to their data types
73
+ * @typeParam CurrentVersion - The current (final) version in the chain
74
+ * @internal
75
+ */
76
+ class DataModelBuilderWithRecover {
77
+ versionChain;
6
78
  migrationSteps;
7
- constructor(steps = []) {
79
+ recoverFn;
80
+ /** @internal */
81
+ constructor({ versionChain, steps, recoverFn, }) {
82
+ this.versionChain = versionChain;
8
83
  this.migrationSteps = steps;
84
+ this.recoverFn = recoverFn;
85
+ }
86
+ /**
87
+ * Finalize the DataModel with initial data factory.
88
+ *
89
+ * The initial data factory is called when creating new blocks or when
90
+ * migration/recovery fails and data must be reset.
91
+ *
92
+ * @param initialData - Factory function returning initial state (must exactly match CurrentVersion's data type)
93
+ * @returns Finalized DataModel instance
94
+ *
95
+ * @example
96
+ * .init(() => ({ numbers: [], labels: [], description: '' }))
97
+ */
98
+ init(initialData,
99
+ // Compile-time check: S must have exactly the same keys as VersionedData[CurrentVersion]
100
+ ..._noExtraKeys) {
101
+ return DataModel[FROM_BUILDER]({
102
+ versionChain: this.versionChain,
103
+ steps: this.migrationSteps,
104
+ initialDataFn: initialData,
105
+ recoverFn: this.recoverFn,
106
+ });
9
107
  }
10
- /** Start a migration chain from an initial type */
11
- static from() {
12
- return new DataModelBuilder();
108
+ }
109
+ /**
110
+ * Internal builder for constructing DataModel with type-safe migration chains.
111
+ *
112
+ * Tracks the current version through the generic type system, ensuring:
113
+ * - Migration functions receive correctly typed input
114
+ * - Migration functions must return the correct output type
115
+ * - Version keys must exist in the VersionedData map
116
+ * - All versions must be covered before calling init()
117
+ *
118
+ * @typeParam VersionedData - Map of version keys to their data types
119
+ * @typeParam CurrentVersion - The current version in the migration chain
120
+ * @typeParam RemainingVersions - Versions not yet covered by migrations
121
+ * @internal
122
+ */
123
+ class DataModelMigrationChain {
124
+ versionChain;
125
+ migrationSteps;
126
+ /** @internal */
127
+ constructor({ versionChain, steps = [], }) {
128
+ this.versionChain = versionChain;
129
+ this.migrationSteps = steps;
13
130
  }
14
- /** Add a migration step */
15
- migrate(fn) {
16
- return new DataModelBuilder([...this.migrationSteps, fn]);
131
+ /**
132
+ * Add a migration step to transform data from current version to next version.
133
+ *
134
+ * Migration functions:
135
+ * - Receive data typed as the current version's data type (readonly)
136
+ * - Must return data matching the target version's data type
137
+ * - Should be pure functions (no side effects)
138
+ * - May throw errors (will result in data reset with warning)
139
+ *
140
+ * @typeParam NextVersion - The target version key (must be in RemainingVersions)
141
+ * @param nextVersion - The version key to migrate to
142
+ * @param fn - Migration function transforming current data to next version
143
+ * @returns Builder with updated current version
144
+ *
145
+ * @example
146
+ * .migrate(Version.V2, (data) => ({ ...data, labels: [] }))
147
+ */
148
+ migrate(nextVersion, fn) {
149
+ if (this.versionChain.includes(nextVersion)) {
150
+ throw new Error(`Duplicate version '${nextVersion}' in migration chain`);
151
+ }
152
+ const fromVersion = this.versionChain[this.versionChain.length - 1];
153
+ const step = { fromVersion, toVersion: nextVersion, migrate: fn };
154
+ return new DataModelMigrationChain({
155
+ versionChain: [...this.versionChain, nextVersion],
156
+ steps: [...this.migrationSteps, step],
157
+ });
17
158
  }
18
- /** Finalize with initial data, creating the DataModel */
19
- create(initialData, ..._) {
20
- return DataModel._fromBuilder(this.migrationSteps, initialData);
159
+ /**
160
+ * Set a recovery handler for unknown or legacy versions.
161
+ *
162
+ * The recover function is called when data has a version not in the migration chain.
163
+ * It should either:
164
+ * - Transform the data to the current version's format and return it
165
+ * - Call `defaultRecover(version, data)` to signal unrecoverable data
166
+ *
167
+ * Can only be called once. After calling, only `init()` is available.
168
+ *
169
+ * @param fn - Recovery function that transforms unknown data or throws
170
+ * @returns Builder with only init() method available
171
+ *
172
+ * @example
173
+ * .recover((version, data) => {
174
+ * if (version === 'legacy' && isLegacyFormat(data)) {
175
+ * return transformLegacy(data);
176
+ * }
177
+ * return defaultRecover(version, data);
178
+ * })
179
+ */
180
+ recover(fn) {
181
+ return new DataModelBuilderWithRecover({
182
+ versionChain: [...this.versionChain],
183
+ steps: [...this.migrationSteps],
184
+ recoverFn: fn,
185
+ });
186
+ }
187
+ /**
188
+ * Finalize the DataModel with initial data factory.
189
+ *
190
+ * Can only be called when all versions in VersionedData have been covered
191
+ * by the migration chain (RemainingVersions is empty).
192
+ *
193
+ * The initial data factory is called when creating new blocks or when
194
+ * migration/recovery fails and data must be reset.
195
+ *
196
+ * @param initialData - Factory function returning initial state (must exactly match CurrentVersion's data type)
197
+ * @returns Finalized DataModel instance
198
+ *
199
+ * @example
200
+ * .init(() => ({ numbers: [], labels: [], description: '' }))
201
+ */
202
+ init(initialData,
203
+ // Compile-time check: S must have exactly the same keys as VersionedData[CurrentVersion]
204
+ ..._noExtraKeys) {
205
+ return DataModel[FROM_BUILDER]({
206
+ versionChain: this.versionChain,
207
+ steps: this.migrationSteps,
208
+ initialDataFn: initialData,
209
+ });
210
+ }
211
+ }
212
+ /**
213
+ * Builder entry point for creating DataModel with type-safe migrations.
214
+ *
215
+ * @typeParam VersionedData - Map of version keys to their data types
216
+ *
217
+ * @example
218
+ * const Version = defineDataVersions({
219
+ * V1: 'v1',
220
+ * V2: 'v2',
221
+ * });
222
+ *
223
+ * type VersionedData = {
224
+ * [Version.V1]: { count: number };
225
+ * [Version.V2]: { count: number; label: string };
226
+ * };
227
+ *
228
+ * const dataModel = new DataModelBuilder<VersionedData>()
229
+ * .from(Version.V1)
230
+ * .migrate(Version.V2, (data) => ({ ...data, label: '' }))
231
+ * .init(() => ({ count: 0, label: '' }));
232
+ */
233
+ class DataModelBuilder {
234
+ /**
235
+ * Start a migration chain from an initial version.
236
+ *
237
+ * @typeParam InitialVersion - The starting version key (inferred from argument)
238
+ * @param initialVersion - The version key to start from
239
+ * @returns Migration chain builder for adding migrations
240
+ *
241
+ * @example
242
+ * new DataModelBuilder<VersionedData>()
243
+ * .from(Version.V1)
244
+ * .migrate(Version.V2, (data) => ({ ...data, newField: '' }))
245
+ */
246
+ from(initialVersion) {
247
+ return new DataModelMigrationChain({ versionChain: [initialVersion] });
21
248
  }
22
249
  }
23
250
  /**
24
251
  * DataModel defines the block's data structure, initial values, and migrations.
25
252
  * Used by BlockModelV3 to manage data state.
26
253
  *
254
+ * Two ways to create a DataModel:
255
+ *
256
+ * 1. **Simple (no migrations)** - Use `DataModel.create()`:
27
257
  * @example
28
- * // Simple data model (no migrations)
29
258
  * const dataModel = DataModel.create<BlockData>(() => ({
30
259
  * numbers: [],
31
260
  * labels: [],
32
261
  * }));
33
262
  *
34
- * // Data model with migrations
35
- * const dataModel = DataModel
36
- * .from<V1>()
37
- * .migrate((data) => ({ ...data, labels: [] })) // v1 → v2
38
- * .migrate((data) => ({ ...data, description: '' })) // v2 → v3
39
- * .create<BlockData>(() => ({ numbers: [], labels: [], description: '' }));
263
+ * 2. **With migrations** - Use `new DataModelBuilder<VersionedData>()`:
264
+ * @example
265
+ * const Version = defineDataVersions({
266
+ * V1: 'v1',
267
+ * V2: 'v2',
268
+ * V3: 'v3',
269
+ * });
270
+ *
271
+ * type VersionedData = {
272
+ * [Version.V1]: { numbers: number[] };
273
+ * [Version.V2]: { numbers: number[]; labels: string[] };
274
+ * [Version.V3]: { numbers: number[]; labels: string[]; description: string };
275
+ * };
276
+ *
277
+ * const dataModel = new DataModelBuilder<VersionedData>()
278
+ * .from(Version.V1)
279
+ * .migrate(Version.V2, (data) => ({ ...data, labels: [] }))
280
+ * .migrate(Version.V3, (data) => ({ ...data, description: '' }))
281
+ * .recover((version, data) => {
282
+ * if (version === 'legacy' && typeof data === 'object' && data !== null && 'numbers' in data) {
283
+ * return { numbers: (data as { numbers: number[] }).numbers, labels: [], description: '' };
284
+ * }
285
+ * return defaultRecover(version, data);
286
+ * })
287
+ * .init(() => ({ numbers: [], labels: [], description: '' }));
40
288
  */
41
289
  class DataModel {
290
+ versionChain;
42
291
  steps;
43
- _initialData;
44
- constructor(steps, initialData) {
292
+ initialDataFn;
293
+ recoverFn;
294
+ constructor({ versionChain, steps, initialDataFn, recoverFn = defaultRecover, }) {
295
+ if (versionChain.length === 0) {
296
+ throw new Error('DataModel requires at least one version key');
297
+ }
298
+ this.versionChain = versionChain;
45
299
  this.steps = steps;
46
- this._initialData = initialData;
47
- }
48
- /** Start a migration chain from an initial type */
49
- static from() {
50
- return DataModelBuilder.from();
300
+ this.initialDataFn = initialDataFn;
301
+ this.recoverFn = recoverFn;
51
302
  }
52
- /** Create a data model with just initial data (no migrations) */
53
- static create(initialData) {
54
- return new DataModel([], initialData);
303
+ /**
304
+ * Create a DataModel with just initial data (no migrations).
305
+ *
306
+ * Use this for simple blocks that don't need version migrations.
307
+ * The version will be set to an internal default value.
308
+ *
309
+ * @typeParam S - The state type
310
+ * @param initialData - Factory function returning initial state
311
+ * @param version - Optional custom version key (defaults to internal version)
312
+ * @returns Finalized DataModel instance
313
+ *
314
+ * @example
315
+ * const dataModel = DataModel.create<BlockData>(() => ({
316
+ * numbers: [],
317
+ * labels: [],
318
+ * }));
319
+ */
320
+ static create(initialData, version = DATA_MODEL_DEFAULT_VERSION) {
321
+ return new DataModel({
322
+ versionChain: [version],
323
+ steps: [],
324
+ initialDataFn: initialData,
325
+ });
55
326
  }
56
- /** Create from builder (internal use) */
57
- static _fromBuilder(steps, initialData) {
58
- return new DataModel(steps, initialData);
327
+ /**
328
+ * Internal method for creating DataModel from builder.
329
+ * Uses Symbol key to prevent external access.
330
+ * @internal
331
+ */
332
+ static [FROM_BUILDER](state) {
333
+ return new DataModel(state);
59
334
  }
60
335
  /**
61
- * Latest version number.
62
- * Version 1 = initial state, each migration adds 1.
336
+ * The latest (current) version key in the migration chain.
63
337
  */
64
338
  get version() {
65
- return this.steps.length + 1;
339
+ return this.versionChain[this.versionChain.length - 1];
66
340
  }
67
- /** Number of migration steps */
341
+ /**
342
+ * Number of migration steps defined.
343
+ */
68
344
  get migrationCount() {
69
345
  return this.steps.length;
70
346
  }
71
- /** Get initial data */
347
+ /**
348
+ * Get a fresh copy of the initial data.
349
+ */
72
350
  initialData() {
73
- return this._initialData();
351
+ return this.initialDataFn();
74
352
  }
75
- /** Get default data wrapped with current version */
353
+ /**
354
+ * Get initial data wrapped with current version.
355
+ * Used when creating new blocks or resetting to defaults.
356
+ */
76
357
  getDefaultData() {
77
- return { version: this.version, data: this._initialData() };
358
+ return makeDataVersioned(this.version, this.initialDataFn());
359
+ }
360
+ recoverFrom(data, version) {
361
+ try {
362
+ return { version: this.version, data: this.recoverFn(version, data) };
363
+ }
364
+ catch (error) {
365
+ if (isDataUnrecoverableError(error)) {
366
+ return { ...this.getDefaultData(), warning: error.message };
367
+ }
368
+ const errorMessage = error instanceof Error ? error.message : String(error);
369
+ return {
370
+ ...this.getDefaultData(),
371
+ warning: `Recover failed for version '${version}': ${errorMessage}`,
372
+ };
373
+ }
78
374
  }
79
375
  /**
80
- * Upgrade versioned data from any version to the latest.
81
- * Applies only the migrations needed (skips already-applied ones).
82
- * If a migration fails, returns default data with a warning.
376
+ * Migrate versioned data from any version to the latest.
377
+ *
378
+ * - If data is already at latest version, returns as-is
379
+ * - If version is in chain, applies needed migrations
380
+ * - If version is unknown, calls recover function
381
+ * - If migration/recovery fails, returns default data with warning
382
+ *
383
+ * @param versioned - Data with version tag
384
+ * @returns Migration result with data at latest version
83
385
  */
84
- upgrade(versioned) {
386
+ migrate(versioned) {
85
387
  const { version: fromVersion, data } = versioned;
86
- if (fromVersion > this.version) {
87
- throw new Error(`Cannot downgrade from version ${fromVersion} to ${this.version}`);
88
- }
89
388
  if (fromVersion === this.version) {
90
389
  return { version: this.version, data: data };
91
390
  }
92
- // Apply migrations starting from (fromVersion - 1) index
93
- // Version 1 -> no migrations applied yet -> start at index 0
94
- // Version 2 -> migration[0] already applied -> start at index 1
95
- const startIndex = fromVersion - 1;
96
- const migrationsToApply = this.steps.slice(startIndex);
391
+ const startIndex = this.versionChain.indexOf(fromVersion);
392
+ if (startIndex < 0) {
393
+ return this.recoverFrom(data, fromVersion);
394
+ }
97
395
  let currentData = data;
98
- for (let i = 0; i < migrationsToApply.length; i++) {
99
- const stepIndex = startIndex + i;
100
- const fromVer = stepIndex + 1;
101
- const toVer = stepIndex + 2;
396
+ for (let i = startIndex; i < this.steps.length; i++) {
397
+ const step = this.steps[i];
102
398
  try {
103
- currentData = migrationsToApply[i](currentData);
399
+ currentData = step.migrate(currentData);
104
400
  }
105
401
  catch (error) {
106
402
  const errorMessage = error instanceof Error ? error.message : String(error);
107
403
  return {
108
404
  ...this.getDefaultData(),
109
- warning: `Migration v${fromVer}→v${toVer} failed: ${errorMessage}`,
405
+ warning: `Migration ${step.fromVersion}→${step.toVersion} failed: ${errorMessage}`,
110
406
  };
111
407
  }
112
408
  }
@@ -116,14 +412,11 @@ class DataModel {
116
412
  * Register callbacks for use in the VM.
117
413
  * Called by BlockModelV3.create() to set up internal callbacks.
118
414
  *
119
- * All callbacks are prefixed with `__pl_` to indicate internal SDK use:
120
- * - `__pl_data_initial`: returns initial data for new blocks
121
- * - `__pl_data_upgrade`: upgrades versioned data from any version to latest
122
- * - `__pl_storage_initial`: returns initial BlockStorage as JSON string
415
+ * @internal
123
416
  */
124
417
  registerCallbacks() {
125
- tryRegisterCallback('__pl_data_initial', () => this._initialData());
126
- tryRegisterCallback('__pl_data_upgrade', (versioned) => this.upgrade(versioned));
418
+ tryRegisterCallback('__pl_data_initial', () => this.initialDataFn());
419
+ tryRegisterCallback('__pl_data_upgrade', (versioned) => this.migrate(versioned));
127
420
  tryRegisterCallback('__pl_storage_initial', () => {
128
421
  const { version, data } = this.getDefaultData();
129
422
  const storage = createBlockStorage(data, version);
@@ -132,5 +425,5 @@ class DataModel {
132
425
  }
133
426
  }
134
427
 
135
- export { DataModel };
428
+ export { DataModel, DataModelBuilder, DataUnrecoverableError, defaultRecover, defineDataVersions, isDataUnrecoverableError, makeDataVersioned };
136
429
  //# sourceMappingURL=block_migrations.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"block_migrations.js","sources":["../src/block_migrations.ts"],"sourcesContent":["import { tryRegisterCallback } from './internal';\nimport { createBlockStorage } from './block_storage';\n\nexport type MigrationFn<From, To> = (prev: Readonly<From>) => To;\n\n/** Versioned data wrapper for persistence */\nexport type Versioned<T> = {\n version: number;\n data: T;\n};\n\n/** Result of upgrade operation, may include warning if migration failed */\nexport type UpgradeResult<T> = Versioned<T> & {\n warning?: string;\n};\n\n/** Internal builder for chaining migrations */\nclass DataModelBuilder<State> {\n private readonly migrationSteps: Array<(x: unknown) => unknown>;\n\n private constructor(steps: Array<(x: unknown) => unknown> = []) {\n this.migrationSteps = steps;\n }\n\n /** Start a migration chain from an initial type */\n static from<T = unknown>(): DataModelBuilder<T> {\n return new DataModelBuilder<T>();\n }\n\n /** Add a migration step */\n migrate<Next>(fn: MigrationFn<State, Next>): DataModelBuilder<Next> {\n return new DataModelBuilder<Next>([...this.migrationSteps, fn as any]);\n }\n\n /** Finalize with initial data, creating the DataModel */\n create<S>(\n initialData: () => S,\n ..._: [State] extends [S] ? [] : [never]\n ): DataModel<S> {\n return DataModel._fromBuilder<S>(this.migrationSteps, initialData);\n }\n}\n\n/**\n * DataModel defines the block's data structure, initial values, and migrations.\n * Used by BlockModelV3 to manage data state.\n *\n * @example\n * // Simple data model (no migrations)\n * const dataModel = DataModel.create<BlockData>(() => ({\n * numbers: [],\n * labels: [],\n * }));\n *\n * // Data model with migrations\n * const dataModel = DataModel\n * .from<V1>()\n * .migrate((data) => ({ ...data, labels: [] })) // v1 → v2\n * .migrate((data) => ({ ...data, description: '' })) // v2 → v3\n * .create<BlockData>(() => ({ numbers: [], labels: [], description: '' }));\n */\nexport class DataModel<State> {\n private readonly steps: Array<(x: unknown) => unknown>;\n private readonly _initialData: () => State;\n\n private constructor(steps: Array<(x: unknown) => unknown>, initialData: () => State) {\n this.steps = steps;\n this._initialData = initialData;\n }\n\n /** Start a migration chain from an initial type */\n static from<S>(): DataModelBuilder<S> {\n return DataModelBuilder.from<S>();\n }\n\n /** Create a data model with just initial data (no migrations) */\n static create<S>(initialData: () => S): DataModel<S> {\n return new DataModel<S>([], initialData);\n }\n\n /** Create from builder (internal use) */\n static _fromBuilder<S>(\n steps: Array<(x: unknown) => unknown>,\n initialData: () => S,\n ): DataModel<S> {\n return new DataModel<S>(steps, initialData);\n }\n\n /**\n * Latest version number.\n * Version 1 = initial state, each migration adds 1.\n */\n get version(): number {\n return this.steps.length + 1;\n }\n\n /** Number of migration steps */\n get migrationCount(): number {\n return this.steps.length;\n }\n\n /** Get initial data */\n initialData(): State {\n return this._initialData();\n }\n\n /** Get default data wrapped with current version */\n getDefaultData(): Versioned<State> {\n return { version: this.version, data: this._initialData() };\n }\n\n /**\n * Upgrade versioned data from any version to the latest.\n * Applies only the migrations needed (skips already-applied ones).\n * If a migration fails, returns default data with a warning.\n */\n upgrade(versioned: Versioned<unknown>): UpgradeResult<State> {\n const { version: fromVersion, data } = versioned;\n\n if (fromVersion > this.version) {\n throw new Error(\n `Cannot downgrade from version ${fromVersion} to ${this.version}`,\n );\n }\n\n if (fromVersion === this.version) {\n return { version: this.version, data: data as State };\n }\n\n // Apply migrations starting from (fromVersion - 1) index\n // Version 1 -> no migrations applied yet -> start at index 0\n // Version 2 -> migration[0] already applied -> start at index 1\n const startIndex = fromVersion - 1;\n const migrationsToApply = this.steps.slice(startIndex);\n\n let currentData: unknown = data;\n for (let i = 0; i < migrationsToApply.length; i++) {\n const stepIndex = startIndex + i;\n const fromVer = stepIndex + 1;\n const toVer = stepIndex + 2;\n try {\n currentData = migrationsToApply[i](currentData);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n return {\n ...this.getDefaultData(),\n warning: `Migration v${fromVer}→v${toVer} failed: ${errorMessage}`,\n };\n }\n }\n\n return { version: this.version, data: currentData as State };\n }\n\n /**\n * Register callbacks for use in the VM.\n * Called by BlockModelV3.create() to set up internal callbacks.\n *\n * All callbacks are prefixed with `__pl_` to indicate internal SDK use:\n * - `__pl_data_initial`: returns initial data for new blocks\n * - `__pl_data_upgrade`: upgrades versioned data from any version to latest\n * - `__pl_storage_initial`: returns initial BlockStorage as JSON string\n */\n registerCallbacks(): void {\n tryRegisterCallback('__pl_data_initial', () => this._initialData());\n tryRegisterCallback('__pl_data_upgrade', (versioned: Versioned<unknown>) => this.upgrade(versioned));\n tryRegisterCallback('__pl_storage_initial', () => {\n const { version, data } = this.getDefaultData();\n const storage = createBlockStorage(data, version);\n return JSON.stringify(storage);\n });\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"block_migrations.js","sources":["../src/block_migrations.ts"],"sourcesContent":["import { tryRegisterCallback } from './internal';\nimport { createBlockStorage, DATA_MODEL_DEFAULT_VERSION } from './block_storage';\n\nexport type DataVersionKey = string;\nexport type DataVersionMap = Record<string, unknown>;\nexport type DataMigrateFn<From, To> = (prev: Readonly<From>) => To;\nexport type DataCreateFn<T> = () => T;\nexport type DataRecoverFn<T> = (version: DataVersionKey, data: unknown) => T;\n\n/**\n * Helper to define version keys with literal type inference and runtime validation.\n * - Validates that all version values are unique\n * - Validates that no version value is empty\n * - Eliminates need for `as const` assertion\n *\n * @throws Error if duplicate or empty version values are found\n *\n * @example\n * const Version = defineDataVersions({\n * Initial: 'v1',\n * AddedLabels: 'v2',\n * });\n *\n * type VersionedData = {\n * [Version.Initial]: DataV1;\n * [Version.AddedLabels]: DataV2;\n * };\n */\nexport function defineDataVersions<const T extends Record<string, string>>(versions: T): T {\n const values = Object.values(versions) as (string & keyof T)[];\n const keys = Object.keys(versions) as (keyof T)[];\n const emptyKeys = keys.filter((key) => versions[key] === '');\n if (emptyKeys.length > 0) {\n throw new Error(`Version values must be non-empty strings (empty: ${emptyKeys.join(', ')})`);\n }\n const unique = new Set(values);\n if (unique.size !== values.length) {\n const duplicates = values.filter((v, i) => values.indexOf(v) !== i);\n throw new Error(`Duplicate version values: ${[...new Set(duplicates)].join(', ')}`);\n }\n return versions;\n}\n\n/** Versioned data wrapper for persistence */\nexport type DataVersioned<T> = {\n version: DataVersionKey;\n data: T;\n};\n\n/** Create a DataVersioned wrapper with correct shape */\nexport function makeDataVersioned<T>(version: DataVersionKey, data: T): DataVersioned<T> {\n return { version, data };\n}\n\n/** Result of migration operation, may include warning if migration failed */\nexport type DataMigrationResult<T> = DataVersioned<T> & {\n warning?: string;\n};\n\n/** Thrown by recover() to signal unrecoverable data. */\nexport class DataUnrecoverableError extends Error {\n name = 'DataUnrecoverableError';\n constructor(dataVersion: DataVersionKey) {\n super(`Unknown version '${dataVersion}'`);\n }\n}\n\nexport function isDataUnrecoverableError(error: unknown): error is DataUnrecoverableError {\n return error instanceof Error && error.name === 'DataUnrecoverableError';\n}\n\ntype MigrationStep = {\n fromVersion: DataVersionKey;\n toVersion: DataVersionKey;\n migrate: (data: unknown) => unknown;\n};\n\n/**\n * Default recover function for unknown versions.\n * Use as fallback at the end of custom recover functions.\n *\n * @example\n * .recover((version, data) => {\n * if (version === 'legacy') {\n * return transformLegacyData(data);\n * }\n * return defaultRecover(version, data);\n * })\n */\nexport const defaultRecover: DataRecoverFn<never> = (version, _data) => {\n throw new DataUnrecoverableError(version);\n};\n\n/** Symbol for internal builder creation method */\nconst FROM_BUILDER = Symbol('fromBuilder');\n\n/** Internal state passed from builder to DataModel */\ntype BuilderState<S> = {\n versionChain: DataVersionKey[];\n steps: MigrationStep[];\n initialDataFn: () => S;\n recoverFn?: DataRecoverFn<S>;\n};\n\n/**\n * Final builder state after recover() is called.\n * Only allows calling create() to finalize the DataModel.\n *\n * @typeParam VersionedData - Map of version keys to their data types\n * @typeParam CurrentVersion - The current (final) version in the chain\n * @internal\n */\nclass DataModelBuilderWithRecover<\n VersionedData extends DataVersionMap,\n CurrentVersion extends keyof VersionedData & string,\n> {\n private readonly versionChain: DataVersionKey[];\n private readonly migrationSteps: MigrationStep[];\n private readonly recoverFn: DataRecoverFn<VersionedData[CurrentVersion]>;\n\n /** @internal */\n constructor({\n versionChain,\n steps,\n recoverFn,\n }: {\n versionChain: DataVersionKey[];\n steps: MigrationStep[];\n recoverFn: DataRecoverFn<VersionedData[CurrentVersion]>;\n }) {\n this.versionChain = versionChain;\n this.migrationSteps = steps;\n this.recoverFn = recoverFn;\n }\n\n /**\n * Finalize the DataModel with initial data factory.\n *\n * The initial data factory is called when creating new blocks or when\n * migration/recovery fails and data must be reset.\n *\n * @param initialData - Factory function returning initial state (must exactly match CurrentVersion's data type)\n * @returns Finalized DataModel instance\n *\n * @example\n * .init(() => ({ numbers: [], labels: [], description: '' }))\n */\n init<S extends VersionedData[CurrentVersion]>(\n initialData: DataCreateFn<S>,\n // Compile-time check: S must have exactly the same keys as VersionedData[CurrentVersion]\n ..._noExtraKeys: Exclude<keyof S, keyof VersionedData[CurrentVersion]> extends never ? [] : [never]\n ): DataModel<VersionedData[CurrentVersion]> {\n return DataModel[FROM_BUILDER]<VersionedData[CurrentVersion]>({\n versionChain: this.versionChain,\n steps: this.migrationSteps,\n initialDataFn: initialData as DataCreateFn<VersionedData[CurrentVersion]>,\n recoverFn: this.recoverFn,\n });\n }\n}\n\n/**\n * Internal builder for constructing DataModel with type-safe migration chains.\n *\n * Tracks the current version through the generic type system, ensuring:\n * - Migration functions receive correctly typed input\n * - Migration functions must return the correct output type\n * - Version keys must exist in the VersionedData map\n * - All versions must be covered before calling init()\n *\n * @typeParam VersionedData - Map of version keys to their data types\n * @typeParam CurrentVersion - The current version in the migration chain\n * @typeParam RemainingVersions - Versions not yet covered by migrations\n * @internal\n */\nclass DataModelMigrationChain<\n VersionedData extends DataVersionMap,\n CurrentVersion extends keyof VersionedData & string,\n RemainingVersions extends keyof VersionedData & string = Exclude<keyof VersionedData & string, CurrentVersion>,\n> {\n private readonly versionChain: DataVersionKey[];\n private readonly migrationSteps: MigrationStep[];\n\n /** @internal */\n constructor({\n versionChain,\n steps = [],\n }: {\n versionChain: DataVersionKey[];\n steps?: MigrationStep[];\n }) {\n this.versionChain = versionChain;\n this.migrationSteps = steps;\n }\n\n /**\n * Add a migration step to transform data from current version to next version.\n *\n * Migration functions:\n * - Receive data typed as the current version's data type (readonly)\n * - Must return data matching the target version's data type\n * - Should be pure functions (no side effects)\n * - May throw errors (will result in data reset with warning)\n *\n * @typeParam NextVersion - The target version key (must be in RemainingVersions)\n * @param nextVersion - The version key to migrate to\n * @param fn - Migration function transforming current data to next version\n * @returns Builder with updated current version\n *\n * @example\n * .migrate(Version.V2, (data) => ({ ...data, labels: [] }))\n */\n migrate<NextVersion extends RemainingVersions>(\n nextVersion: NextVersion,\n fn: DataMigrateFn<VersionedData[CurrentVersion], VersionedData[NextVersion]>,\n ): DataModelMigrationChain<VersionedData, NextVersion, Exclude<RemainingVersions, NextVersion>> {\n if (this.versionChain.includes(nextVersion)) {\n throw new Error(`Duplicate version '${nextVersion}' in migration chain`);\n }\n const fromVersion = this.versionChain[this.versionChain.length - 1];\n const step: MigrationStep = { fromVersion, toVersion: nextVersion, migrate: fn as (data: unknown) => unknown };\n return new DataModelMigrationChain<VersionedData, NextVersion, Exclude<RemainingVersions, NextVersion>>({\n versionChain: [...this.versionChain, nextVersion],\n steps: [...this.migrationSteps, step],\n });\n }\n\n /**\n * Set a recovery handler for unknown or legacy versions.\n *\n * The recover function is called when data has a version not in the migration chain.\n * It should either:\n * - Transform the data to the current version's format and return it\n * - Call `defaultRecover(version, data)` to signal unrecoverable data\n *\n * Can only be called once. After calling, only `init()` is available.\n *\n * @param fn - Recovery function that transforms unknown data or throws\n * @returns Builder with only init() method available\n *\n * @example\n * .recover((version, data) => {\n * if (version === 'legacy' && isLegacyFormat(data)) {\n * return transformLegacy(data);\n * }\n * return defaultRecover(version, data);\n * })\n */\n recover(\n fn: DataRecoverFn<VersionedData[CurrentVersion]>,\n ): DataModelBuilderWithRecover<VersionedData, CurrentVersion> {\n return new DataModelBuilderWithRecover<VersionedData, CurrentVersion>({\n versionChain: [...this.versionChain],\n steps: [...this.migrationSteps],\n recoverFn: fn,\n });\n }\n\n /**\n * Finalize the DataModel with initial data factory.\n *\n * Can only be called when all versions in VersionedData have been covered\n * by the migration chain (RemainingVersions is empty).\n *\n * The initial data factory is called when creating new blocks or when\n * migration/recovery fails and data must be reset.\n *\n * @param initialData - Factory function returning initial state (must exactly match CurrentVersion's data type)\n * @returns Finalized DataModel instance\n *\n * @example\n * .init(() => ({ numbers: [], labels: [], description: '' }))\n */\n init<S extends VersionedData[CurrentVersion]>(\n // Compile-time check: RemainingVersions must be empty (all versions covered)\n this: DataModelMigrationChain<VersionedData, CurrentVersion, never>,\n initialData: DataCreateFn<S>,\n // Compile-time check: S must have exactly the same keys as VersionedData[CurrentVersion]\n ..._noExtraKeys: Exclude<keyof S, keyof VersionedData[CurrentVersion]> extends never ? [] : [never]\n ): DataModel<VersionedData[CurrentVersion]> {\n return DataModel[FROM_BUILDER]<VersionedData[CurrentVersion]>({\n versionChain: this.versionChain,\n steps: this.migrationSteps,\n initialDataFn: initialData as DataCreateFn<VersionedData[CurrentVersion]>,\n });\n }\n}\n\n/**\n * Builder entry point for creating DataModel with type-safe migrations.\n *\n * @typeParam VersionedData - Map of version keys to their data types\n *\n * @example\n * const Version = defineDataVersions({\n * V1: 'v1',\n * V2: 'v2',\n * });\n *\n * type VersionedData = {\n * [Version.V1]: { count: number };\n * [Version.V2]: { count: number; label: string };\n * };\n *\n * const dataModel = new DataModelBuilder<VersionedData>()\n * .from(Version.V1)\n * .migrate(Version.V2, (data) => ({ ...data, label: '' }))\n * .init(() => ({ count: 0, label: '' }));\n */\nexport class DataModelBuilder<VersionedData extends DataVersionMap> {\n /**\n * Start a migration chain from an initial version.\n *\n * @typeParam InitialVersion - The starting version key (inferred from argument)\n * @param initialVersion - The version key to start from\n * @returns Migration chain builder for adding migrations\n *\n * @example\n * new DataModelBuilder<VersionedData>()\n * .from(Version.V1)\n * .migrate(Version.V2, (data) => ({ ...data, newField: '' }))\n */\n from<InitialVersion extends keyof VersionedData & string>(\n initialVersion: InitialVersion,\n ): DataModelMigrationChain<VersionedData, InitialVersion, Exclude<keyof VersionedData & string, InitialVersion>> {\n return new DataModelMigrationChain<\n VersionedData,\n InitialVersion,\n Exclude<keyof VersionedData & string, InitialVersion>\n >({ versionChain: [initialVersion] });\n }\n}\n\n/**\n * DataModel defines the block's data structure, initial values, and migrations.\n * Used by BlockModelV3 to manage data state.\n *\n * Two ways to create a DataModel:\n *\n * 1. **Simple (no migrations)** - Use `DataModel.create()`:\n * @example\n * const dataModel = DataModel.create<BlockData>(() => ({\n * numbers: [],\n * labels: [],\n * }));\n *\n * 2. **With migrations** - Use `new DataModelBuilder<VersionedData>()`:\n * @example\n * const Version = defineDataVersions({\n * V1: 'v1',\n * V2: 'v2',\n * V3: 'v3',\n * });\n *\n * type VersionedData = {\n * [Version.V1]: { numbers: number[] };\n * [Version.V2]: { numbers: number[]; labels: string[] };\n * [Version.V3]: { numbers: number[]; labels: string[]; description: string };\n * };\n *\n * const dataModel = new DataModelBuilder<VersionedData>()\n * .from(Version.V1)\n * .migrate(Version.V2, (data) => ({ ...data, labels: [] }))\n * .migrate(Version.V3, (data) => ({ ...data, description: '' }))\n * .recover((version, data) => {\n * if (version === 'legacy' && typeof data === 'object' && data !== null && 'numbers' in data) {\n * return { numbers: (data as { numbers: number[] }).numbers, labels: [], description: '' };\n * }\n * return defaultRecover(version, data);\n * })\n * .init(() => ({ numbers: [], labels: [], description: '' }));\n */\nexport class DataModel<State> {\n private readonly versionChain: DataVersionKey[];\n private readonly steps: MigrationStep[];\n private readonly initialDataFn: () => State;\n private readonly recoverFn: DataRecoverFn<State>;\n\n private constructor({\n versionChain,\n steps,\n initialDataFn,\n recoverFn = defaultRecover as DataRecoverFn<State>,\n }: {\n versionChain: DataVersionKey[];\n steps: MigrationStep[];\n initialDataFn: () => State;\n recoverFn?: DataRecoverFn<State>;\n }) {\n if (versionChain.length === 0) {\n throw new Error('DataModel requires at least one version key');\n }\n this.versionChain = versionChain;\n this.steps = steps;\n this.initialDataFn = initialDataFn;\n this.recoverFn = recoverFn;\n }\n\n /**\n * Create a DataModel with just initial data (no migrations).\n *\n * Use this for simple blocks that don't need version migrations.\n * The version will be set to an internal default value.\n *\n * @typeParam S - The state type\n * @param initialData - Factory function returning initial state\n * @param version - Optional custom version key (defaults to internal version)\n * @returns Finalized DataModel instance\n *\n * @example\n * const dataModel = DataModel.create<BlockData>(() => ({\n * numbers: [],\n * labels: [],\n * }));\n */\n static create<S>(initialData: () => S, version: DataVersionKey = DATA_MODEL_DEFAULT_VERSION): DataModel<S> {\n return new DataModel<S>({\n versionChain: [version],\n steps: [],\n initialDataFn: initialData,\n });\n }\n\n /**\n * Internal method for creating DataModel from builder.\n * Uses Symbol key to prevent external access.\n * @internal\n */\n static [FROM_BUILDER]<S>(state: BuilderState<S>): DataModel<S> {\n return new DataModel<S>(state);\n }\n\n /**\n * The latest (current) version key in the migration chain.\n */\n get version(): DataVersionKey {\n return this.versionChain[this.versionChain.length - 1];\n }\n\n /**\n * Number of migration steps defined.\n */\n get migrationCount(): number {\n return this.steps.length;\n }\n\n /**\n * Get a fresh copy of the initial data.\n */\n initialData(): State {\n return this.initialDataFn();\n }\n\n /**\n * Get initial data wrapped with current version.\n * Used when creating new blocks or resetting to defaults.\n */\n getDefaultData(): DataVersioned<State> {\n return makeDataVersioned(this.version, this.initialDataFn());\n }\n\n private recoverFrom(data: unknown, version: DataVersionKey): DataMigrationResult<State> {\n try {\n return { version: this.version, data: this.recoverFn(version, data) };\n } catch (error) {\n if (isDataUnrecoverableError(error)) {\n return { ...this.getDefaultData(), warning: error.message };\n }\n const errorMessage = error instanceof Error ? error.message : String(error);\n return {\n ...this.getDefaultData(),\n warning: `Recover failed for version '${version}': ${errorMessage}`,\n };\n }\n }\n\n /**\n * Migrate versioned data from any version to the latest.\n *\n * - If data is already at latest version, returns as-is\n * - If version is in chain, applies needed migrations\n * - If version is unknown, calls recover function\n * - If migration/recovery fails, returns default data with warning\n *\n * @param versioned - Data with version tag\n * @returns Migration result with data at latest version\n */\n migrate(versioned: DataVersioned<unknown>): DataMigrationResult<State> {\n const { version: fromVersion, data } = versioned;\n\n if (fromVersion === this.version) {\n return { version: this.version, data: data as State };\n }\n\n const startIndex = this.versionChain.indexOf(fromVersion);\n if (startIndex < 0) {\n return this.recoverFrom(data, fromVersion);\n }\n\n let currentData: unknown = data;\n for (let i = startIndex; i < this.steps.length; i++) {\n const step = this.steps[i];\n try {\n currentData = step.migrate(currentData);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n return {\n ...this.getDefaultData(),\n warning: `Migration ${step.fromVersion}→${step.toVersion} failed: ${errorMessage}`,\n };\n }\n }\n\n return { version: this.version, data: currentData as State };\n }\n\n /**\n * Register callbacks for use in the VM.\n * Called by BlockModelV3.create() to set up internal callbacks.\n *\n * @internal\n */\n registerCallbacks(): void {\n tryRegisterCallback('__pl_data_initial', () => this.initialDataFn());\n tryRegisterCallback('__pl_data_upgrade', (versioned: DataVersioned<unknown>) => this.migrate(versioned));\n tryRegisterCallback('__pl_storage_initial', () => {\n const { version, data } = this.getDefaultData();\n const storage = createBlockStorage(data, version);\n return JSON.stringify(storage);\n });\n }\n}\n"],"names":[],"mappings":"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"}
@@ -23,6 +23,11 @@ const BLOCK_STORAGE_KEY = '__pl_a7f3e2b9__';
23
23
  * Increment this when the storage structure itself changes (not block state migrations).
24
24
  */
25
25
  const BLOCK_STORAGE_SCHEMA_VERSION = 'v1';
26
+ /**
27
+ * Default data version for new blocks without migrations.
28
+ * Unique identifier ensures blocks are created via DataModel API.
29
+ */
30
+ const DATA_MODEL_DEFAULT_VERSION = '__pl_v1_d4e8f2a1__';
26
31
  /**
27
32
  * Type guard to check if a value is a valid BlockStorage object.
28
33
  * Checks for the discriminator key and valid schema version.
@@ -42,10 +47,10 @@ function isBlockStorage(value) {
42
47
  * Creates a BlockStorage with the given initial data
43
48
  *
44
49
  * @param initialData - The initial data value (defaults to empty object)
45
- * @param version - The initial data version (defaults to 1)
50
+ * @param version - The initial data version key (defaults to DATA_MODEL_DEFAULT_VERSION)
46
51
  * @returns A new BlockStorage instance with discriminator key
47
52
  */
48
- function createBlockStorage(initialData = {}, version = 1) {
53
+ function createBlockStorage(initialData = {}, version = DATA_MODEL_DEFAULT_VERSION) {
49
54
  return {
50
55
  [BLOCK_STORAGE_KEY]: BLOCK_STORAGE_SCHEMA_VERSION,
51
56
  __dataVersion: version,
@@ -62,10 +67,17 @@ function createBlockStorage(initialData = {}, version = 1) {
62
67
  */
63
68
  function normalizeBlockStorage(raw) {
64
69
  if (isBlockStorage(raw)) {
65
- return raw;
70
+ const storage = raw;
71
+ return {
72
+ ...storage,
73
+ // Fix for early released version where __dataVersion was a number
74
+ __dataVersion: typeof storage.__dataVersion === 'number'
75
+ ? DATA_MODEL_DEFAULT_VERSION
76
+ : storage.__dataVersion,
77
+ };
66
78
  }
67
79
  // Legacy format: raw is the state directly
68
- return createBlockStorage(raw, 1);
80
+ return createBlockStorage(raw);
69
81
  }
70
82
  // =============================================================================
71
83
  // Data Access & Update Functions
@@ -114,7 +126,7 @@ function updateStorageData(storage, payload) {
114
126
  * Gets the data version from BlockStorage
115
127
  *
116
128
  * @param storage - The BlockStorage instance
117
- * @returns The data version number
129
+ * @returns The data version key
118
130
  */
119
131
  function getStorageDataVersion(storage) {
120
132
  return storage.__dataVersion;
@@ -123,7 +135,7 @@ function getStorageDataVersion(storage) {
123
135
  * Updates the data version in BlockStorage (immutable)
124
136
  *
125
137
  * @param storage - The current BlockStorage
126
- * @param version - The new version number
138
+ * @param version - The new version key
127
139
  * @returns A new BlockStorage with updated version
128
140
  */
129
141
  function updateStorageDataVersion(storage, version) {
@@ -225,6 +237,7 @@ function mergeBlockStorageHandlers(customHandlers) {
225
237
 
226
238
  exports.BLOCK_STORAGE_KEY = BLOCK_STORAGE_KEY;
227
239
  exports.BLOCK_STORAGE_SCHEMA_VERSION = BLOCK_STORAGE_SCHEMA_VERSION;
240
+ exports.DATA_MODEL_DEFAULT_VERSION = DATA_MODEL_DEFAULT_VERSION;
228
241
  exports.createBlockStorage = createBlockStorage;
229
242
  exports.defaultBlockStorageHandlers = defaultBlockStorageHandlers;
230
243
  exports.deriveDataFromStorage = deriveDataFromStorage;