@platforma-sdk/model 1.51.9 → 1.52.3

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 (92) 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/PFrameForGraphs.cjs.map +1 -1
  38. package/dist/components/PFrameForGraphs.d.ts +2 -2
  39. package/dist/components/PFrameForGraphs.d.ts.map +1 -1
  40. package/dist/components/PFrameForGraphs.js.map +1 -1
  41. package/dist/components/PlDataTable.cjs.map +1 -1
  42. package/dist/components/PlDataTable.d.ts +3 -3
  43. package/dist/components/PlDataTable.d.ts.map +1 -1
  44. package/dist/components/PlDataTable.js.map +1 -1
  45. package/dist/index.cjs +25 -0
  46. package/dist/index.cjs.map +1 -1
  47. package/dist/index.d.ts +3 -0
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +4 -1
  50. package/dist/index.js.map +1 -1
  51. package/dist/internal.cjs +38 -0
  52. package/dist/internal.cjs.map +1 -1
  53. package/dist/internal.d.ts +21 -0
  54. package/dist/internal.d.ts.map +1 -1
  55. package/dist/internal.js +36 -1
  56. package/dist/internal.js.map +1 -1
  57. package/dist/package.json.cjs +1 -1
  58. package/dist/package.json.js +1 -1
  59. package/dist/pframe_utils/columns.cjs.map +1 -1
  60. package/dist/pframe_utils/columns.d.ts +3 -3
  61. package/dist/pframe_utils/columns.d.ts.map +1 -1
  62. package/dist/pframe_utils/columns.js.map +1 -1
  63. package/dist/platforma.d.ts +18 -3
  64. package/dist/platforma.d.ts.map +1 -1
  65. package/dist/render/api.cjs +43 -16
  66. package/dist/render/api.cjs.map +1 -1
  67. package/dist/render/api.d.ts +19 -7
  68. package/dist/render/api.d.ts.map +1 -1
  69. package/dist/render/api.js +42 -17
  70. package/dist/render/api.js.map +1 -1
  71. package/dist/render/internal.cjs.map +1 -1
  72. package/dist/render/internal.d.ts +3 -1
  73. package/dist/render/internal.d.ts.map +1 -1
  74. package/dist/render/internal.js.map +1 -1
  75. package/package.json +7 -7
  76. package/src/bconfig/lambdas.ts +35 -4
  77. package/src/bconfig/v3.ts +12 -2
  78. package/src/block_api_v3.ts +49 -0
  79. package/src/block_migrations.ts +173 -0
  80. package/src/block_model.ts +440 -0
  81. package/src/block_storage.test.ts +258 -0
  82. package/src/block_storage.ts +365 -0
  83. package/src/block_storage_vm.ts +349 -0
  84. package/src/builder.ts +24 -59
  85. package/src/components/PFrameForGraphs.ts +2 -2
  86. package/src/components/PlDataTable.ts +3 -3
  87. package/src/index.ts +3 -0
  88. package/src/internal.ts +51 -0
  89. package/src/pframe_utils/columns.ts +3 -3
  90. package/src/platforma.ts +31 -5
  91. package/src/render/api.ts +52 -21
  92. package/src/render/internal.ts +3 -1
@@ -0,0 +1,258 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ BLOCK_STORAGE_KEY,
4
+ BLOCK_STORAGE_SCHEMA_VERSION,
5
+ createBlockStorage,
6
+ defaultBlockStorageHandlers,
7
+ getFromStorage,
8
+ getPluginData,
9
+ getPluginNames,
10
+ getStorageData,
11
+ getStorageDataVersion,
12
+ isBlockStorage,
13
+ mergeBlockStorageHandlers,
14
+ normalizeBlockStorage,
15
+ removePluginData,
16
+ setPluginData,
17
+ updateStorageData,
18
+ updateStorageDataVersion,
19
+ updateStorage,
20
+ } from './block_storage';
21
+
22
+ describe('BlockStorage', () => {
23
+ describe('BLOCK_STORAGE_KEY and BLOCK_STORAGE_SCHEMA_VERSION', () => {
24
+ it('should have correct key constant', () => {
25
+ expect(typeof BLOCK_STORAGE_KEY).toBe('string');
26
+ expect(BLOCK_STORAGE_KEY).toBe('__pl_a7f3e2b9__');
27
+ });
28
+
29
+ it('should have correct schema version', () => {
30
+ expect(BLOCK_STORAGE_SCHEMA_VERSION).toBe('v1');
31
+ });
32
+ });
33
+
34
+ describe('isBlockStorage', () => {
35
+ it('should return true for valid BlockStorage with discriminator', () => {
36
+ const storage = createBlockStorage({});
37
+ expect(isBlockStorage(storage)).toBe(true);
38
+ });
39
+
40
+ it('should return true for BlockStorage with plugin data', () => {
41
+ const storage = setPluginData(createBlockStorage({ foo: 'bar' }), 'test', { data: 123 });
42
+ expect(isBlockStorage(storage)).toBe(true);
43
+ });
44
+
45
+ it('should return false for null', () => {
46
+ expect(isBlockStorage(null)).toBe(false);
47
+ });
48
+
49
+ it('should return false for undefined', () => {
50
+ expect(isBlockStorage(undefined)).toBe(false);
51
+ });
52
+
53
+ it('should return false for primitive values', () => {
54
+ expect(isBlockStorage(42)).toBe(false);
55
+ expect(isBlockStorage('string')).toBe(false);
56
+ expect(isBlockStorage(true)).toBe(false);
57
+ });
58
+
59
+ it('should return false for objects without discriminator', () => {
60
+ expect(isBlockStorage({ __dataVersion: 1, __data: {} })).toBe(false);
61
+ });
62
+
63
+ it('should return false for objects with wrong discriminator value', () => {
64
+ expect(isBlockStorage({ [BLOCK_STORAGE_KEY]: 'wrong', __dataVersion: 1, __data: {} })).toBe(false);
65
+ });
66
+ });
67
+
68
+ describe('createBlockStorage', () => {
69
+ it('should create storage with discriminator key and default values', () => {
70
+ const storage = createBlockStorage();
71
+ expect(storage[BLOCK_STORAGE_KEY]).toBe(BLOCK_STORAGE_SCHEMA_VERSION);
72
+ expect(storage.__dataVersion).toBe(1);
73
+ expect(storage.__data).toEqual({});
74
+ });
75
+
76
+ it('should create storage with custom initial data', () => {
77
+ const data = { numbers: [1, 2, 3] };
78
+ const storage = createBlockStorage(data);
79
+ expect(storage[BLOCK_STORAGE_KEY]).toBe(BLOCK_STORAGE_SCHEMA_VERSION);
80
+ expect(storage.__dataVersion).toBe(1);
81
+ expect(storage.__data).toEqual(data);
82
+ });
83
+
84
+ it('should create storage with custom version', () => {
85
+ const storage = createBlockStorage({ foo: 'bar' }, 5);
86
+ expect(storage[BLOCK_STORAGE_KEY]).toBe(BLOCK_STORAGE_SCHEMA_VERSION);
87
+ expect(storage.__dataVersion).toBe(5);
88
+ expect(storage.__data).toEqual({ foo: 'bar' });
89
+ });
90
+ });
91
+
92
+ describe('normalizeBlockStorage', () => {
93
+ it('should return BlockStorage as-is', () => {
94
+ const storage = createBlockStorage({ data: 'test' }, 2);
95
+ const normalized = normalizeBlockStorage(storage);
96
+ expect(normalized).toEqual(storage);
97
+ });
98
+
99
+ it('should wrap legacy data in BlockStorage structure', () => {
100
+ const legacyData = { numbers: [1, 2, 3], name: 'test' };
101
+ const normalized = normalizeBlockStorage(legacyData);
102
+ expect(normalized[BLOCK_STORAGE_KEY]).toBe(BLOCK_STORAGE_SCHEMA_VERSION);
103
+ expect(normalized.__dataVersion).toBe(1);
104
+ expect(normalized.__data).toEqual(legacyData);
105
+ });
106
+
107
+ it('should wrap primitive legacy data', () => {
108
+ const normalized = normalizeBlockStorage('simple string');
109
+ expect(normalized[BLOCK_STORAGE_KEY]).toBe(BLOCK_STORAGE_SCHEMA_VERSION);
110
+ expect(normalized.__dataVersion).toBe(1);
111
+ expect(normalized.__data).toBe('simple string');
112
+ });
113
+
114
+ it('should wrap null legacy data', () => {
115
+ const normalized = normalizeBlockStorage(null);
116
+ expect(normalized[BLOCK_STORAGE_KEY]).toBe(BLOCK_STORAGE_SCHEMA_VERSION);
117
+ expect(normalized.__dataVersion).toBe(1);
118
+ expect(normalized.__data).toBeNull();
119
+ });
120
+ });
121
+
122
+ describe('Data access functions', () => {
123
+ const storage = createBlockStorage({ count: 42 }, 3);
124
+
125
+ it('getStorageData should return the data', () => {
126
+ expect(getStorageData(storage)).toEqual({ count: 42 });
127
+ });
128
+
129
+ it('getStorageDataVersion should return the version', () => {
130
+ expect(getStorageDataVersion(storage)).toBe(3);
131
+ });
132
+
133
+ it('updateStorageData should return new storage with updated data', () => {
134
+ const newStorage = updateStorageData(storage, { operation: 'update-data', value: { count: 100 } });
135
+ expect(newStorage.__data).toEqual({ count: 100 });
136
+ expect(newStorage.__dataVersion).toBe(3);
137
+ expect(newStorage[BLOCK_STORAGE_KEY]).toBe(BLOCK_STORAGE_SCHEMA_VERSION);
138
+ // Original should be unchanged
139
+ expect(storage.__data).toEqual({ count: 42 });
140
+ });
141
+
142
+ it('updateStorageDataVersion should return new storage with updated version', () => {
143
+ const newStorage = updateStorageDataVersion(storage, 5);
144
+ expect(newStorage.__dataVersion).toBe(5);
145
+ expect(newStorage.__data).toEqual({ count: 42 });
146
+ expect(newStorage[BLOCK_STORAGE_KEY]).toBe(BLOCK_STORAGE_SCHEMA_VERSION);
147
+ // Original should be unchanged
148
+ expect(storage.__dataVersion).toBe(3);
149
+ });
150
+ });
151
+
152
+ describe('Plugin data functions', () => {
153
+ const baseStorage = createBlockStorage({});
154
+
155
+ it('setPluginData should add plugin data', () => {
156
+ const storage = setPluginData(baseStorage, 'table', { columns: ['a', 'b'] });
157
+ expect(storage['@plugin/table']).toEqual({ columns: ['a', 'b'] });
158
+ });
159
+
160
+ it('getPluginData should retrieve plugin data', () => {
161
+ const storage = setPluginData(baseStorage, 'chart', { type: 'bar' });
162
+ expect(getPluginData(storage, 'chart')).toEqual({ type: 'bar' });
163
+ });
164
+
165
+ it('getPluginData should return undefined for missing plugin', () => {
166
+ expect(getPluginData(baseStorage, 'nonexistent')).toBeUndefined();
167
+ });
168
+
169
+ it('removePluginData should remove plugin data', () => {
170
+ let storage = setPluginData(baseStorage, 'toRemove', { data: 'test' });
171
+ storage = setPluginData(storage, 'toKeep', { other: 'data' });
172
+ const result = removePluginData(storage, 'toRemove');
173
+ expect(result['@plugin/toRemove']).toBeUndefined();
174
+ expect(result['@plugin/toKeep']).toEqual({ other: 'data' });
175
+ });
176
+
177
+ it('getPluginNames should return all plugin names', () => {
178
+ let storage = createBlockStorage({});
179
+ storage = setPluginData(storage, 'alpha', {});
180
+ storage = setPluginData(storage, 'beta', {});
181
+ storage = setPluginData(storage, 'gamma', {});
182
+ const names = getPluginNames(storage);
183
+ expect(names.sort()).toEqual(['alpha', 'beta', 'gamma']);
184
+ });
185
+
186
+ it('getPluginNames should return empty array when no plugins', () => {
187
+ expect(getPluginNames(baseStorage)).toEqual([]);
188
+ });
189
+ });
190
+
191
+ describe('Generic storage access', () => {
192
+ const storage = createBlockStorage('hello', 2);
193
+
194
+ it('getFromStorage should get __data', () => {
195
+ expect(getFromStorage(storage, '__data')).toBe('hello');
196
+ });
197
+
198
+ it('getFromStorage should get __dataVersion', () => {
199
+ expect(getFromStorage(storage, '__dataVersion')).toBe(2);
200
+ });
201
+
202
+ it('updateStorage should update any key', () => {
203
+ const updated = updateStorage(storage, '__data', 'world');
204
+ expect(updated.__data).toBe('world');
205
+ expect(storage.__data).toBe('hello'); // immutable
206
+ });
207
+ });
208
+
209
+ describe('BlockStorageHandlers', () => {
210
+ describe('defaultBlockStorageHandlers', () => {
211
+ it('transformStateForStorage should replace data', () => {
212
+ const storage = createBlockStorage('old');
213
+ const result = defaultBlockStorageHandlers.transformStateForStorage(storage, 'new');
214
+ expect(result.__data).toBe('new');
215
+ expect(result.__dataVersion).toBe(1);
216
+ });
217
+
218
+ it('deriveStateForArgs should return data directly', () => {
219
+ const storage = createBlockStorage({ data: 'test' });
220
+ expect(defaultBlockStorageHandlers.deriveStateForArgs(storage)).toEqual({ data: 'test' });
221
+ });
222
+
223
+ it('migrateStorage should update version only', () => {
224
+ const storage = createBlockStorage({ data: 'test' });
225
+ const result = defaultBlockStorageHandlers.migrateStorage(storage, 1, 3);
226
+ expect(result.__dataVersion).toBe(3);
227
+ expect(result.__data).toEqual({ data: 'test' });
228
+ });
229
+ });
230
+
231
+ describe('mergeBlockStorageHandlers', () => {
232
+ it('should return defaults when no custom handlers provided', () => {
233
+ const handlers = mergeBlockStorageHandlers();
234
+ expect(handlers.transformStateForStorage).toBe(
235
+ defaultBlockStorageHandlers.transformStateForStorage,
236
+ );
237
+ expect(handlers.deriveStateForArgs).toBe(defaultBlockStorageHandlers.deriveStateForArgs);
238
+ expect(handlers.migrateStorage).toBe(defaultBlockStorageHandlers.migrateStorage);
239
+ });
240
+
241
+ it('should override with custom handlers', () => {
242
+ const customTransform = <T>(storage: ReturnType<typeof createBlockStorage<T>>, data: T) => ({
243
+ ...storage,
244
+ __data: data,
245
+ __dataVersion: storage.__dataVersion + 1,
246
+ });
247
+
248
+ const handlers = mergeBlockStorageHandlers({
249
+ transformStateForStorage: customTransform,
250
+ });
251
+
252
+ expect(handlers.transformStateForStorage).toBe(customTransform);
253
+ expect(handlers.deriveStateForArgs).toBe(defaultBlockStorageHandlers.deriveStateForArgs);
254
+ });
255
+ });
256
+ });
257
+ });
258
+
@@ -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
+ }