@platforma-sdk/model 1.51.6 → 1.52.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 (89) hide show
  1. package/dist/bconfig/lambdas.d.ts +26 -4
  2. package/dist/bconfig/lambdas.d.ts.map +1 -1
  3. package/dist/bconfig/v3.d.ts +4 -2
  4. package/dist/bconfig/v3.d.ts.map +1 -1
  5. package/dist/block_api_v3.d.ts +32 -0
  6. package/dist/block_api_v3.d.ts.map +1 -0
  7. package/dist/block_migrations.cjs +138 -0
  8. package/dist/block_migrations.cjs.map +1 -0
  9. package/dist/block_migrations.d.ts +79 -0
  10. package/dist/block_migrations.d.ts.map +1 -0
  11. package/dist/block_migrations.js +136 -0
  12. package/dist/block_migrations.js.map +1 -0
  13. package/dist/block_model.cjs +222 -0
  14. package/dist/block_model.cjs.map +1 -0
  15. package/dist/block_model.d.ts +132 -0
  16. package/dist/block_model.d.ts.map +1 -0
  17. package/dist/block_model.js +220 -0
  18. package/dist/block_model.js.map +1 -0
  19. package/dist/block_storage.cjs +244 -0
  20. package/dist/block_storage.cjs.map +1 -0
  21. package/dist/block_storage.d.ts +208 -0
  22. package/dist/block_storage.d.ts.map +1 -0
  23. package/dist/block_storage.js +225 -0
  24. package/dist/block_storage.js.map +1 -0
  25. package/dist/block_storage_vm.cjs +264 -0
  26. package/dist/block_storage_vm.cjs.map +1 -0
  27. package/dist/block_storage_vm.d.ts +67 -0
  28. package/dist/block_storage_vm.d.ts.map +1 -0
  29. package/dist/block_storage_vm.js +260 -0
  30. package/dist/block_storage_vm.js.map +1 -0
  31. package/dist/builder.cjs +9 -6
  32. package/dist/builder.cjs.map +1 -1
  33. package/dist/builder.d.ts +15 -30
  34. package/dist/builder.d.ts.map +1 -1
  35. package/dist/builder.js +10 -7
  36. package/dist/builder.js.map +1 -1
  37. package/dist/components/PlDataTable.cjs.map +1 -1
  38. package/dist/components/PlDataTable.d.ts +2 -2
  39. package/dist/components/PlDataTable.d.ts.map +1 -1
  40. package/dist/components/PlDataTable.js.map +1 -1
  41. package/dist/index.cjs +25 -0
  42. package/dist/index.cjs.map +1 -1
  43. package/dist/index.d.ts +3 -0
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +4 -1
  46. package/dist/index.js.map +1 -1
  47. package/dist/internal.cjs +38 -0
  48. package/dist/internal.cjs.map +1 -1
  49. package/dist/internal.d.ts +21 -0
  50. package/dist/internal.d.ts.map +1 -1
  51. package/dist/internal.js +36 -1
  52. package/dist/internal.js.map +1 -1
  53. package/dist/package.json.cjs +1 -1
  54. package/dist/package.json.js +1 -1
  55. package/dist/platforma.d.ts +18 -3
  56. package/dist/platforma.d.ts.map +1 -1
  57. package/dist/render/api.cjs +43 -16
  58. package/dist/render/api.cjs.map +1 -1
  59. package/dist/render/api.d.ts +19 -7
  60. package/dist/render/api.d.ts.map +1 -1
  61. package/dist/render/api.js +42 -17
  62. package/dist/render/api.js.map +1 -1
  63. package/dist/render/internal.cjs.map +1 -1
  64. package/dist/render/internal.d.ts +3 -1
  65. package/dist/render/internal.d.ts.map +1 -1
  66. package/dist/render/internal.js.map +1 -1
  67. package/dist/render/util/label.cjs +9 -4
  68. package/dist/render/util/label.cjs.map +1 -1
  69. package/dist/render/util/label.d.ts.map +1 -1
  70. package/dist/render/util/label.js +9 -4
  71. package/dist/render/util/label.js.map +1 -1
  72. package/package.json +5 -5
  73. package/src/bconfig/lambdas.ts +35 -4
  74. package/src/bconfig/v3.ts +12 -2
  75. package/src/block_api_v3.ts +49 -0
  76. package/src/block_migrations.ts +173 -0
  77. package/src/block_model.ts +440 -0
  78. package/src/block_storage.test.ts +258 -0
  79. package/src/block_storage.ts +365 -0
  80. package/src/block_storage_vm.ts +349 -0
  81. package/src/builder.ts +24 -59
  82. package/src/components/PlDataTable.ts +2 -1
  83. package/src/index.ts +3 -0
  84. package/src/internal.ts +51 -0
  85. package/src/platforma.ts +31 -5
  86. package/src/render/api.ts +52 -21
  87. package/src/render/internal.ts +3 -1
  88. package/src/render/util/label.test.ts +25 -0
  89. package/src/render/util/label.ts +11 -5
@@ -0,0 +1,365 @@
1
+ /**
2
+ * BlockStorage - Typed storage abstraction for block persistent data.
3
+ *
4
+ * This module provides:
5
+ * - A typed structure for block storage with versioning and plugin support
6
+ * - Utility functions for manipulating storage
7
+ * - Handler interfaces for model-level customization
8
+ *
9
+ * @module block_storage
10
+ */
11
+
12
+ // =============================================================================
13
+ // Core Types
14
+ // =============================================================================
15
+
16
+ /**
17
+ * Discriminator key for BlockStorage format detection.
18
+ * This unique hash-based key identifies data as BlockStorage vs legacy formats.
19
+ */
20
+ export const BLOCK_STORAGE_KEY = '__pl_a7f3e2b9__';
21
+
22
+ /**
23
+ * Current BlockStorage schema version.
24
+ * Increment this when the storage structure itself changes (not block state migrations).
25
+ */
26
+ export const BLOCK_STORAGE_SCHEMA_VERSION = 'v1';
27
+
28
+ /**
29
+ * Type for valid schema versions
30
+ */
31
+ export type BlockStorageSchemaVersion = 'v1'; // Add 'v2', 'v3', etc. as schema evolves
32
+
33
+ /**
34
+ * Plugin key type - keys starting with `@plugin/` are reserved for plugin data
35
+ */
36
+ export type PluginKey = `@plugin/${string}`;
37
+
38
+ /**
39
+ * Core BlockStorage type that holds:
40
+ * - __pl_a7f3e2b9__: Schema version (discriminator key identifies BlockStorage format)
41
+ * - __dataVersion: Version number for block data migrations
42
+ * - __data: The block's user-facing data (state)
43
+ * - @plugin/*: Optional plugin-specific data
44
+ */
45
+ export type BlockStorage<TState = unknown> = {
46
+ /** Schema version - the key itself is the discriminator */
47
+ readonly [BLOCK_STORAGE_KEY]: BlockStorageSchemaVersion;
48
+ /** Version of the block data, used for migrations */
49
+ __dataVersion: number;
50
+ /** The block's user-facing data (state) */
51
+ __data: TState;
52
+ } & {
53
+ /** Plugin-specific data, keyed by `@plugin/<pluginName>` */
54
+ [K in PluginKey]?: unknown;
55
+ };
56
+
57
+ /**
58
+ * Type guard to check if a value is a valid BlockStorage object.
59
+ * Checks for the discriminator key and valid schema version.
60
+ */
61
+ export function isBlockStorage(value: unknown): value is BlockStorage {
62
+ if (value === null || typeof value !== 'object') return false;
63
+ const obj = value as Record<string, unknown>;
64
+ const schemaVersion = obj[BLOCK_STORAGE_KEY];
65
+ // Currently only 'v1' is valid, but this allows future versions
66
+ return schemaVersion === 'v1'; // Add more versions as schema evolves
67
+ }
68
+
69
+ // =============================================================================
70
+ // Factory Functions
71
+ // =============================================================================
72
+
73
+ /**
74
+ * Creates a BlockStorage with the given initial data
75
+ *
76
+ * @param initialData - The initial data value (defaults to empty object)
77
+ * @param version - The initial data version (defaults to 1)
78
+ * @returns A new BlockStorage instance with discriminator key
79
+ */
80
+ export function createBlockStorage<TState = unknown>(
81
+ initialData: TState = {} as TState,
82
+ version: number = 1,
83
+ ): BlockStorage<TState> {
84
+ return {
85
+ [BLOCK_STORAGE_KEY]: BLOCK_STORAGE_SCHEMA_VERSION,
86
+ __dataVersion: version,
87
+ __data: initialData,
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Normalizes raw storage data to BlockStorage format.
93
+ * If the input is already a BlockStorage, returns it as-is.
94
+ * If the input is legacy format (raw state), wraps it in BlockStorage structure.
95
+ *
96
+ * @param raw - Raw storage data (may be legacy format or BlockStorage)
97
+ * @returns Normalized BlockStorage
98
+ */
99
+ export function normalizeBlockStorage<TState = unknown>(raw: unknown): BlockStorage<TState> {
100
+ if (isBlockStorage(raw)) {
101
+ return raw as BlockStorage<TState>;
102
+ }
103
+ // Legacy format: raw is the state directly
104
+ return createBlockStorage(raw as TState, 1);
105
+ }
106
+
107
+ // =============================================================================
108
+ // Data Access & Update Functions
109
+ // =============================================================================
110
+
111
+ /**
112
+ * Gets the data from BlockStorage
113
+ *
114
+ * @param storage - The BlockStorage instance
115
+ * @returns The data value
116
+ */
117
+ export function getStorageData<TState>(storage: BlockStorage<TState>): TState {
118
+ return storage.__data;
119
+ }
120
+
121
+ /**
122
+ * Derives data from raw block storage.
123
+ * This function is meant to be called from sdk/ui-vue to extract
124
+ * user-facing data from the raw storage returned by the middle layer.
125
+ *
126
+ * The middle layer returns raw storage (opaque to it), and the UI
127
+ * uses this function to derive the actual data value.
128
+ *
129
+ * @param rawStorage - Raw storage data from middle layer (may be any format)
130
+ * @returns The extracted data value, or undefined if storage is undefined/null
131
+ */
132
+ export function deriveDataFromStorage<TData = unknown>(
133
+ rawStorage: unknown,
134
+ ): TData {
135
+ // Normalize to BlockStorage format (handles legacy formats too)
136
+ const storage = normalizeBlockStorage<TData>(rawStorage);
137
+ return getStorageData(storage);
138
+ }
139
+
140
+ /** Payload for storage mutation operations. SDK defines specific operations. */
141
+ export type MutateStoragePayload<T = unknown> = { operation: 'update-data'; value: T };
142
+
143
+ /**
144
+ * Updates the data in BlockStorage (immutable)
145
+ *
146
+ * @param storage - The current BlockStorage
147
+ * @param payload - The update payload with operation and value
148
+ * @returns A new BlockStorage with updated data
149
+ */
150
+ export function updateStorageData<TValue = unknown>(
151
+ storage: BlockStorage<TValue>,
152
+ payload: MutateStoragePayload<TValue>,
153
+ ): BlockStorage<TValue> {
154
+ switch (payload.operation) {
155
+ case 'update-data':
156
+ return { ...storage, __data: payload.value };
157
+ default:
158
+ throw new Error(`Unknown storage operation: ${(payload as { operation: string }).operation}`);
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Gets the data version from BlockStorage
164
+ *
165
+ * @param storage - The BlockStorage instance
166
+ * @returns The data version number
167
+ */
168
+ export function getStorageDataVersion(storage: BlockStorage): number {
169
+ return storage.__dataVersion;
170
+ }
171
+
172
+ /**
173
+ * Updates the data version in BlockStorage (immutable)
174
+ *
175
+ * @param storage - The current BlockStorage
176
+ * @param version - The new version number
177
+ * @returns A new BlockStorage with updated version
178
+ */
179
+ export function updateStorageDataVersion<TState>(
180
+ storage: BlockStorage<TState>,
181
+ version: number,
182
+ ): BlockStorage<TState> {
183
+ return { ...storage, __dataVersion: version };
184
+ }
185
+
186
+ // =============================================================================
187
+ // Plugin Data Functions
188
+ // =============================================================================
189
+
190
+ /**
191
+ * Gets plugin-specific data from BlockStorage
192
+ *
193
+ * @param storage - The BlockStorage instance
194
+ * @param pluginName - The plugin name (without `@plugin/` prefix)
195
+ * @returns The plugin data or undefined if not set
196
+ */
197
+ export function getPluginData<TData = unknown>(
198
+ storage: BlockStorage,
199
+ pluginName: string,
200
+ ): TData | undefined {
201
+ const key: PluginKey = `@plugin/${pluginName}`;
202
+ return storage[key] as TData | undefined;
203
+ }
204
+
205
+ /**
206
+ * Sets plugin-specific data in BlockStorage (immutable)
207
+ *
208
+ * @param storage - The current BlockStorage
209
+ * @param pluginName - The plugin name (without `@plugin/` prefix)
210
+ * @param data - The plugin data to store
211
+ * @returns A new BlockStorage with updated plugin data
212
+ */
213
+ export function setPluginData<TState>(
214
+ storage: BlockStorage<TState>,
215
+ pluginName: string,
216
+ data: unknown,
217
+ ): BlockStorage<TState> {
218
+ const key: PluginKey = `@plugin/${pluginName}`;
219
+ return { ...storage, [key]: data };
220
+ }
221
+
222
+ /**
223
+ * Removes plugin-specific data from BlockStorage (immutable)
224
+ *
225
+ * @param storage - The current BlockStorage
226
+ * @param pluginName - The plugin name (without `@plugin/` prefix)
227
+ * @returns A new BlockStorage with the plugin data removed
228
+ */
229
+ export function removePluginData<TState>(
230
+ storage: BlockStorage<TState>,
231
+ pluginName: string,
232
+ ): BlockStorage<TState> {
233
+ const key: PluginKey = `@plugin/${pluginName}`;
234
+ const { [key]: _, ...rest } = storage;
235
+ return rest as BlockStorage<TState>;
236
+ }
237
+
238
+ /**
239
+ * Gets all plugin names that have data stored
240
+ *
241
+ * @param storage - The BlockStorage instance
242
+ * @returns Array of plugin names (without `@plugin/` prefix)
243
+ */
244
+ export function getPluginNames(storage: BlockStorage): string[] {
245
+ return Object.keys(storage)
246
+ .filter((key): key is PluginKey => key.startsWith('@plugin/'))
247
+ .map((key) => key.slice('@plugin/'.length));
248
+ }
249
+
250
+ // =============================================================================
251
+ // Generic Storage Access
252
+ // =============================================================================
253
+
254
+ /**
255
+ * Gets a value from BlockStorage by key
256
+ *
257
+ * @param storage - The BlockStorage instance
258
+ * @param key - The key to retrieve
259
+ * @returns The value at the given key
260
+ */
261
+ export function getFromStorage<
262
+ TState,
263
+ K extends keyof BlockStorage<TState>,
264
+ >(storage: BlockStorage<TState>, key: K): BlockStorage<TState>[K] {
265
+ return storage[key];
266
+ }
267
+
268
+ /**
269
+ * Updates a value in BlockStorage by key (immutable)
270
+ *
271
+ * @param storage - The current BlockStorage
272
+ * @param key - The key to update
273
+ * @param value - The new value
274
+ * @returns A new BlockStorage with the updated value
275
+ */
276
+ export function updateStorage<
277
+ TState,
278
+ K extends keyof BlockStorage<TState>,
279
+ >(
280
+ storage: BlockStorage<TState>,
281
+ key: K,
282
+ value: BlockStorage<TState>[K],
283
+ ): BlockStorage<TState> {
284
+ return { ...storage, [key]: value };
285
+ }
286
+
287
+ // =============================================================================
288
+ // Storage Handlers (for Phase 2 - Model-Level Customization)
289
+ // =============================================================================
290
+
291
+ /**
292
+ * Interface for model-configurable storage operations.
293
+ * These handlers allow block models to customize how storage is managed.
294
+ */
295
+ export interface BlockStorageHandlers<TState = unknown> {
296
+ /**
297
+ * Called when setState is invoked - transforms the new state before storing.
298
+ * Default behavior: replaces the state directly.
299
+ *
300
+ * @param currentStorage - The current BlockStorage
301
+ * @param newState - The new state being set
302
+ * @returns The updated BlockStorage
303
+ */
304
+ transformStateForStorage?: (
305
+ currentStorage: BlockStorage<TState>,
306
+ newState: TState,
307
+ ) => BlockStorage<TState>;
308
+
309
+ /**
310
+ * Called when reading state for args derivation.
311
+ * Default behavior: returns the state directly.
312
+ *
313
+ * @param storage - The current BlockStorage
314
+ * @returns The state to use for args derivation
315
+ */
316
+ deriveStateForArgs?: (storage: BlockStorage<TState>) => TState;
317
+
318
+ /**
319
+ * Called during storage schema migration.
320
+ * Default behavior: updates stateVersion only.
321
+ *
322
+ * @param oldStorage - The storage before migration
323
+ * @param fromVersion - The version migrating from
324
+ * @param toVersion - The version migrating to
325
+ * @returns The migrated BlockStorage
326
+ */
327
+ migrateStorage?: (
328
+ oldStorage: BlockStorage<TState>,
329
+ fromVersion: number,
330
+ toVersion: number,
331
+ ) => BlockStorage<TState>;
332
+ }
333
+
334
+ /**
335
+ * Default implementations of storage handlers
336
+ */
337
+ export const defaultBlockStorageHandlers: Required<BlockStorageHandlers<unknown>> = {
338
+ transformStateForStorage: <TState>(
339
+ storage: BlockStorage<TState>,
340
+ newState: TState,
341
+ ): BlockStorage<TState> => updateStorageData(storage, { operation: 'update-data', value: newState }),
342
+
343
+ deriveStateForArgs: <TState>(storage: BlockStorage<TState>): TState => getStorageData(storage),
344
+
345
+ migrateStorage: <TState>(
346
+ storage: BlockStorage<TState>,
347
+ _fromVersion: number,
348
+ toVersion: number,
349
+ ): BlockStorage<TState> => updateStorageDataVersion(storage, toVersion),
350
+ };
351
+
352
+ /**
353
+ * Merges custom handlers with defaults
354
+ *
355
+ * @param customHandlers - Custom handlers to merge
356
+ * @returns Complete handlers with defaults for missing functions
357
+ */
358
+ export function mergeBlockStorageHandlers<TState>(
359
+ customHandlers?: BlockStorageHandlers<TState>,
360
+ ): Required<BlockStorageHandlers<TState>> {
361
+ return {
362
+ ...defaultBlockStorageHandlers,
363
+ ...customHandlers,
364
+ } as Required<BlockStorageHandlers<TState>>;
365
+ }
@@ -0,0 +1,349 @@
1
+ /**
2
+ * BlockStorage VM Integration - Internal module for VM-based storage operations.
3
+ *
4
+ * This module auto-registers internal callbacks that the middle layer can invoke
5
+ * to perform storage transformations. Block developers never interact with these
6
+ * directly - they only see `state`.
7
+ *
8
+ * Registered callbacks (all prefixed with `__pl_` for internal SDK use):
9
+ * - `__pl_storage_normalize`: (rawStorage) => { storage, data }
10
+ * - `__pl_storage_applyUpdate`: (currentStorageJson, payload) => updatedStorageJson
11
+ * - `__pl_storage_getInfo`: (rawStorage) => JSON string with storage info
12
+ * - `__pl_storage_migrate`: (currentStorageJson) => MigrationResult
13
+ * - `__pl_args_derive`: (storageJson) => ArgsDeriveResult
14
+ * - `__pl_prerunArgs_derive`: (storageJson) => ArgsDeriveResult
15
+ *
16
+ * Callbacks registered by DataModel.registerCallbacks():
17
+ * - `__pl_data_initial`: () => initial data
18
+ * - `__pl_data_upgrade`: (versioned) => UpgradeResult
19
+ * - `__pl_storage_initial`: () => initial BlockStorage as JSON string
20
+ *
21
+ * @module block_storage_vm
22
+ * @internal
23
+ */
24
+
25
+ import {
26
+ BLOCK_STORAGE_KEY,
27
+ BLOCK_STORAGE_SCHEMA_VERSION,
28
+ type BlockStorage,
29
+ type MutateStoragePayload,
30
+ createBlockStorage,
31
+ getStorageData,
32
+ isBlockStorage,
33
+ updateStorageData,
34
+ } from './block_storage';
35
+ import { tryGetCfgRenderCtx, tryRegisterCallback } from './internal';
36
+
37
+ /**
38
+ * Result of storage normalization
39
+ */
40
+ export interface NormalizeStorageResult {
41
+ /** The normalized BlockStorage object */
42
+ storage: BlockStorage;
43
+ /** The extracted data (what developers see) */
44
+ data: unknown;
45
+ }
46
+
47
+ /**
48
+ * Normalizes raw storage data and extracts state.
49
+ * Handles all formats:
50
+ * - New BlockStorage format (has discriminator)
51
+ * - Legacy V1/V2 format ({ args, uiState })
52
+ * - Raw V3 state (any other format)
53
+ *
54
+ * @param rawStorage - Raw data from blockStorage field (may be JSON string or object)
55
+ * @returns Object with normalized storage and extracted state
56
+ */
57
+ function normalizeStorage(rawStorage: unknown): NormalizeStorageResult {
58
+ // Handle undefined/null
59
+ if (rawStorage === undefined || rawStorage === null) {
60
+ const storage = createBlockStorage({});
61
+ return { storage, data: {} };
62
+ }
63
+
64
+ // Parse JSON string if needed
65
+ let parsed = rawStorage;
66
+ if (typeof rawStorage === 'string') {
67
+ try {
68
+ parsed = JSON.parse(rawStorage);
69
+ } catch {
70
+ // If parsing fails, treat string as the data
71
+ const storage = createBlockStorage(rawStorage);
72
+ return { storage, data: rawStorage };
73
+ }
74
+ }
75
+
76
+ // Check for BlockStorage format (has discriminator)
77
+ if (isBlockStorage(parsed)) {
78
+ return { storage: parsed, data: getStorageData(parsed) };
79
+ }
80
+
81
+ // Check for legacy V1/V2 format: { args, uiState }
82
+ if (isLegacyModelV1ApiFormat(parsed)) {
83
+ // For legacy format, the whole object IS the data
84
+ const storage = createBlockStorage(parsed);
85
+ return { storage, data: parsed };
86
+ }
87
+
88
+ // Raw V3 data - wrap it
89
+ const storage = createBlockStorage(parsed);
90
+ return { storage, data: parsed };
91
+ }
92
+
93
+ /**
94
+ * Applies a state update to existing storage.
95
+ * Used when setData is called from the frontend.
96
+ *
97
+ * @param currentStorageJson - Current storage as JSON string (must be defined)
98
+ * @param newData - New data from application
99
+ * @returns Updated storage as JSON string
100
+ */
101
+ function applyStorageUpdate(currentStorageJson: string, payload: MutateStoragePayload): string {
102
+ const { storage: currentStorage } = normalizeStorage(currentStorageJson);
103
+
104
+ // Update data while preserving other storage fields (version, plugins)
105
+ const updatedStorage = updateStorageData(currentStorage, payload);
106
+
107
+ return JSON.stringify(updatedStorage);
108
+ }
109
+
110
+ /**
111
+ * Checks if data is in legacy Model API v1 format.
112
+ * Legacy format has { args, uiState? } at top level without the BlockStorage discriminator.
113
+ */
114
+ function isLegacyModelV1ApiFormat(data: unknown): data is { args?: unknown } {
115
+ if (data === null || typeof data !== 'object') return false;
116
+ if (isBlockStorage(data)) return false;
117
+
118
+ const obj = data as Record<string, unknown>;
119
+ return 'args' in obj;
120
+ }
121
+
122
+ // =============================================================================
123
+ // Auto-register internal callbacks when module is loaded in VM
124
+ // =============================================================================
125
+
126
+ // Register normalize callback
127
+ tryRegisterCallback('__pl_storage_normalize', (rawStorage: unknown) => {
128
+ return normalizeStorage(rawStorage);
129
+ });
130
+
131
+ // Register apply update callback (requires existing storage)
132
+ tryRegisterCallback('__pl_storage_applyUpdate', (currentStorageJson: string, payload: MutateStoragePayload) => {
133
+ return applyStorageUpdate(currentStorageJson, payload);
134
+ });
135
+
136
+ /**
137
+ * Storage info result returned by __pl_storage_getInfo callback.
138
+ */
139
+ export interface StorageInfo {
140
+ /** Current data version (1-based, starts at 1) */
141
+ dataVersion: number;
142
+ }
143
+
144
+ /**
145
+ * Gets storage info from raw storage data.
146
+ * Returns structured info about the storage state.
147
+ *
148
+ * @param rawStorage - Raw data from blockStorage field (may be JSON string or object)
149
+ * @returns JSON string with storage info
150
+ */
151
+ function getStorageInfo(rawStorage: unknown): string {
152
+ const { storage } = normalizeStorage(rawStorage);
153
+ const info: StorageInfo = {
154
+ dataVersion: storage.__dataVersion,
155
+ };
156
+ return JSON.stringify(info);
157
+ }
158
+
159
+ // Register get info callback
160
+ tryRegisterCallback('__pl_storage_getInfo', (rawStorage: unknown) => {
161
+ return getStorageInfo(rawStorage);
162
+ });
163
+
164
+ // =============================================================================
165
+ // Migration Support
166
+ // =============================================================================
167
+
168
+ /**
169
+ * Result of storage migration.
170
+ * Returned by __pl_storage_migrate callback.
171
+ *
172
+ * - Error result: { error: string } - serious failure (no context, etc.)
173
+ * - Success result: { newStorageJson: string, info: string, warn?: string } - migration succeeded or reset to initial
174
+ */
175
+ export type MigrationResult =
176
+ | { error: string }
177
+ | { error?: undefined; newStorageJson: string; info: string; warn?: string };
178
+
179
+ /** Result from Migrator.upgrade() */
180
+ interface UpgradeResult {
181
+ version: number;
182
+ data: unknown;
183
+ warning?: string;
184
+ }
185
+
186
+ /**
187
+ * Runs storage migration using the DataModel's upgrade callback.
188
+ * This is the main entry point for the middle layer to trigger migrations.
189
+ *
190
+ * Uses the '__pl_data_upgrade' callback registered by DataModel.registerCallbacks() which:
191
+ * - Handles all migration logic internally
192
+ * - Returns { version, data, warning? } - warning present if reset to initial data
193
+ *
194
+ * @param currentStorageJson - Current storage as JSON string (or undefined)
195
+ * @returns MigrationResult
196
+ */
197
+ function migrateStorage(currentStorageJson: string | undefined): MigrationResult {
198
+ // Get the callback registry context
199
+ const ctx = tryGetCfgRenderCtx();
200
+ if (ctx === undefined) {
201
+ return { error: 'Not in config rendering context' };
202
+ }
203
+
204
+ // Normalize storage to get current data and version
205
+ const { storage: currentStorage, data: currentData } = normalizeStorage(currentStorageJson);
206
+ const currentVersion = currentStorage.__dataVersion;
207
+
208
+ // Helper to create storage with given data and version
209
+ const createStorageJson = (data: unknown, version: number): string => {
210
+ return JSON.stringify({
211
+ ...currentStorage,
212
+ __dataVersion: version,
213
+ __data: data,
214
+ });
215
+ };
216
+
217
+ // Get the upgrade callback (registered by DataModel.registerCallbacks())
218
+ const upgradeCallback = ctx.callbackRegistry['__pl_data_upgrade'] as ((v: { version: number; data: unknown }) => UpgradeResult) | undefined;
219
+ if (typeof upgradeCallback !== 'function') {
220
+ return { error: '__pl_data_upgrade callback not found (DataModel not registered)' };
221
+ }
222
+
223
+ // Call the migrator's upgrade function
224
+ let result: UpgradeResult;
225
+ try {
226
+ result = upgradeCallback({ version: currentVersion, data: currentData });
227
+ } catch (e) {
228
+ const errorMsg = e instanceof Error ? e.message : String(e);
229
+ return { error: `upgrade() threw: ${errorMsg}` };
230
+ }
231
+
232
+ // Build info message
233
+ const info = result.version === currentVersion
234
+ ? `No migration needed (v${currentVersion})`
235
+ : result.warning
236
+ ? `Reset to initial data (v${result.version})`
237
+ : `Migrated v${currentVersion}→v${result.version}`;
238
+
239
+ return {
240
+ newStorageJson: createStorageJson(result.data, result.version),
241
+ info,
242
+ warn: result.warning,
243
+ };
244
+ }
245
+
246
+ // Register migrate callback
247
+ tryRegisterCallback('__pl_storage_migrate', (currentStorageJson: string | undefined) => {
248
+ return migrateStorage(currentStorageJson);
249
+ });
250
+
251
+ // =============================================================================
252
+ // Args Derivation from Storage
253
+ // =============================================================================
254
+
255
+ /**
256
+ * Result of args derivation from storage.
257
+ * Returned by __pl_args_derive and __pl_prerunArgs_derive callbacks.
258
+ */
259
+ export type ArgsDeriveResult =
260
+ | { error: string }
261
+ | { error?: undefined; value: unknown };
262
+
263
+ /**
264
+ * Derives args from storage using the registered 'args' callback.
265
+ * This extracts data from storage and passes it to the block's args() function.
266
+ *
267
+ * @param storageJson - Storage as JSON string
268
+ * @returns ArgsDeriveResult with derived args or error
269
+ */
270
+ function deriveArgsFromStorage(storageJson: string): ArgsDeriveResult {
271
+ const ctx = tryGetCfgRenderCtx();
272
+ if (ctx === undefined) {
273
+ return { error: 'Not in config rendering context' };
274
+ }
275
+
276
+ // Extract data from storage
277
+ const { data } = normalizeStorage(storageJson);
278
+
279
+ // Get the args callback (registered by BlockModelV3.args())
280
+ const argsCallback = ctx.callbackRegistry['args'] as ((data: unknown) => unknown) | undefined;
281
+ if (typeof argsCallback !== 'function') {
282
+ return { error: 'args callback not found' };
283
+ }
284
+
285
+ // Call the args callback with extracted data
286
+ try {
287
+ const result = argsCallback(data);
288
+ return { value: result };
289
+ } catch (e) {
290
+ const errorMsg = e instanceof Error ? e.message : String(e);
291
+ return { error: `args() threw: ${errorMsg}` };
292
+ }
293
+ }
294
+
295
+ // Register args derivation callback
296
+ tryRegisterCallback('__pl_args_derive', (storageJson: string) => {
297
+ return deriveArgsFromStorage(storageJson);
298
+ });
299
+
300
+ /**
301
+ * Derives prerunArgs from storage using the registered 'prerunArgs' callback.
302
+ * Falls back to 'args' callback if 'prerunArgs' is not defined.
303
+ *
304
+ * @param storageJson - Storage as JSON string
305
+ * @returns ArgsDeriveResult with derived prerunArgs or error
306
+ */
307
+ function derivePrerunArgsFromStorage(storageJson: string): ArgsDeriveResult {
308
+ const ctx = tryGetCfgRenderCtx();
309
+ if (ctx === undefined) {
310
+ return { error: 'Not in config rendering context' };
311
+ }
312
+
313
+ // Extract data from storage
314
+ const { data } = normalizeStorage(storageJson);
315
+
316
+ // Try prerunArgs callback first
317
+ const prerunArgsCallback = ctx.callbackRegistry['prerunArgs'] as ((data: unknown) => unknown) | undefined;
318
+ if (typeof prerunArgsCallback === 'function') {
319
+ try {
320
+ const result = prerunArgsCallback(data);
321
+ return { value: result };
322
+ } catch (e) {
323
+ const errorMsg = e instanceof Error ? e.message : String(e);
324
+ return { error: `prerunArgs() threw: ${errorMsg}` };
325
+ }
326
+ }
327
+
328
+ // Fall back to args callback
329
+ const argsCallback = ctx.callbackRegistry['args'] as ((data: unknown) => unknown) | undefined;
330
+ if (typeof argsCallback !== 'function') {
331
+ return { error: 'args callback not found (fallback from missing prerunArgs)' };
332
+ }
333
+
334
+ try {
335
+ const result = argsCallback(data);
336
+ return { value: result };
337
+ } catch (e) {
338
+ const errorMsg = e instanceof Error ? e.message : String(e);
339
+ return { error: `args() threw (fallback): ${errorMsg}` };
340
+ }
341
+ }
342
+
343
+ // Register prerunArgs derivation callback
344
+ tryRegisterCallback('__pl_prerunArgs_derive', (storageJson: string) => {
345
+ return derivePrerunArgsFromStorage(storageJson);
346
+ });
347
+
348
+ // Export discriminator key and schema version for external checks
349
+ export { BLOCK_STORAGE_KEY, BLOCK_STORAGE_SCHEMA_VERSION };