@platforma-sdk/model 1.54.13 → 1.56.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.
- package/dist/bconfig/normalization.cjs +8 -1
- package/dist/bconfig/normalization.cjs.map +1 -1
- package/dist/bconfig/normalization.d.ts.map +1 -1
- package/dist/bconfig/normalization.js +8 -1
- package/dist/bconfig/normalization.js.map +1 -1
- package/dist/block_api_v3.d.ts +2 -2
- package/dist/block_api_v3.d.ts.map +1 -1
- package/dist/block_migrations.cjs +246 -214
- package/dist/block_migrations.cjs.map +1 -1
- package/dist/block_migrations.d.ts +180 -158
- package/dist/block_migrations.d.ts.map +1 -1
- package/dist/block_migrations.js +247 -214
- package/dist/block_migrations.js.map +1 -1
- package/dist/block_model.cjs +85 -35
- package/dist/block_model.cjs.map +1 -1
- package/dist/block_model.d.ts +66 -38
- package/dist/block_model.d.ts.map +1 -1
- package/dist/block_model.js +86 -36
- package/dist/block_model.js.map +1 -1
- package/dist/{builder.cjs → block_model_legacy.cjs} +2 -2
- package/dist/block_model_legacy.cjs.map +1 -0
- package/dist/{builder.d.ts → block_model_legacy.d.ts} +1 -1
- package/dist/block_model_legacy.d.ts.map +1 -0
- package/dist/{builder.js → block_model_legacy.js} +2 -2
- package/dist/block_model_legacy.js.map +1 -0
- package/dist/block_state_patch.d.ts +11 -1
- package/dist/block_state_patch.d.ts.map +1 -1
- package/dist/block_storage.cjs +126 -109
- package/dist/block_storage.cjs.map +1 -1
- package/dist/block_storage.d.ts +109 -112
- package/dist/block_storage.d.ts.map +1 -1
- package/dist/block_storage.js +126 -101
- package/dist/block_storage.js.map +1 -1
- package/dist/block_storage_callbacks.cjs +227 -0
- package/dist/block_storage_callbacks.cjs.map +1 -0
- package/dist/block_storage_callbacks.d.ts +113 -0
- package/dist/block_storage_callbacks.d.ts.map +1 -0
- package/dist/block_storage_callbacks.js +218 -0
- package/dist/block_storage_callbacks.js.map +1 -0
- package/dist/block_storage_facade.cjs +104 -0
- package/dist/block_storage_facade.cjs.map +1 -0
- package/dist/block_storage_facade.d.ts +168 -0
- package/dist/block_storage_facade.d.ts.map +1 -0
- package/dist/block_storage_facade.js +99 -0
- package/dist/block_storage_facade.js.map +1 -0
- package/dist/index.cjs +13 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +8 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/dist/package.json.cjs +1 -1
- package/dist/package.json.js +1 -1
- package/dist/platforma.d.ts +11 -4
- package/dist/platforma.d.ts.map +1 -1
- package/dist/plugin_model.cjs +171 -0
- package/dist/plugin_model.cjs.map +1 -0
- package/dist/plugin_model.d.ts +162 -0
- package/dist/plugin_model.d.ts.map +1 -0
- package/dist/plugin_model.js +169 -0
- package/dist/plugin_model.js.map +1 -0
- package/dist/render/api.cjs +20 -21
- package/dist/render/api.cjs.map +1 -1
- package/dist/render/api.d.ts +8 -8
- package/dist/render/api.d.ts.map +1 -1
- package/dist/render/api.js +20 -21
- package/dist/render/api.js.map +1 -1
- package/dist/render/internal.cjs.map +1 -1
- package/dist/render/internal.d.ts +2 -1
- package/dist/render/internal.d.ts.map +1 -1
- package/dist/render/internal.js.map +1 -1
- package/dist/version.cjs +4 -0
- package/dist/version.cjs.map +1 -1
- package/dist/version.d.ts +4 -0
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +4 -1
- package/dist/version.js.map +1 -1
- package/package.json +5 -5
- package/src/bconfig/normalization.ts +8 -1
- package/src/block_api_v3.ts +2 -2
- package/src/block_migrations.test.ts +141 -171
- package/src/block_migrations.ts +300 -285
- package/src/block_model.ts +205 -95
- package/src/{builder.ts → block_model_legacy.ts} +1 -1
- package/src/block_state_patch.ts +13 -1
- package/src/block_storage.test.ts +283 -95
- package/src/block_storage.ts +199 -188
- package/src/block_storage_callbacks.ts +326 -0
- package/src/block_storage_facade.ts +199 -0
- package/src/index.ts +7 -3
- package/src/platforma.ts +26 -7
- package/src/plugin_model.test.ts +168 -0
- package/src/plugin_model.ts +242 -0
- package/src/render/api.ts +26 -24
- package/src/render/internal.ts +3 -1
- package/src/typing.test.ts +1 -1
- package/src/version.ts +8 -0
- package/dist/block_storage_vm.cjs +0 -262
- package/dist/block_storage_vm.cjs.map +0 -1
- package/dist/block_storage_vm.d.ts +0 -59
- package/dist/block_storage_vm.d.ts.map +0 -1
- package/dist/block_storage_vm.js +0 -258
- package/dist/block_storage_vm.js.map +0 -1
- package/dist/branding.d.ts +0 -7
- package/dist/branding.d.ts.map +0 -1
- package/dist/builder.cjs.map +0 -1
- package/dist/builder.d.ts.map +0 -1
- package/dist/builder.js.map +0 -1
- package/dist/sdk_info.cjs +0 -10
- package/dist/sdk_info.cjs.map +0 -1
- package/dist/sdk_info.d.ts +0 -5
- package/dist/sdk_info.d.ts.map +0 -1
- package/dist/sdk_info.js +0 -8
- package/dist/sdk_info.js.map +0 -1
- package/dist/unionize.d.ts +0 -12
- package/dist/unionize.d.ts.map +0 -1
- package/src/block_storage_vm.ts +0 -346
- package/src/branding.ts +0 -4
- package/src/sdk_info.ts +0 -9
- package/src/unionize.ts +0 -12
package/dist/block_migrations.js
CHANGED
|
@@ -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
|
-
*
|
|
70
|
-
*
|
|
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
|
|
41
|
+
class MigrationChainBase {
|
|
77
42
|
versionChain;
|
|
78
43
|
migrationSteps;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
79
|
+
...this.recoverState(),
|
|
106
80
|
});
|
|
107
81
|
}
|
|
108
82
|
}
|
|
109
83
|
/**
|
|
110
|
-
*
|
|
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
|
-
*
|
|
113
|
-
*
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
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
|
-
|
|
129
|
-
this.migrationSteps = steps;
|
|
136
|
+
super({ versionChain, steps });
|
|
130
137
|
}
|
|
131
138
|
/**
|
|
132
|
-
* Add a migration step
|
|
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
|
|
141
|
-
* @param nextVersion -
|
|
142
|
-
* @param fn - Migration function
|
|
143
|
-
* @returns Builder with
|
|
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(
|
|
147
|
+
* .migrate<BlockDataV2>("v2", (v1) => ({ ...v1, labels: [] }))
|
|
147
148
|
*/
|
|
148
149
|
migrate(nextVersion, fn) {
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
168
|
-
*
|
|
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
|
|
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
|
|
174
|
-
* @returns Builder with
|
|
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
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
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
|
|
186
|
-
versionChain:
|
|
187
|
-
steps:
|
|
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
|
-
*
|
|
186
|
+
* Handle legacy V1 model state ({ args, uiState }) when upgrading a block from
|
|
187
|
+
* BlockModel V1 to BlockModelV3.
|
|
193
188
|
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
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
|
-
*
|
|
198
|
-
*
|
|
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
|
-
*
|
|
201
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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
|
-
* @
|
|
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
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
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
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
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
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
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
|
|
282
|
+
* Start the migration chain with the given initial data type and version key.
|
|
240
283
|
*
|
|
241
|
-
* @typeParam
|
|
242
|
-
* @param initialVersion -
|
|
243
|
-
* @returns Migration chain builder
|
|
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
|
|
296
|
+
* Use `new DataModelBuilder()` to create a DataModel.
|
|
259
297
|
*
|
|
260
|
-
* **Simple (no migrations):**
|
|
261
298
|
* @example
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
* .
|
|
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 ===
|
|
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
|
-
* .
|
|
308
|
+
* .migrate<V3>("v3", (v2) => ({ ...v2, description: "" }))
|
|
309
|
+
* .init(() => ({ count: 0, label: "", description: "" }));
|
|
294
310
|
*/
|
|
295
311
|
class DataModel {
|
|
296
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
361
|
-
* - If version is
|
|
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.
|
|
371
|
-
return { version: this.
|
|
405
|
+
if (fromVersion === this.latestVersion) {
|
|
406
|
+
return { version: this.latestVersion, data: data };
|
|
372
407
|
}
|
|
373
|
-
const startIndex = this.
|
|
374
|
-
if (startIndex
|
|
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.
|
|
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,
|
|
443
|
+
export { DataModel, DataModelBuilder, DataUnrecoverableError, defaultRecover, isDataUnrecoverableError, makeDataVersioned };
|
|
411
444
|
//# sourceMappingURL=block_migrations.js.map
|