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