@platforma-sdk/model 1.54.10 → 1.55.0

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.
Files changed (151) hide show
  1. package/dist/bconfig/normalization.cjs +8 -1
  2. package/dist/bconfig/normalization.cjs.map +1 -1
  3. package/dist/bconfig/normalization.d.ts.map +1 -1
  4. package/dist/bconfig/normalization.js +8 -1
  5. package/dist/bconfig/normalization.js.map +1 -1
  6. package/dist/block_api_v3.d.ts +2 -2
  7. package/dist/block_api_v3.d.ts.map +1 -1
  8. package/dist/block_migrations.cjs +246 -214
  9. package/dist/block_migrations.cjs.map +1 -1
  10. package/dist/block_migrations.d.ts +180 -158
  11. package/dist/block_migrations.d.ts.map +1 -1
  12. package/dist/block_migrations.js +247 -214
  13. package/dist/block_migrations.js.map +1 -1
  14. package/dist/block_model.cjs +85 -35
  15. package/dist/block_model.cjs.map +1 -1
  16. package/dist/block_model.d.ts +66 -38
  17. package/dist/block_model.d.ts.map +1 -1
  18. package/dist/block_model.js +86 -36
  19. package/dist/block_model.js.map +1 -1
  20. package/dist/{builder.cjs → block_model_legacy.cjs} +2 -2
  21. package/dist/block_model_legacy.cjs.map +1 -0
  22. package/dist/{builder.d.ts → block_model_legacy.d.ts} +1 -1
  23. package/dist/block_model_legacy.d.ts.map +1 -0
  24. package/dist/{builder.js → block_model_legacy.js} +2 -2
  25. package/dist/block_model_legacy.js.map +1 -0
  26. package/dist/block_state_patch.d.ts +11 -1
  27. package/dist/block_state_patch.d.ts.map +1 -1
  28. package/dist/block_storage.cjs +126 -109
  29. package/dist/block_storage.cjs.map +1 -1
  30. package/dist/block_storage.d.ts +109 -112
  31. package/dist/block_storage.d.ts.map +1 -1
  32. package/dist/block_storage.js +126 -101
  33. package/dist/block_storage.js.map +1 -1
  34. package/dist/block_storage_callbacks.cjs +227 -0
  35. package/dist/block_storage_callbacks.cjs.map +1 -0
  36. package/dist/block_storage_callbacks.d.ts +113 -0
  37. package/dist/block_storage_callbacks.d.ts.map +1 -0
  38. package/dist/block_storage_callbacks.js +218 -0
  39. package/dist/block_storage_callbacks.js.map +1 -0
  40. package/dist/block_storage_facade.cjs +104 -0
  41. package/dist/block_storage_facade.cjs.map +1 -0
  42. package/dist/block_storage_facade.d.ts +168 -0
  43. package/dist/block_storage_facade.d.ts.map +1 -0
  44. package/dist/block_storage_facade.js +99 -0
  45. package/dist/block_storage_facade.js.map +1 -0
  46. package/dist/components/PlDataTable/state-migration.cjs.map +1 -1
  47. package/dist/components/PlDataTable/state-migration.js.map +1 -1
  48. package/dist/components/PlDataTable/table.cjs +11 -2
  49. package/dist/components/PlDataTable/table.cjs.map +1 -1
  50. package/dist/components/PlDataTable/table.d.ts.map +1 -1
  51. package/dist/components/PlDataTable/table.js +12 -3
  52. package/dist/components/PlDataTable/table.js.map +1 -1
  53. package/dist/components/PlDataTable/v5.d.ts +7 -4
  54. package/dist/components/PlDataTable/v5.d.ts.map +1 -1
  55. package/dist/filters/converters/filterToQuery.cjs +3 -4
  56. package/dist/filters/converters/filterToQuery.cjs.map +1 -1
  57. package/dist/filters/converters/filterToQuery.d.ts +1 -1
  58. package/dist/filters/converters/filterToQuery.d.ts.map +1 -1
  59. package/dist/filters/converters/filterToQuery.js +3 -4
  60. package/dist/filters/converters/filterToQuery.js.map +1 -1
  61. package/dist/filters/distill.cjs.map +1 -1
  62. package/dist/filters/distill.d.ts +3 -2
  63. package/dist/filters/distill.d.ts.map +1 -1
  64. package/dist/filters/distill.js.map +1 -1
  65. package/dist/filters/traverse.cjs +7 -3
  66. package/dist/filters/traverse.cjs.map +1 -1
  67. package/dist/filters/traverse.d.ts +14 -12
  68. package/dist/filters/traverse.d.ts.map +1 -1
  69. package/dist/filters/traverse.js +7 -3
  70. package/dist/filters/traverse.js.map +1 -1
  71. package/dist/index.cjs +13 -14
  72. package/dist/index.cjs.map +1 -1
  73. package/dist/index.d.ts +8 -3
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +6 -4
  76. package/dist/index.js.map +1 -1
  77. package/dist/package.json.cjs +1 -1
  78. package/dist/package.json.js +1 -1
  79. package/dist/platforma.d.ts +11 -4
  80. package/dist/platforma.d.ts.map +1 -1
  81. package/dist/plugin_model.cjs +171 -0
  82. package/dist/plugin_model.cjs.map +1 -0
  83. package/dist/plugin_model.d.ts +162 -0
  84. package/dist/plugin_model.d.ts.map +1 -0
  85. package/dist/plugin_model.js +169 -0
  86. package/dist/plugin_model.js.map +1 -0
  87. package/dist/render/api.cjs +20 -21
  88. package/dist/render/api.cjs.map +1 -1
  89. package/dist/render/api.d.ts +8 -8
  90. package/dist/render/api.d.ts.map +1 -1
  91. package/dist/render/api.js +20 -21
  92. package/dist/render/api.js.map +1 -1
  93. package/dist/render/internal.cjs.map +1 -1
  94. package/dist/render/internal.d.ts +1 -1
  95. package/dist/render/internal.d.ts.map +1 -1
  96. package/dist/render/internal.js.map +1 -1
  97. package/dist/version.cjs +4 -0
  98. package/dist/version.cjs.map +1 -1
  99. package/dist/version.d.ts +4 -0
  100. package/dist/version.d.ts.map +1 -1
  101. package/dist/version.js +4 -1
  102. package/dist/version.js.map +1 -1
  103. package/package.json +6 -6
  104. package/src/bconfig/normalization.ts +8 -1
  105. package/src/block_api_v3.ts +2 -2
  106. package/src/block_migrations.test.ts +141 -171
  107. package/src/block_migrations.ts +300 -285
  108. package/src/block_model.ts +205 -95
  109. package/src/{builder.ts → block_model_legacy.ts} +1 -1
  110. package/src/block_state_patch.ts +13 -1
  111. package/src/block_storage.test.ts +283 -95
  112. package/src/block_storage.ts +199 -188
  113. package/src/block_storage_callbacks.ts +326 -0
  114. package/src/block_storage_facade.ts +199 -0
  115. package/src/components/PlDataTable/state-migration.ts +4 -4
  116. package/src/components/PlDataTable/table.ts +16 -3
  117. package/src/components/PlDataTable/v5.ts +9 -5
  118. package/src/filters/converters/filterToQuery.ts +8 -7
  119. package/src/filters/distill.ts +19 -11
  120. package/src/filters/traverse.ts +44 -24
  121. package/src/index.ts +7 -3
  122. package/src/platforma.ts +26 -7
  123. package/src/plugin_model.test.ts +168 -0
  124. package/src/plugin_model.ts +242 -0
  125. package/src/render/api.ts +26 -24
  126. package/src/render/internal.ts +1 -1
  127. package/src/typing.test.ts +1 -1
  128. package/src/version.ts +8 -0
  129. package/dist/block_storage_vm.cjs +0 -262
  130. package/dist/block_storage_vm.cjs.map +0 -1
  131. package/dist/block_storage_vm.d.ts +0 -59
  132. package/dist/block_storage_vm.d.ts.map +0 -1
  133. package/dist/block_storage_vm.js +0 -258
  134. package/dist/block_storage_vm.js.map +0 -1
  135. package/dist/branding.d.ts +0 -7
  136. package/dist/branding.d.ts.map +0 -1
  137. package/dist/builder.cjs.map +0 -1
  138. package/dist/builder.d.ts.map +0 -1
  139. package/dist/builder.js.map +0 -1
  140. package/dist/sdk_info.cjs +0 -10
  141. package/dist/sdk_info.cjs.map +0 -1
  142. package/dist/sdk_info.d.ts +0 -5
  143. package/dist/sdk_info.d.ts.map +0 -1
  144. package/dist/sdk_info.js +0 -8
  145. package/dist/sdk_info.js.map +0 -1
  146. package/dist/unionize.d.ts +0 -12
  147. package/dist/unionize.d.ts.map +0 -1
  148. package/src/block_storage_vm.ts +0 -346
  149. package/src/branding.ts +0 -4
  150. package/src/sdk_info.ts +0 -9
  151. package/src/unionize.ts +0 -12
@@ -1,39 +1,3 @@
1
- import { tryRegisterCallback } from './internal.js';
2
- import { createBlockStorage } from './block_storage.js';
3
-
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
1
  /** Create a DataVersioned wrapper with correct shape */
38
2
  function makeDataVersioned(version, data) {
39
3
  return { version, data };
@@ -66,186 +30,260 @@ const defaultRecover = (version, _data) => {
66
30
  /** Symbol for internal builder creation method */
67
31
  const FROM_BUILDER = Symbol("fromBuilder");
68
32
  /**
69
- * Final builder state after recover() is called.
70
- * Only allows calling create() to finalize the DataModel.
33
+ * Abstract base for both migration chain types.
34
+ * Holds shared state, buildStep() helper, and init().
35
+ * migrate() cannot be shared due to a TypeScript limitation: when the base class
36
+ * migrate() return type is abstract, subclasses cannot narrow it without losing type safety.
37
+ * Each subclass therefore owns its migrate() with the correct concrete return type.
71
38
  *
72
- * @typeParam VersionedData - Map of version keys to their data types
73
- * @typeParam CurrentVersion - The current (final) version in the chain
74
39
  * @internal
75
40
  */
76
- class DataModelBuilderWithRecover {
41
+ class MigrationChainBase {
77
42
  versionChain;
78
43
  migrationSteps;
79
- recoverFn;
80
- /** @internal */
81
- constructor({ versionChain, steps, recoverFn, }) {
82
- this.versionChain = versionChain;
83
- this.migrationSteps = steps;
84
- this.recoverFn = recoverFn;
44
+ constructor(state) {
45
+ this.versionChain = state.versionChain;
46
+ this.migrationSteps = state.steps;
47
+ }
48
+ /** Appends a migration step and returns the new versionChain and steps arrays. */
49
+ buildStep(nextVersion, fn) {
50
+ if (this.versionChain.includes(nextVersion)) {
51
+ throw new Error(`Duplicate version '${nextVersion}' in migration chain`);
52
+ }
53
+ const fromVersion = this.versionChain[this.versionChain.length - 1];
54
+ const step = {
55
+ fromVersion,
56
+ toVersion: nextVersion,
57
+ migrate: fn,
58
+ };
59
+ return {
60
+ versionChain: [...this.versionChain, nextVersion],
61
+ steps: [...this.migrationSteps, step],
62
+ };
63
+ }
64
+ /** Returns recover-specific fields for DataModel construction. Overridden by WithRecover. */
65
+ recoverState() {
66
+ return {};
85
67
  }
86
68
  /**
87
69
  * Finalize the DataModel with initial data factory.
88
70
  *
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)
71
+ * @param initialData - Factory function returning the initial state
93
72
  * @returns Finalized DataModel instance
94
- *
95
- * @example
96
- * .init(() => ({ numbers: [], labels: [], description: '' }))
97
73
  */
98
- init(initialData,
99
- // Compile-time check: S must have exactly the same keys as VersionedData[CurrentVersion]
100
- ..._noExtraKeys) {
74
+ init(initialData) {
101
75
  return DataModel[FROM_BUILDER]({
102
76
  versionChain: this.versionChain,
103
77
  steps: this.migrationSteps,
104
78
  initialDataFn: initialData,
105
- recoverFn: this.recoverFn,
79
+ ...this.recoverState(),
106
80
  });
107
81
  }
108
82
  }
109
83
  /**
110
- * Internal builder for constructing DataModel with type-safe migration chains.
84
+ * Migration chain after recover() or upgradeLegacy() has been called.
85
+ * Further migrate() calls are allowed; recover() and upgradeLegacy() are not
86
+ * (enforced by type — no such methods on this class).
111
87
  *
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()
88
+ * @typeParam Current - Data type at the current point in the chain
89
+ * @internal
90
+ */
91
+ class DataModelMigrationChainWithRecover extends MigrationChainBase {
92
+ recoverFn;
93
+ recoverFromIndex;
94
+ upgradeLegacyFn;
95
+ /** @internal */
96
+ constructor(state) {
97
+ super(state);
98
+ this.recoverFn = state.recoverFn;
99
+ this.recoverFromIndex = state.recoverFromIndex;
100
+ this.upgradeLegacyFn = state.upgradeLegacyFn;
101
+ }
102
+ recoverState() {
103
+ return {
104
+ recoverFn: this.recoverFn,
105
+ recoverFromIndex: this.recoverFromIndex,
106
+ upgradeLegacyFn: this.upgradeLegacyFn,
107
+ };
108
+ }
109
+ /**
110
+ * Add a migration step. Same semantics as on the base chain.
111
+ * recover() and upgradeLegacy() are not available — one has already been called.
112
+ */
113
+ migrate(nextVersion, fn) {
114
+ const { versionChain, steps } = this.buildStep(nextVersion, fn);
115
+ return new DataModelMigrationChainWithRecover({
116
+ versionChain,
117
+ steps,
118
+ recoverFn: this.recoverFn,
119
+ recoverFromIndex: this.recoverFromIndex,
120
+ upgradeLegacyFn: this.upgradeLegacyFn,
121
+ });
122
+ }
123
+ }
124
+ /**
125
+ * Migration chain builder.
126
+ * Each migrate() call advances the current data type. recover() can be called once
127
+ * at any point — it removes itself from the returned chain so it cannot be called again.
128
+ * Duplicate version keys throw at runtime.
117
129
  *
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
130
+ * @typeParam Current - Data type at the current point in the migration chain
121
131
  * @internal
122
132
  */
123
- class DataModelMigrationChain {
124
- versionChain;
125
- migrationSteps;
133
+ class DataModelMigrationChain extends MigrationChainBase {
126
134
  /** @internal */
127
135
  constructor({ versionChain, steps = [], }) {
128
- this.versionChain = versionChain;
129
- this.migrationSteps = steps;
136
+ super({ versionChain, steps });
130
137
  }
131
138
  /**
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
+ * Add a migration step transforming data from the current version to the next.
139
140
  *
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
141
+ * @typeParam Next - Data type of the next version
142
+ * @param nextVersion - Version key to migrate to (must be unique in the chain)
143
+ * @param fn - Migration function
144
+ * @returns Builder with the next version as current
144
145
  *
145
146
  * @example
146
- * .migrate(Version.V2, (data) => ({ ...data, labels: [] }))
147
+ * .migrate<BlockDataV2>("v2", (v1) => ({ ...v1, labels: [] }))
147
148
  */
148
149
  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 = {
154
- fromVersion,
155
- toVersion: nextVersion,
156
- migrate: fn,
157
- };
158
- return new DataModelMigrationChain({
159
- versionChain: [...this.versionChain, nextVersion],
160
- steps: [...this.migrationSteps, step],
161
- });
150
+ const { versionChain, steps } = this.buildStep(nextVersion, fn);
151
+ return new DataModelMigrationChain({ versionChain, steps });
162
152
  }
163
153
  /**
164
154
  * Set a recovery handler for unknown or legacy versions.
165
155
  *
166
156
  * The recover function is called when data has a version not in the migration chain.
167
- * It should either:
168
- * - Transform the data to the current version's format and return it
169
- * - Call `defaultRecover(version, data)` to signal unrecoverable data
157
+ * It must return data of the type at this point in the chain (Current). Any migrate()
158
+ * steps added after recover() will then run on the recovered data.
170
159
  *
171
- * Can only be called once. After calling, only `init()` is available.
160
+ * Can only be called once the returned chain has no recover() method.
161
+ * Mutually exclusive with upgradeLegacy().
172
162
  *
173
- * @param fn - Recovery function that transforms unknown data or throws
174
- * @returns Builder with only init() method available
163
+ * @param fn - Recovery function returning Current (the type at this chain position)
164
+ * @returns Builder with migrate() and init() but without recover() or upgradeLegacy()
175
165
  *
176
166
  * @example
177
- * .recover((version, data) => {
178
- * if (version === 'legacy' && isLegacyFormat(data)) {
179
- * return transformLegacy(data);
180
- * }
181
- * return defaultRecover(version, data);
182
- * })
167
+ * // Recover between migrations — recovered data goes through v3 migration
168
+ * new DataModelBuilder<V1>("v1")
169
+ * .migrate<V2>("v2", (v1) => ({ ...v1, label: "" }))
170
+ * .recover((version, data) => {
171
+ * if (version === 'legacy') return transformLegacy(data); // returns V2
172
+ * return defaultRecover(version, data);
173
+ * })
174
+ * .migrate<V3>("v3", (v2) => ({ ...v2, description: "" }))
175
+ * .init(() => ({ count: 0, label: "", description: "" }));
183
176
  */
184
177
  recover(fn) {
185
- return new DataModelBuilderWithRecover({
186
- versionChain: [...this.versionChain],
187
- steps: [...this.migrationSteps],
178
+ return new DataModelMigrationChainWithRecover({
179
+ versionChain: this.versionChain,
180
+ steps: this.migrationSteps,
188
181
  recoverFn: fn,
182
+ recoverFromIndex: this.migrationSteps.length,
189
183
  });
190
184
  }
191
185
  /**
192
- * Finalize the DataModel with initial data factory.
186
+ * Handle legacy V1 model state ({ args, uiState }) when upgrading a block from
187
+ * BlockModel V1 to BlockModelV3.
193
188
  *
194
- * Can only be called when all versions in VersionedData have been covered
195
- * by the migration chain (RemainingVersions is empty).
189
+ * When a V1 block is upgraded, its stored state `{ args, uiState }` arrives at the
190
+ * initial version (DATA_MODEL_DEFAULT_VERSION) in the migration chain. This method
191
+ * detects the legacy shape and transforms it to the current chain type using the
192
+ * provided typed callback. Non-legacy data passes through unchanged.
196
193
  *
197
- * The initial data factory is called when creating new blocks or when
198
- * migration/recovery fails and data must be reset.
194
+ * Should be called right after `.from()` (before any `.migrate()` calls), since legacy
195
+ * data always arrives at the initial version. Any `.migrate()` steps added after
196
+ * `upgradeLegacy()` will run on the transformed result.
199
197
  *
200
- * @param initialData - Factory function returning initial state (must exactly match CurrentVersion's data type)
201
- * @returns Finalized DataModel instance
198
+ * Can only be called once the returned chain has no upgradeLegacy() method.
199
+ * Mutually exclusive with recover().
200
+ *
201
+ * @typeParam Args - Type of the legacy block args
202
+ * @typeParam UiState - Type of the legacy block uiState
203
+ * @param fn - Typed transform from { args, uiState } to Current
204
+ * @returns Builder with migrate() and init() but without recover() or upgradeLegacy()
202
205
  *
203
206
  * @example
204
- * .init(() => ({ numbers: [], labels: [], description: '' }))
207
+ * type OldArgs = { inputFile: string; threshold: number };
208
+ * type OldUiState = { selectedTab: string };
209
+ * type BlockData = { inputFile: string; threshold: number; selectedTab: string };
210
+ *
211
+ * const dataModel = new DataModelBuilder()
212
+ * .from<BlockData>(DATA_MODEL_DEFAULT_VERSION)
213
+ * .upgradeLegacy<OldArgs, OldUiState>(({ args, uiState }) => ({
214
+ * inputFile: args.inputFile,
215
+ * threshold: args.threshold,
216
+ * selectedTab: uiState.selectedTab,
217
+ * }))
218
+ * .init(() => ({ inputFile: '', threshold: 0, selectedTab: 'main' }));
205
219
  */
206
- init(initialData,
207
- // Compile-time check: S must have exactly the same keys as VersionedData[CurrentVersion]
208
- ..._noExtraKeys) {
209
- return DataModel[FROM_BUILDER]({
220
+ upgradeLegacy(fn) {
221
+ const wrappedFn = (data) => {
222
+ if (data !== null && typeof data === "object" && "args" in data) {
223
+ return fn(data);
224
+ }
225
+ return data;
226
+ };
227
+ return new DataModelMigrationChainWithRecover({
210
228
  versionChain: this.versionChain,
211
229
  steps: this.migrationSteps,
212
- initialDataFn: initialData,
230
+ upgradeLegacyFn: wrappedFn,
213
231
  });
214
232
  }
215
233
  }
216
234
  /**
217
235
  * Builder entry point for creating DataModel with type-safe migrations.
218
236
  *
219
- * @typeParam VersionedData - Map of version keys to their data types
237
+ * @example
238
+ * // Simple (no migrations):
239
+ * const dataModel = new DataModelBuilder()
240
+ * .from<BlockData>(DATA_MODEL_DEFAULT_VERSION)
241
+ * .init(() => ({ numbers: [] }));
242
+ *
243
+ * @example
244
+ * // With migrations:
245
+ * const dataModel = new DataModelBuilder()
246
+ * .from<BlockDataV1>(DATA_MODEL_DEFAULT_VERSION)
247
+ * .migrate<BlockDataV2>("v2", (v1) => ({ ...v1, labels: [] }))
248
+ * .migrate<BlockDataV3>("v3", (v2) => ({ ...v2, description: '' }))
249
+ * .init(() => ({ numbers: [], labels: [], description: '' }));
220
250
  *
221
251
  * @example
222
- * const Version = defineDataVersions({
223
- * V1: 'v1',
224
- * V2: 'v2',
225
- * });
252
+ * // With recover() between migrations — recovered data goes through remaining migrations:
253
+ * const dataModelChain = new DataModelBuilder()
254
+ * .from<BlockDataV1>(DATA_MODEL_DEFAULT_VERSION)
255
+ * .migrate<BlockDataV2>("v2", (v1) => ({ ...v1, labels: [] }));
226
256
  *
227
- * type VersionedData = {
228
- * [Version.V1]: { count: number };
229
- * [Version.V2]: { count: number; label: string };
230
- * };
257
+ * // recover() placed before the v3 migration: recovered data goes through v3
258
+ * const dataModel = dataModelChain
259
+ * .recover((version, data) => {
260
+ * if (version === 'legacy' && isLegacyData(data)) return transformLegacy(data); // returns V2
261
+ * return defaultRecover(version, data);
262
+ * })
263
+ * .migrate<BlockDataV3>("v3", (v2) => ({ ...v2, description: '' }))
264
+ * .init(() => ({ numbers: [], labels: [], description: '' }));
231
265
  *
232
- * const dataModel = new DataModelBuilder<VersionedData>()
233
- * .from(Version.V1)
234
- * .migrate(Version.V2, (data) => ({ ...data, label: '' }))
235
- * .init(() => ({ count: 0, label: '' }));
266
+ * @example
267
+ * // With upgradeLegacy() — typed upgrade from BlockModel V1 state:
268
+ * type OldArgs = { inputFile: string };
269
+ * type OldUiState = { selectedTab: string };
270
+ * type BlockData = { inputFile: string; selectedTab: string };
271
+ *
272
+ * const dataModel = new DataModelBuilder()
273
+ * .from<BlockData>(DATA_MODEL_DEFAULT_VERSION)
274
+ * .upgradeLegacy<OldArgs, OldUiState>(({ args, uiState }) => ({
275
+ * inputFile: args.inputFile,
276
+ * selectedTab: uiState.selectedTab,
277
+ * }))
278
+ * .init(() => ({ inputFile: '', selectedTab: 'main' }));
236
279
  */
237
280
  class DataModelBuilder {
238
281
  /**
239
- * Start a migration chain from an initial version.
282
+ * Start the migration chain with the given initial data type and version key.
240
283
  *
241
- * @typeParam InitialVersion - The starting version key (inferred from argument)
242
- * @param initialVersion - The version key to start from
243
- * @returns Migration chain builder for adding migrations
244
- *
245
- * @example
246
- * new DataModelBuilder<VersionedData>()
247
- * .from(Version.V1)
248
- * .migrate(Version.V2, (data) => ({ ...data, newField: '' }))
284
+ * @typeParam T - Data type for the initial version
285
+ * @param initialVersion - Version key string (e.g. DATA_MODEL_DEFAULT_VERSION or "v1")
286
+ * @returns Migration chain builder
249
287
  */
250
288
  from(initialVersion) {
251
289
  return new DataModelMigrationChain({ versionChain: [initialVersion] });
@@ -255,56 +293,43 @@ class DataModelBuilder {
255
293
  * DataModel defines the block's data structure, initial values, and migrations.
256
294
  * Used by BlockModelV3 to manage data state.
257
295
  *
258
- * Use `new DataModelBuilder<VersionedData>()` to create a DataModel:
296
+ * Use `new DataModelBuilder()` to create a DataModel.
259
297
  *
260
- * **Simple (no migrations):**
261
298
  * @example
262
- * const Version = defineDataVersions({ V1: DATA_MODEL_DEFAULT_VERSION });
263
- * type VersionedData = { [Version.V1]: BlockData };
264
- *
265
- * const dataModel = new DataModelBuilder<VersionedData>()
266
- * .from(Version.V1)
267
- * .init(() => ({ numbers: [], labels: [] }));
268
- *
269
- * **With migrations:**
270
- * @example
271
- * const Version = defineDataVersions({
272
- * V1: DATA_MODEL_DEFAULT_VERSION,
273
- * V2: 'v2',
274
- * V3: 'v3',
275
- * });
276
- *
277
- * type VersionedData = {
278
- * [Version.V1]: { numbers: number[] };
279
- * [Version.V2]: { numbers: number[]; labels: string[] };
280
- * [Version.V3]: { numbers: number[]; labels: string[]; description: string };
281
- * };
282
- *
283
- * const dataModel = new DataModelBuilder<VersionedData>()
284
- * .from(Version.V1)
285
- * .migrate(Version.V2, (data) => ({ ...data, labels: [] }))
286
- * .migrate(Version.V3, (data) => ({ ...data, description: '' }))
299
+ * // With recover() between migrations:
300
+ * // Recovered data (V2) goes through the v2→v3 migration automatically.
301
+ * const dataModel = new DataModelBuilder()
302
+ * .from<V1>(DATA_MODEL_DEFAULT_VERSION)
303
+ * .migrate<V2>("v2", (v1) => ({ ...v1, label: "" }))
287
304
  * .recover((version, data) => {
288
- * if (version === 'legacy' && typeof data === 'object' && data !== null && 'numbers' in data) {
289
- * return { numbers: (data as { numbers: number[] }).numbers, labels: [], description: '' };
290
- * }
305
+ * if (version === "legacy") return transformLegacy(data); // returns V2
291
306
  * return defaultRecover(version, data);
292
307
  * })
293
- * .init(() => ({ numbers: [], labels: [], description: '' }));
308
+ * .migrate<V3>("v3", (v2) => ({ ...v2, description: "" }))
309
+ * .init(() => ({ count: 0, label: "", description: "" }));
294
310
  */
295
311
  class DataModel {
296
- versionChain;
312
+ /** Latest version key — O(1) access for the common "already current" check. */
313
+ latestVersion;
314
+ /** Maps each known version key to the index of the first step to run from it. O(1) lookup. */
315
+ stepsByFromVersion;
297
316
  steps;
298
317
  initialDataFn;
299
318
  recoverFn;
300
- constructor({ versionChain, steps, initialDataFn, recoverFn = defaultRecover, }) {
319
+ recoverFromIndex;
320
+ /** Transforms legacy V1 model data at the initial version before running migrations. */
321
+ upgradeLegacyFn;
322
+ constructor({ versionChain, steps, initialDataFn, recoverFn = defaultRecover, recoverFromIndex, upgradeLegacyFn, }) {
301
323
  if (versionChain.length === 0) {
302
324
  throw new Error("DataModel requires at least one version key");
303
325
  }
304
- this.versionChain = versionChain;
326
+ this.latestVersion = versionChain[versionChain.length - 1];
327
+ this.stepsByFromVersion = new Map(versionChain.map((v, i) => [v, i]));
305
328
  this.steps = steps;
306
329
  this.initialDataFn = initialDataFn;
307
330
  this.recoverFn = recoverFn;
331
+ this.recoverFromIndex = recoverFromIndex ?? steps.length;
332
+ this.upgradeLegacyFn = upgradeLegacyFn;
308
333
  }
309
334
  /**
310
335
  * Internal method for creating DataModel from builder.
@@ -318,13 +343,7 @@ class DataModel {
318
343
  * The latest (current) version key in the migration chain.
319
344
  */
320
345
  get version() {
321
- return this.versionChain[this.versionChain.length - 1];
322
- }
323
- /**
324
- * Number of migration steps defined.
325
- */
326
- get migrationCount() {
327
- return this.steps.length;
346
+ return this.latestVersion;
328
347
  }
329
348
  /**
330
349
  * Get a fresh copy of the initial data.
@@ -337,11 +356,13 @@ class DataModel {
337
356
  * Used when creating new blocks or resetting to defaults.
338
357
  */
339
358
  getDefaultData() {
340
- return makeDataVersioned(this.version, this.initialDataFn());
359
+ return makeDataVersioned(this.latestVersion, this.initialDataFn());
341
360
  }
342
361
  recoverFrom(data, version) {
362
+ // Step 1: call the recover function to get data at the recover point
363
+ let currentData;
343
364
  try {
344
- return { version: this.version, data: this.recoverFn(version, data) };
365
+ currentData = this.recoverFn(version, data);
345
366
  }
346
367
  catch (error) {
347
368
  if (isDataUnrecoverableError(error)) {
@@ -353,13 +374,27 @@ class DataModel {
353
374
  warning: `Recover failed for version '${version}': ${errorMessage}`,
354
375
  };
355
376
  }
377
+ // Step 2: run any migrations that were added after recover() in the chain
378
+ for (let i = this.recoverFromIndex; i < this.steps.length; i++) {
379
+ const step = this.steps[i];
380
+ try {
381
+ currentData = step.migrate(currentData);
382
+ }
383
+ catch (error) {
384
+ const errorMessage = error instanceof Error ? error.message : String(error);
385
+ return {
386
+ ...this.getDefaultData(),
387
+ warning: `Migration ${step.fromVersion}→${step.toVersion} failed: ${errorMessage}`,
388
+ };
389
+ }
390
+ }
391
+ return { version: this.latestVersion, data: currentData };
356
392
  }
357
393
  /**
358
394
  * Migrate versioned data from any version to the latest.
359
395
  *
360
- * - If data is already at latest version, returns as-is
361
- * - If version is in chain, applies needed migrations
362
- * - If version is unknown, calls recover function
396
+ * - If version is in chain, applies needed migrations (O(1) lookup)
397
+ * - If version is unknown, calls recover function then runs remaining migrations
363
398
  * - If migration/recovery fails, returns default data with warning
364
399
  *
365
400
  * @param versioned - Data with version tag
@@ -367,14 +402,27 @@ class DataModel {
367
402
  */
368
403
  migrate(versioned) {
369
404
  const { version: fromVersion, data } = versioned;
370
- if (fromVersion === this.version) {
371
- return { version: this.version, data: data };
405
+ if (fromVersion === this.latestVersion) {
406
+ return { version: this.latestVersion, data: data };
372
407
  }
373
- const startIndex = this.versionChain.indexOf(fromVersion);
374
- if (startIndex < 0) {
408
+ const startIndex = this.stepsByFromVersion.get(fromVersion);
409
+ if (startIndex === undefined) {
375
410
  return this.recoverFrom(data, fromVersion);
376
411
  }
377
412
  let currentData = data;
413
+ // Legacy V1 upgrade: detect and transform { args, uiState } at the initial version
414
+ if (startIndex === 0 && this.upgradeLegacyFn) {
415
+ try {
416
+ currentData = this.upgradeLegacyFn(currentData);
417
+ }
418
+ catch (error) {
419
+ const errorMessage = error instanceof Error ? error.message : String(error);
420
+ return {
421
+ ...this.getDefaultData(),
422
+ warning: `Legacy upgrade failed: ${errorMessage}`,
423
+ };
424
+ }
425
+ }
378
426
  for (let i = startIndex; i < this.steps.length; i++) {
379
427
  const step = this.steps[i];
380
428
  try {
@@ -388,24 +436,9 @@ class DataModel {
388
436
  };
389
437
  }
390
438
  }
391
- return { version: this.version, data: currentData };
392
- }
393
- /**
394
- * Register callbacks for use in the VM.
395
- * Called by BlockModelV3.create() to set up internal callbacks.
396
- *
397
- * @internal
398
- */
399
- registerCallbacks() {
400
- tryRegisterCallback("__pl_data_initial", () => this.initialDataFn());
401
- tryRegisterCallback("__pl_data_upgrade", (versioned) => this.migrate(versioned));
402
- tryRegisterCallback("__pl_storage_initial", () => {
403
- const { version, data } = this.getDefaultData();
404
- const storage = createBlockStorage(data, version);
405
- return JSON.stringify(storage);
406
- });
439
+ return { version: this.latestVersion, data: currentData };
407
440
  }
408
441
  }
409
442
 
410
- export { DataModel, DataModelBuilder, DataUnrecoverableError, defaultRecover, defineDataVersions, isDataUnrecoverableError, makeDataVersioned };
443
+ export { DataModel, DataModelBuilder, DataUnrecoverableError, defaultRecover, isDataUnrecoverableError, makeDataVersioned };
411
444
  //# sourceMappingURL=block_migrations.js.map