@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.
Files changed (120) 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/index.cjs +13 -14
  47. package/dist/index.cjs.map +1 -1
  48. package/dist/index.d.ts +8 -3
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +6 -4
  51. package/dist/index.js.map +1 -1
  52. package/dist/package.json.cjs +1 -1
  53. package/dist/package.json.js +1 -1
  54. package/dist/platforma.d.ts +11 -4
  55. package/dist/platforma.d.ts.map +1 -1
  56. package/dist/plugin_model.cjs +171 -0
  57. package/dist/plugin_model.cjs.map +1 -0
  58. package/dist/plugin_model.d.ts +162 -0
  59. package/dist/plugin_model.d.ts.map +1 -0
  60. package/dist/plugin_model.js +169 -0
  61. package/dist/plugin_model.js.map +1 -0
  62. package/dist/render/api.cjs +20 -21
  63. package/dist/render/api.cjs.map +1 -1
  64. package/dist/render/api.d.ts +8 -8
  65. package/dist/render/api.d.ts.map +1 -1
  66. package/dist/render/api.js +20 -21
  67. package/dist/render/api.js.map +1 -1
  68. package/dist/render/internal.cjs.map +1 -1
  69. package/dist/render/internal.d.ts +2 -1
  70. package/dist/render/internal.d.ts.map +1 -1
  71. package/dist/render/internal.js.map +1 -1
  72. package/dist/version.cjs +4 -0
  73. package/dist/version.cjs.map +1 -1
  74. package/dist/version.d.ts +4 -0
  75. package/dist/version.d.ts.map +1 -1
  76. package/dist/version.js +4 -1
  77. package/dist/version.js.map +1 -1
  78. package/package.json +5 -5
  79. package/src/bconfig/normalization.ts +8 -1
  80. package/src/block_api_v3.ts +2 -2
  81. package/src/block_migrations.test.ts +141 -171
  82. package/src/block_migrations.ts +300 -285
  83. package/src/block_model.ts +205 -95
  84. package/src/{builder.ts → block_model_legacy.ts} +1 -1
  85. package/src/block_state_patch.ts +13 -1
  86. package/src/block_storage.test.ts +283 -95
  87. package/src/block_storage.ts +199 -188
  88. package/src/block_storage_callbacks.ts +326 -0
  89. package/src/block_storage_facade.ts +199 -0
  90. package/src/index.ts +7 -3
  91. package/src/platforma.ts +26 -7
  92. package/src/plugin_model.test.ts +168 -0
  93. package/src/plugin_model.ts +242 -0
  94. package/src/render/api.ts +26 -24
  95. package/src/render/internal.ts +3 -1
  96. package/src/typing.test.ts +1 -1
  97. package/src/version.ts +8 -0
  98. package/dist/block_storage_vm.cjs +0 -262
  99. package/dist/block_storage_vm.cjs.map +0 -1
  100. package/dist/block_storage_vm.d.ts +0 -59
  101. package/dist/block_storage_vm.d.ts.map +0 -1
  102. package/dist/block_storage_vm.js +0 -258
  103. package/dist/block_storage_vm.js.map +0 -1
  104. package/dist/branding.d.ts +0 -7
  105. package/dist/branding.d.ts.map +0 -1
  106. package/dist/builder.cjs.map +0 -1
  107. package/dist/builder.d.ts.map +0 -1
  108. package/dist/builder.js.map +0 -1
  109. package/dist/sdk_info.cjs +0 -10
  110. package/dist/sdk_info.cjs.map +0 -1
  111. package/dist/sdk_info.d.ts +0 -5
  112. package/dist/sdk_info.d.ts.map +0 -1
  113. package/dist/sdk_info.js +0 -8
  114. package/dist/sdk_info.js.map +0 -1
  115. package/dist/unionize.d.ts +0 -12
  116. package/dist/unionize.d.ts.map +0 -1
  117. package/src/block_storage_vm.ts +0 -346
  118. package/src/branding.ts +0 -4
  119. package/src/sdk_info.ts +0 -9
  120. package/src/unionize.ts +0 -12
@@ -4,20 +4,15 @@ import {
4
4
  BLOCK_STORAGE_SCHEMA_VERSION,
5
5
  DATA_MODEL_DEFAULT_VERSION,
6
6
  createBlockStorage,
7
- defaultBlockStorageHandlers,
8
- getFromStorage,
9
7
  getPluginData,
10
- getPluginNames,
11
8
  getStorageData,
12
- getStorageDataVersion,
13
9
  isBlockStorage,
14
- mergeBlockStorageHandlers,
10
+ migrateBlockStorage,
15
11
  normalizeBlockStorage,
16
- removePluginData,
17
- setPluginData,
18
12
  updateStorageData,
19
- updateStorageDataVersion,
20
- updateStorage,
13
+ type MutateStoragePayload,
14
+ type PluginName,
15
+ type PluginRegistry,
21
16
  } from "./block_storage";
22
17
 
23
18
  describe("BlockStorage", () => {
@@ -39,7 +34,11 @@ describe("BlockStorage", () => {
39
34
  });
40
35
 
41
36
  it("should return true for BlockStorage with plugin data", () => {
42
- const storage = setPluginData(createBlockStorage({ foo: "bar" }), "test", { data: 123 });
37
+ const storage = updateStorageData(createBlockStorage({ foo: "bar" }), {
38
+ operation: "update-plugin-data",
39
+ pluginId: "testPlugin",
40
+ value: { data: 123 },
41
+ });
43
42
  expect(isBlockStorage(storage)).toBe(true);
44
43
  });
45
44
 
@@ -90,13 +89,46 @@ describe("BlockStorage", () => {
90
89
  expect(storage.__dataVersion).toBe("v5");
91
90
  expect(storage.__data).toEqual({ foo: "bar" });
92
91
  });
92
+
93
+ it("should create storage with empty plugin fields by default", () => {
94
+ const storage = createBlockStorage({});
95
+ expect(storage.__pluginRegistry).toEqual({});
96
+ expect(storage.__plugins).toEqual({});
97
+ });
93
98
  });
94
99
 
95
100
  describe("normalizeBlockStorage", () => {
96
- it("should return BlockStorage as-is", () => {
101
+ it("should return BlockStorage as-is with plugin defaults added", () => {
97
102
  const storage = createBlockStorage({ data: "test" }, "v2");
98
103
  const normalized = normalizeBlockStorage(storage);
99
104
  expect(normalized).toEqual(storage);
105
+ expect(normalized.__pluginRegistry).toEqual({});
106
+ expect(normalized.__plugins).toEqual({});
107
+ });
108
+
109
+ it("should add plugin defaults to BlockStorage without them", () => {
110
+ // Simulate storage from earlier version without plugin fields
111
+ const oldStorage = {
112
+ [BLOCK_STORAGE_KEY]: "v1" as const,
113
+ __dataVersion: "v2",
114
+ __data: { data: "test" },
115
+ };
116
+ const normalized = normalizeBlockStorage(oldStorage);
117
+ expect(normalized.__pluginRegistry).toEqual({});
118
+ expect(normalized.__plugins).toEqual({});
119
+ });
120
+
121
+ it("should preserve existing plugin data when normalizing", () => {
122
+ const storageWithPlugins = {
123
+ [BLOCK_STORAGE_KEY]: "v1" as const,
124
+ __dataVersion: "v2",
125
+ __data: { data: "test" },
126
+ __pluginRegistry: { p1: "plugin1" as PluginName },
127
+ __plugins: { p1: { __dataVersion: "v1", __data: { foo: "bar" } } },
128
+ };
129
+ const normalized = normalizeBlockStorage(storageWithPlugins);
130
+ expect(normalized.__pluginRegistry).toEqual({ p1: "plugin1" as PluginName });
131
+ expect(normalized.__plugins).toEqual({ p1: { __dataVersion: "v1", __data: { foo: "bar" } } });
100
132
  });
101
133
 
102
134
  it("should wrap legacy data in BlockStorage structure", () => {
@@ -105,6 +137,8 @@ describe("BlockStorage", () => {
105
137
  expect(normalized[BLOCK_STORAGE_KEY]).toBe(BLOCK_STORAGE_SCHEMA_VERSION);
106
138
  expect(normalized.__dataVersion).toBe(DATA_MODEL_DEFAULT_VERSION);
107
139
  expect(normalized.__data).toEqual(legacyData);
140
+ expect(normalized.__pluginRegistry).toEqual({});
141
+ expect(normalized.__plugins).toEqual({});
108
142
  });
109
143
 
110
144
  it("should wrap primitive legacy data", () => {
@@ -129,13 +163,9 @@ describe("BlockStorage", () => {
129
163
  expect(getStorageData(storage)).toEqual({ count: 42 });
130
164
  });
131
165
 
132
- it("getStorageDataVersion should return the version", () => {
133
- expect(getStorageDataVersion(storage)).toBe("v3");
134
- });
135
-
136
166
  it("updateStorageData should return new storage with updated data", () => {
137
167
  const newStorage = updateStorageData(storage, {
138
- operation: "update-data",
168
+ operation: "update-block-data",
139
169
  value: { count: 100 },
140
170
  });
141
171
  expect(newStorage.__data).toEqual({ count: 100 });
@@ -144,123 +174,281 @@ describe("BlockStorage", () => {
144
174
  // Original should be unchanged
145
175
  expect(storage.__data).toEqual({ count: 42 });
146
176
  });
147
-
148
- it("updateStorageDataVersion should return new storage with updated version", () => {
149
- const newStorage = updateStorageDataVersion(storage, "v5");
150
- expect(newStorage.__dataVersion).toBe("v5");
151
- expect(newStorage.__data).toEqual({ count: 42 });
152
- expect(newStorage[BLOCK_STORAGE_KEY]).toBe(BLOCK_STORAGE_SCHEMA_VERSION);
153
- // Original should be unchanged
154
- expect(storage.__dataVersion).toBe("v3");
155
- });
156
177
  });
157
178
 
158
- describe("Plugin data functions", () => {
179
+ describe("Plugin data functions (UI)", () => {
159
180
  const baseStorage = createBlockStorage({});
160
181
 
161
- it("setPluginData should add plugin data", () => {
162
- const storage = setPluginData(baseStorage, "table", { columns: ["a", "b"] });
163
- expect(storage["@plugin/table"]).toEqual({ columns: ["a", "b"] });
182
+ it("update-plugin operation should add plugin data with default version", () => {
183
+ const storage = updateStorageData(baseStorage, {
184
+ operation: "update-plugin-data",
185
+ pluginId: "table1",
186
+ value: { columns: ["a", "b"] },
187
+ });
188
+ expect(storage.__plugins).toEqual({
189
+ table1: {
190
+ __dataVersion: DATA_MODEL_DEFAULT_VERSION,
191
+ __data: { columns: ["a", "b"] },
192
+ },
193
+ });
194
+ });
195
+
196
+ it("update-plugin operation should preserve existing version when updating", () => {
197
+ let storage = {
198
+ ...baseStorage,
199
+ __plugins: { table1: { __dataVersion: "v5", __data: { old: true } } },
200
+ };
201
+ storage = updateStorageData(storage, {
202
+ operation: "update-plugin-data",
203
+ pluginId: "table1",
204
+ value: { new: true },
205
+ });
206
+ expect(storage.__plugins?.table1).toEqual({
207
+ __dataVersion: "v5",
208
+ __data: { new: true },
209
+ });
164
210
  });
165
211
 
166
212
  it("getPluginData should retrieve plugin data", () => {
167
- const storage = setPluginData(baseStorage, "chart", { type: "bar" });
168
- expect(getPluginData(storage, "chart")).toEqual({ type: "bar" });
213
+ const storage = updateStorageData(baseStorage, {
214
+ operation: "update-plugin-data",
215
+ pluginId: "chart1",
216
+ value: { type: "bar" },
217
+ });
218
+ expect(getPluginData(storage, "chart1")).toEqual({ type: "bar" });
169
219
  });
170
220
 
171
221
  it("getPluginData should return undefined for missing plugin", () => {
172
222
  expect(getPluginData(baseStorage, "nonexistent")).toBeUndefined();
173
223
  });
174
224
 
175
- it("removePluginData should remove plugin data", () => {
176
- let storage = setPluginData(baseStorage, "toRemove", { data: "test" });
177
- storage = setPluginData(storage, "toKeep", { other: "data" });
178
- const result = removePluginData(storage, "toRemove");
179
- expect(result["@plugin/toRemove"]).toBeUndefined();
180
- expect(result["@plugin/toKeep"]).toEqual({ other: "data" });
225
+ it("should not modify original storage (immutability)", () => {
226
+ const storage = updateStorageData(baseStorage, {
227
+ operation: "update-plugin-data",
228
+ pluginId: "table1",
229
+ value: { data: "test" },
230
+ });
231
+ expect(baseStorage.__plugins).toEqual({});
232
+ expect(storage.__plugins?.table1).toBeDefined();
181
233
  });
234
+ });
182
235
 
183
- it("getPluginNames should return all plugin names", () => {
184
- let storage = createBlockStorage({});
185
- storage = setPluginData(storage, "alpha", {});
186
- storage = setPluginData(storage, "beta", {});
187
- storage = setPluginData(storage, "gamma", {});
188
- const names = getPluginNames(storage);
189
- expect(names.sort()).toEqual(["alpha", "beta", "gamma"]);
236
+ describe("updateStorageData operations", () => {
237
+ it("update-data operation should update block data", () => {
238
+ const storage = createBlockStorage({ count: 1 });
239
+ const updated = updateStorageData(storage, {
240
+ operation: "update-block-data",
241
+ value: { count: 2 },
242
+ });
243
+ expect(updated.__data).toEqual({ count: 2 });
190
244
  });
191
245
 
192
- it("getPluginNames should return empty array when no plugins", () => {
193
- expect(getPluginNames(baseStorage)).toEqual([]);
246
+ it("update-plugin operation should update plugin data", () => {
247
+ const storage = createBlockStorage({});
248
+ const updated = updateStorageData(storage, {
249
+ operation: "update-plugin-data",
250
+ pluginId: "plugin1",
251
+ value: { foo: "bar" },
252
+ });
253
+ expect(updated.__plugins?.plugin1).toEqual({
254
+ __dataVersion: DATA_MODEL_DEFAULT_VERSION,
255
+ __data: { foo: "bar" },
256
+ });
257
+ });
258
+
259
+ it("should throw on unknown operation", () => {
260
+ const storage = createBlockStorage({});
261
+ expect(() => {
262
+ updateStorageData(storage, { operation: "invalid" } as MutateStoragePayload);
263
+ }).toThrow("Unknown storage operation: invalid");
194
264
  });
195
265
  });
196
266
 
197
- describe("Generic storage access", () => {
198
- const storage = createBlockStorage("hello", "v2");
267
+ describe("Atomic migration (migrateBlockStorage)", () => {
268
+ const createTestStorage = () => {
269
+ const storage = createBlockStorage({ count: 1 }, "v1");
270
+ return {
271
+ ...storage,
272
+ __pluginRegistry: { plugin1: "typeA" as PluginName },
273
+ __plugins: { plugin1: { __dataVersion: "v1", __data: { value: "old" } } },
274
+ };
275
+ };
276
+
277
+ it("should migrate block data and plugins atomically on success", () => {
278
+ const storage = createTestStorage();
279
+ const newRegistry: PluginRegistry = { plugin1: "typeA" as PluginName };
280
+
281
+ const result = migrateBlockStorage(storage, {
282
+ migrateBlockData: (versioned) => {
283
+ const d = versioned.data as { count: number };
284
+ return { data: { count: d.count + 1 }, version: "v2" };
285
+ },
286
+ migratePluginData: (_pluginId, _versioned) => ({
287
+ version: "v2",
288
+ data: { value: "migrated" },
289
+ }),
290
+ newPluginRegistry: newRegistry,
291
+ createPluginData: (_pluginId) => ({ version: "v1", data: {} }),
292
+ });
199
293
 
200
- it("getFromStorage should get __data", () => {
201
- expect(getFromStorage(storage, "__data")).toBe("hello");
294
+ expect(result.success).toBe(true);
295
+ if (result.success) {
296
+ expect(result.storage.__data).toEqual({ count: 2 });
297
+ expect(result.storage.__dataVersion).toBe("v2");
298
+ expect(result.storage.__plugins?.plugin1).toEqual({
299
+ __dataVersion: "v2",
300
+ __data: { value: "migrated" },
301
+ });
302
+ }
202
303
  });
203
304
 
204
- it("getFromStorage should get __dataVersion", () => {
205
- expect(getFromStorage(storage, "__dataVersion")).toBe("v2");
206
- });
305
+ it("should return failure and preserve original storage when block migration throws", () => {
306
+ const storage = createTestStorage();
307
+ const originalData = storage.__data;
308
+
309
+ const result = migrateBlockStorage(storage, {
310
+ migrateBlockData: () => {
311
+ throw new Error("Block migration failed");
312
+ },
313
+ migratePluginData: (_pluginId, _versioned) => ({
314
+ version: "v1",
315
+ data: {},
316
+ }),
317
+ newPluginRegistry: {},
318
+ createPluginData: (_pluginId) => ({ version: "v1", data: {} }),
319
+ });
207
320
 
208
- it("updateStorage should update any key", () => {
209
- const updated = updateStorage(storage, "__data", "world");
210
- expect(updated.__data).toBe("world");
211
- expect(storage.__data).toBe("hello"); // immutable
321
+ expect(result.success).toBe(false);
322
+ if (!result.success) {
323
+ expect(result.error).toBe("Block migration failed");
324
+ expect(result.failedAt).toBe("block");
325
+ }
326
+ // Original storage untouched
327
+ expect(storage.__data).toEqual(originalData);
212
328
  });
213
- });
214
329
 
215
- describe("BlockStorageHandlers", () => {
216
- describe("defaultBlockStorageHandlers", () => {
217
- it("transformStateForStorage should replace data", () => {
218
- const storage = createBlockStorage("old");
219
- const result = defaultBlockStorageHandlers.transformStateForStorage(storage, "new");
220
- expect(result.__data).toBe("new");
221
- expect(result.__dataVersion).toBe(DATA_MODEL_DEFAULT_VERSION);
330
+ it("should return failure and preserve original storage when plugin migration throws", () => {
331
+ const storage = createTestStorage();
332
+ const newRegistry: PluginRegistry = { plugin1: "typeA" as PluginName };
333
+
334
+ const result = migrateBlockStorage(storage, {
335
+ migrateBlockData: (versioned) => ({
336
+ data: versioned.data as { count: number },
337
+ version: "v2",
338
+ }),
339
+ migratePluginData: (pluginId) => {
340
+ throw new Error(`Plugin ${pluginId} migration failed`);
341
+ },
342
+ newPluginRegistry: newRegistry,
343
+ createPluginData: (_pluginId) => ({ version: "v1", data: {} }),
222
344
  });
223
345
 
224
- it("deriveStateForArgs should return data directly", () => {
225
- const storage = createBlockStorage({ data: "test" });
226
- expect(defaultBlockStorageHandlers.deriveStateForArgs(storage)).toEqual({ data: "test" });
227
- });
346
+ expect(result.success).toBe(false);
347
+ if (!result.success) {
348
+ expect(result.error).toBe("Plugin plugin1 migration failed");
349
+ expect(result.failedAt).toBe("plugin1");
350
+ }
351
+ });
228
352
 
229
- it("migrateStorage should update version only", () => {
230
- const storage = createBlockStorage({ data: "test" });
231
- const result = defaultBlockStorageHandlers.migrateStorage(storage, "v1", "v3");
232
- expect(result.__dataVersion).toBe("v3");
233
- expect(result.__data).toEqual({ data: "test" });
353
+ it("should reset plugin data when plugin type changes", () => {
354
+ const storage = createTestStorage();
355
+ const newRegistry: PluginRegistry = { plugin1: "typeB" as PluginName }; // Different type
356
+
357
+ const result = migrateBlockStorage(storage, {
358
+ migrateBlockData: (versioned) => ({
359
+ data: versioned.data as { count: number },
360
+ version: "v2",
361
+ }),
362
+ migratePluginData: () => {
363
+ throw new Error("Should not be called for type change");
364
+ },
365
+ newPluginRegistry: newRegistry,
366
+ createPluginData: (pluginId) => ({
367
+ version: "v1",
368
+ data: { fresh: true, type: newRegistry[pluginId] },
369
+ }),
234
370
  });
371
+
372
+ expect(result.success).toBe(true);
373
+ if (result.success) {
374
+ expect(result.storage.__plugins?.plugin1).toEqual({
375
+ __dataVersion: "v1",
376
+ __data: { fresh: true, type: "typeB" },
377
+ });
378
+ }
235
379
  });
236
380
 
237
- describe("mergeBlockStorageHandlers", () => {
238
- it("should return defaults when no custom handlers provided", () => {
239
- const handlers = mergeBlockStorageHandlers();
240
- expect(handlers.transformStateForStorage).toBe(
241
- defaultBlockStorageHandlers.transformStateForStorage,
242
- );
243
- expect(handlers.deriveStateForArgs).toBe(defaultBlockStorageHandlers.deriveStateForArgs);
244
- expect(handlers.migrateStorage).toBe(defaultBlockStorageHandlers.migrateStorage);
381
+ it("should create initial data for new plugins", () => {
382
+ const storage = createBlockStorage({ count: 1 }, "v1");
383
+ const newRegistry: PluginRegistry = { newPlugin: "typeNew" as PluginName };
384
+
385
+ const result = migrateBlockStorage(storage, {
386
+ migrateBlockData: (versioned) => ({
387
+ data: versioned.data as { count: number },
388
+ version: "v2",
389
+ }),
390
+ migratePluginData: () => {
391
+ throw new Error("Should not be called for new plugin");
392
+ },
393
+ newPluginRegistry: newRegistry,
394
+ createPluginData: (_pluginId) => ({
395
+ version: "v1",
396
+ data: { initialized: true },
397
+ }),
245
398
  });
246
399
 
247
- it("should override with custom handlers", () => {
248
- const customTransform = <T>(
249
- storage: ReturnType<typeof createBlockStorage<T>>,
250
- data: T,
251
- ) => ({
252
- ...storage,
253
- __data: data,
254
- __dataVersion: `${storage.__dataVersion}-next`,
400
+ expect(result.success).toBe(true);
401
+ if (result.success) {
402
+ expect(result.storage.__plugins?.newPlugin).toEqual({
403
+ __dataVersion: "v1",
404
+ __data: { initialized: true },
255
405
  });
406
+ }
407
+ });
256
408
 
257
- const handlers = mergeBlockStorageHandlers({
258
- transformStateForStorage: customTransform,
259
- });
409
+ it("should drop plugins not in new registry", () => {
410
+ const storage = createTestStorage();
411
+ const newRegistry: PluginRegistry = {}; // Empty - removes plugin1
412
+
413
+ const result = migrateBlockStorage(storage, {
414
+ migrateBlockData: (versioned) => ({
415
+ data: versioned.data as { count: number },
416
+ version: "v2",
417
+ }),
418
+ migratePluginData: () => {
419
+ throw new Error("Should not be called for dropped plugin");
420
+ },
421
+ newPluginRegistry: newRegistry,
422
+ createPluginData: () => {
423
+ throw new Error("Should not be called for dropped plugin");
424
+ },
425
+ });
260
426
 
261
- expect(handlers.transformStateForStorage).toBe(customTransform);
262
- expect(handlers.deriveStateForArgs).toBe(defaultBlockStorageHandlers.deriveStateForArgs);
427
+ expect(result.success).toBe(true);
428
+ if (result.success) {
429
+ expect(result.storage.__plugins).toEqual({});
430
+ expect(result.storage.__pluginRegistry).toEqual({});
431
+ }
432
+ });
433
+
434
+ it("should allow plugin migration to return undefined to remove plugin", () => {
435
+ const storage = createTestStorage();
436
+ const newRegistry: PluginRegistry = { plugin1: "typeA" as PluginName };
437
+
438
+ const result = migrateBlockStorage(storage, {
439
+ migrateBlockData: (versioned) => ({
440
+ data: versioned.data as { count: number },
441
+ version: "v2",
442
+ }),
443
+ migratePluginData: () => undefined, // Remove plugin
444
+ newPluginRegistry: newRegistry,
445
+ createPluginData: () => ({ version: "v1", data: {} }),
263
446
  });
447
+
448
+ expect(result.success).toBe(true);
449
+ if (result.success) {
450
+ expect(result.storage.__plugins).toEqual({});
451
+ }
264
452
  });
265
453
  });
266
454
  });