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