@milaboratories/pl-middle-layer 1.45.5 → 1.46.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 (132) hide show
  1. package/dist/index.cjs +58 -0
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.js +2 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/js_render/computable_context.cjs +37 -7
  6. package/dist/js_render/computable_context.cjs.map +1 -1
  7. package/dist/js_render/computable_context.d.ts.map +1 -1
  8. package/dist/js_render/computable_context.js +37 -7
  9. package/dist/js_render/computable_context.js.map +1 -1
  10. package/dist/js_render/context.cjs +12 -4
  11. package/dist/js_render/context.cjs.map +1 -1
  12. package/dist/js_render/context.d.ts +9 -0
  13. package/dist/js_render/context.d.ts.map +1 -1
  14. package/dist/js_render/context.js +12 -4
  15. package/dist/js_render/context.js.map +1 -1
  16. package/dist/js_render/index.cjs +1 -1
  17. package/dist/js_render/index.cjs.map +1 -1
  18. package/dist/js_render/index.js +1 -1
  19. package/dist/js_render/index.js.map +1 -1
  20. package/dist/middle_layer/block.cjs +7 -8
  21. package/dist/middle_layer/block.cjs.map +1 -1
  22. package/dist/middle_layer/block.d.ts +4 -4
  23. package/dist/middle_layer/block.d.ts.map +1 -1
  24. package/dist/middle_layer/block.js +7 -8
  25. package/dist/middle_layer/block.js.map +1 -1
  26. package/dist/middle_layer/block_ctx.cjs +67 -13
  27. package/dist/middle_layer/block_ctx.cjs.map +1 -1
  28. package/dist/middle_layer/block_ctx.d.ts +4 -7
  29. package/dist/middle_layer/block_ctx.d.ts.map +1 -1
  30. package/dist/middle_layer/block_ctx.js +68 -14
  31. package/dist/middle_layer/block_ctx.js.map +1 -1
  32. package/dist/middle_layer/block_ctx_unsafe.cjs +10 -3
  33. package/dist/middle_layer/block_ctx_unsafe.cjs.map +1 -1
  34. package/dist/middle_layer/block_ctx_unsafe.d.ts +1 -1
  35. package/dist/middle_layer/block_ctx_unsafe.d.ts.map +1 -1
  36. package/dist/middle_layer/block_ctx_unsafe.js +10 -3
  37. package/dist/middle_layer/block_ctx_unsafe.js.map +1 -1
  38. package/dist/middle_layer/frontend_path.cjs +1 -0
  39. package/dist/middle_layer/frontend_path.cjs.map +1 -1
  40. package/dist/middle_layer/frontend_path.js +1 -0
  41. package/dist/middle_layer/frontend_path.js.map +1 -1
  42. package/dist/middle_layer/middle_layer.cjs +1 -0
  43. package/dist/middle_layer/middle_layer.cjs.map +1 -1
  44. package/dist/middle_layer/middle_layer.d.ts +1 -1
  45. package/dist/middle_layer/middle_layer.d.ts.map +1 -1
  46. package/dist/middle_layer/middle_layer.js +1 -0
  47. package/dist/middle_layer/middle_layer.js.map +1 -1
  48. package/dist/middle_layer/project.cjs +75 -28
  49. package/dist/middle_layer/project.cjs.map +1 -1
  50. package/dist/middle_layer/project.d.ts +34 -7
  51. package/dist/middle_layer/project.d.ts.map +1 -1
  52. package/dist/middle_layer/project.js +76 -29
  53. package/dist/middle_layer/project.js.map +1 -1
  54. package/dist/middle_layer/project_overview.cjs +32 -11
  55. package/dist/middle_layer/project_overview.cjs.map +1 -1
  56. package/dist/middle_layer/project_overview.d.ts.map +1 -1
  57. package/dist/middle_layer/project_overview.js +32 -11
  58. package/dist/middle_layer/project_overview.js.map +1 -1
  59. package/dist/middle_layer/render.cjs +1 -1
  60. package/dist/middle_layer/render.cjs.map +1 -1
  61. package/dist/middle_layer/render.js +1 -1
  62. package/dist/middle_layer/render.js.map +1 -1
  63. package/dist/middle_layer/render.test.d.ts.map +1 -1
  64. package/dist/model/block_storage_helper.cjs +210 -0
  65. package/dist/model/block_storage_helper.cjs.map +1 -0
  66. package/dist/model/block_storage_helper.d.ts +98 -0
  67. package/dist/model/block_storage_helper.d.ts.map +1 -0
  68. package/dist/model/block_storage_helper.js +153 -0
  69. package/dist/model/block_storage_helper.js.map +1 -0
  70. package/dist/model/index.d.ts +2 -1
  71. package/dist/model/index.d.ts.map +1 -1
  72. package/dist/model/project_helper.cjs +177 -0
  73. package/dist/model/project_helper.cjs.map +1 -1
  74. package/dist/model/project_helper.d.ts +110 -1
  75. package/dist/model/project_helper.d.ts.map +1 -1
  76. package/dist/model/project_helper.js +178 -1
  77. package/dist/model/project_helper.js.map +1 -1
  78. package/dist/model/project_model.cjs +6 -3
  79. package/dist/model/project_model.cjs.map +1 -1
  80. package/dist/model/project_model.d.ts +3 -2
  81. package/dist/model/project_model.d.ts.map +1 -1
  82. package/dist/model/project_model.js +6 -4
  83. package/dist/model/project_model.js.map +1 -1
  84. package/dist/mutator/block-pack/block_pack.cjs +1 -2
  85. package/dist/mutator/block-pack/block_pack.cjs.map +1 -1
  86. package/dist/mutator/block-pack/block_pack.d.ts.map +1 -1
  87. package/dist/mutator/block-pack/block_pack.js +1 -2
  88. package/dist/mutator/block-pack/block_pack.js.map +1 -1
  89. package/dist/mutator/block-pack/frontend.cjs +1 -0
  90. package/dist/mutator/block-pack/frontend.cjs.map +1 -1
  91. package/dist/mutator/block-pack/frontend.js +1 -0
  92. package/dist/mutator/block-pack/frontend.js.map +1 -1
  93. package/dist/mutator/migration.cjs +64 -3
  94. package/dist/mutator/migration.cjs.map +1 -1
  95. package/dist/mutator/migration.d.ts.map +1 -1
  96. package/dist/mutator/migration.js +66 -5
  97. package/dist/mutator/migration.js.map +1 -1
  98. package/dist/mutator/project-v3.test.d.ts +2 -0
  99. package/dist/mutator/project-v3.test.d.ts.map +1 -0
  100. package/dist/mutator/project.cjs +282 -41
  101. package/dist/mutator/project.cjs.map +1 -1
  102. package/dist/mutator/project.d.ts +77 -12
  103. package/dist/mutator/project.d.ts.map +1 -1
  104. package/dist/mutator/project.js +283 -42
  105. package/dist/mutator/project.js.map +1 -1
  106. package/dist/pool/result_pool.cjs +9 -6
  107. package/dist/pool/result_pool.cjs.map +1 -1
  108. package/dist/pool/result_pool.d.ts.map +1 -1
  109. package/dist/pool/result_pool.js +9 -6
  110. package/dist/pool/result_pool.js.map +1 -1
  111. package/package.json +17 -17
  112. package/src/js_render/computable_context.ts +37 -7
  113. package/src/js_render/context.ts +12 -5
  114. package/src/js_render/index.ts +1 -1
  115. package/src/middle_layer/block.ts +13 -14
  116. package/src/middle_layer/block_ctx.ts +70 -23
  117. package/src/middle_layer/block_ctx_unsafe.ts +11 -4
  118. package/src/middle_layer/middle_layer.ts +2 -1
  119. package/src/middle_layer/project.ts +86 -40
  120. package/src/middle_layer/project_overview.ts +44 -20
  121. package/src/middle_layer/render.test.ts +1 -1
  122. package/src/middle_layer/render.ts +1 -1
  123. package/src/model/block_storage_helper.ts +213 -0
  124. package/src/model/index.ts +2 -1
  125. package/src/model/project_helper.ts +249 -1
  126. package/src/model/project_model.ts +9 -5
  127. package/src/mutator/block-pack/block_pack.ts +1 -2
  128. package/src/mutator/migration.ts +79 -6
  129. package/src/mutator/project-v3.test.ts +280 -0
  130. package/src/mutator/project.test.ts +27 -27
  131. package/src/mutator/project.ts +351 -68
  132. package/src/pool/result_pool.ts +11 -4
@@ -24,7 +24,7 @@ import { SynchronizedTreeState, treeDumpStats } from '@milaboratories/pl-tree';
24
24
  import { setTimeout } from 'node:timers/promises';
25
25
  import { frontendData } from './frontend_path';
26
26
  import type { NavigationState } from '@milaboratories/pl-model-common';
27
- import { blockArgsAndUiState, blockOutputs } from './block';
27
+ import { getBlockParameters, blockOutputs } from './block';
28
28
  import type { FrontendData } from '../model/frontend';
29
29
  import type { ProjectStructure } from '../model/project_model';
30
30
  import { projectFieldName } from '../model/project_model';
@@ -33,8 +33,8 @@ import type { BlockPackInfo } from '../model/block_pack';
33
33
  import type {
34
34
  ProjectOverview,
35
35
  AuthorMarker,
36
- BlockStateInternal,
37
36
  BlockSettings,
37
+ BlockStateInternalV3,
38
38
  } from '@milaboratories/pl-model-middle-layer';
39
39
  import { activeConfigs } from './active_cfg';
40
40
  import { NavigationStates } from './navigation_states';
@@ -46,7 +46,7 @@ import { projectOverviewLight } from './project_overview_light';
46
46
  import { applyProjectMigrations } from '../mutator/migration';
47
47
 
48
48
  type BlockStateComputables = {
49
- readonly fullState: Computable<BlockStateInternal>;
49
+ readonly fullState: Computable<BlockStateInternalV3>;
50
50
  };
51
51
 
52
52
  function stringifyForDump(object: unknown): string {
@@ -131,7 +131,7 @@ export class Project {
131
131
  await this.activeConfigs.getValue();
132
132
  await setTimeout(this.env.ops.projectRefreshInterval, this.abortController.signal);
133
133
 
134
- // Block computables houskeeping
134
+ // Block computables housekeeping
135
135
  const overviewLight = await this.overviewLight.getValue();
136
136
  const existingBlocks = new Set(overviewLight.listOfBlocks);
137
137
  // Doing cleanup for deleted blocks
@@ -187,6 +187,12 @@ export class Project {
187
187
  const preparedBp = await this.env.bpPreparer.prepare(blockPackSpec);
188
188
  const blockCfgContainer = await this.env.bpPreparer.getBlockConfigContainer(blockPackSpec);
189
189
  const blockCfg = extractConfig(blockCfgContainer); // full content of this var should never be persisted
190
+
191
+ // Build NewBlockSpec based on model API version
192
+ const newBlockSpec = blockCfg.modelAPIVersion === 2
193
+ ? { storageMode: 'fromModel' as const, blockPack: preparedBp }
194
+ : { storageMode: 'legacy' as const, blockPack: preparedBp, legacyState: canonicalize({ args: blockCfg.initialArgs, uiState: blockCfg.initialUiState })! };
195
+
190
196
  await withProjectAuthored(this.env.projectHelper, this.env.pl, this.rid, author, (mut) => {
191
197
  return mut.addBlock(
192
198
  {
@@ -194,11 +200,7 @@ export class Project {
194
200
  label: blockLabel,
195
201
  renderingMode: blockCfg.renderingMode,
196
202
  },
197
- {
198
- args: canonicalize(blockCfg.initialArgs)!,
199
- uiState: canonicalize(blockCfg.initialUiState)!,
200
- blockPack: preparedBp,
201
- },
203
+ newBlockSpec,
202
204
  before,
203
205
  );
204
206
  },
@@ -255,11 +257,17 @@ export class Project {
255
257
  ): Promise<void> {
256
258
  const preparedBp = await this.env.bpPreparer.prepare(blockPackSpec);
257
259
  const blockCfg = extractConfig(await this.env.bpPreparer.getBlockConfigContainer(blockPackSpec));
260
+ // resetState signals to mutator to reset storage
261
+ // For v2+ blocks: mutator gets initial storage directly via getInitialStorageInVM
262
+ // For v1 blocks: we pass the legacy state format
263
+ const resetState = resetArgs
264
+ ? { state: blockCfg.modelAPIVersion === 2 ? {} : { args: blockCfg.initialArgs, uiState: blockCfg.initialUiState } }
265
+ : undefined;
258
266
  await withProjectAuthored(this.env.projectHelper, this.env.pl, this.rid, author, (mut) =>
259
267
  mut.migrateBlockPack(
260
268
  blockId,
261
269
  preparedBp,
262
- resetArgs ? { args: blockCfg.initialArgs, uiState: blockCfg.initialUiState } : undefined,
270
+ resetState,
263
271
  ),
264
272
  { name: 'updateBlockPack', lockId: this.projectLockId },
265
273
  );
@@ -326,49 +334,38 @@ export class Project {
326
334
  await this.projectTree.refreshState();
327
335
  }
328
336
 
329
- // /** Update block label. */
330
- // public async setBlockLabel(blockId: string, label: string, author?: AuthorMarker) {
331
- // await withProjectAuthored(this.env.pl, this.rid, author, (mut) => {
332
- // mut.setBlockLabel(blockId, label);
333
- // });
334
- // await this.projectTree.refreshState();
335
- // }
336
-
337
337
  /**
338
+ * @deprecated Use mutateBlockStorage() for V3 blocks.
338
339
  * Sets block args, and changes whole project state accordingly.
339
340
  * Along with setting arguments one can specify author marker, that will be
340
341
  * transactionally associated with the block, to facilitate conflict resolution
341
342
  * in collaborative editing scenario.
342
343
  * */
343
344
  public async setBlockArgs(blockId: string, args: unknown, author?: AuthorMarker) {
344
- await withProjectAuthored(this.env.projectHelper, this.env.pl, this.rid, author, (mut) =>
345
- mut.setStates([{ blockId, args }]),
346
- { name: 'setBlockArgs', lockId: this.projectLockId });
345
+ await withProjectAuthored(this.env.projectHelper, this.env.pl, this.rid, author, (mut) => {
346
+ const state = mut.mergeBlockState(blockId, { args });
347
+ mut.setStates([{ modelAPIVersion: 1, blockId, state }]);
348
+ }, { name: 'setBlockArgs', lockId: this.projectLockId });
347
349
  await this.projectTree.refreshState();
348
350
  }
349
351
 
350
352
  /**
353
+ * @deprecated Use mutateBlockStorage() for V3 blocks.
351
354
  * Sets ui block state associated with the block.
352
355
  * Along with setting arguments one can specify author marker, that will be
353
356
  * transactionally associated with the block, to facilitate conflict resolution
354
357
  * in collaborative editing scenario.
355
358
  * */
356
359
  public async setUiState(blockId: string, uiState: unknown, author?: AuthorMarker) {
357
- await withProjectAuthored(this.env.projectHelper, this.env.pl, this.rid, author, (mut) =>
358
- mut.setStates([{ blockId, uiState }]),
359
- { name: 'setUiState', lockId: this.projectLockId });
360
+ await withProjectAuthored(this.env.projectHelper, this.env.pl, this.rid, author, (mut) => {
361
+ const state = mut.mergeBlockState(blockId, { uiState });
362
+ mut.setStates([{ modelAPIVersion: 1, blockId, state }]);
363
+ }, { name: 'setUiState', lockId: this.projectLockId });
360
364
  await this.projectTree.refreshState();
361
365
  }
362
366
 
363
367
  /**
364
- * Sets navigation state.
365
- * */
366
- // eslint-disable-next-line @typescript-eslint/require-await
367
- public async setNavigationState(blockId: string, state: NavigationState): Promise<void> {
368
- this.navigationStates.setState(blockId, state);
369
- }
370
-
371
- /**
368
+ * @deprecated Use mutateBlockStorage() for V3 blocks.
372
369
  * Sets block args and ui state, and changes the whole project state accordingly.
373
370
  * Along with setting arguments one can specify author marker, that will be
374
371
  * transactionally associated with the block, to facilitate conflict resolution
@@ -376,16 +373,43 @@ export class Project {
376
373
  * */
377
374
  public async setBlockArgsAndUiState(
378
375
  blockId: string,
379
- args: unknown,
380
- uiState: unknown,
376
+ args: unknown, // keep for v1/v2 compatibility
377
+ uiState: unknown, // keep for v1/v2 compatibility
381
378
  author?: AuthorMarker,
382
379
  ) {
380
+ // Normalize to unified state format { args, uiState } for v1/v2 blocks
381
+ const state = { args, uiState };
383
382
  await withProjectAuthored(this.env.projectHelper, this.env.pl, this.rid, author, (mut) => {
384
- mut.setStates([{ blockId, args, uiState }]);
383
+ mut.setStates([{ modelAPIVersion: 1, blockId, state }]);
385
384
  }, { name: 'setBlockArgsAndUiState', lockId: this.projectLockId });
386
385
  await this.projectTree.refreshState();
387
386
  }
388
387
 
388
+ /**
389
+ * Sets navigation state.
390
+ * */
391
+ // eslint-disable-next-line @typescript-eslint/require-await
392
+ public async setNavigationState(blockId: string, state: NavigationState): Promise<void> {
393
+ this.navigationStates.setState(blockId, state);
394
+ }
395
+
396
+ /**
397
+ * Mutates block storage for Model API v3 blocks.
398
+ * Applies a storage operation (e.g., 'update-data') which triggers
399
+ * args derivation (args(data) and prerunArgs(data)).
400
+ * The derived args are stored atomically with the data.
401
+ *
402
+ * @param blockId - The block ID
403
+ * @param payload - Storage mutation payload with operation and value
404
+ * @param author - Optional author marker for collaborative editing
405
+ */
406
+ public async mutateBlockStorage(blockId: string, payload: { operation: string; value: unknown }, author?: AuthorMarker) {
407
+ await withProjectAuthored(this.env.projectHelper, this.env.pl, this.rid, author, (mut) =>
408
+ mut.setStates([{ modelAPIVersion: 2, blockId, payload }]),
409
+ { name: 'mutateBlockStorage', lockId: this.projectLockId });
410
+ await this.projectTree.refreshState();
411
+ }
412
+
389
413
  /** Update block settings */
390
414
  public async setBlockSettings(blockId: string, newValue: BlockSettings) {
391
415
  await withProjectAuthored(this.env.projectHelper, this.env.pl, this.rid, undefined, (mut) => {
@@ -394,6 +418,20 @@ export class Project {
394
418
  await this.projectTree.refreshState();
395
419
  }
396
420
 
421
+ /**
422
+ * Sets raw block storage content directly.
423
+ * This bypasses all normalization and VM transformations.
424
+ *
425
+ * @param blockId The block to set storage for
426
+ * @param rawStorageJson Raw storage as JSON string
427
+ */
428
+ public async setBlockStorageRaw(blockId: string, rawStorageJson: string): Promise<void> {
429
+ await withProjectAuthored(this.env.projectHelper, this.env.pl, this.rid, undefined, (mut) => {
430
+ mut.setBlockStorageRaw(blockId, rawStorageJson);
431
+ }, { name: 'setBlockStorageRaw' });
432
+ await this.projectTree.refreshState();
433
+ }
434
+
397
435
  /** Resets arguments and ui state of the block to initial state */
398
436
  public async resetBlockArgsAndUiState(blockId: string, author?: AuthorMarker): Promise<void> {
399
437
  await this.env.pl.withWriteTx('BlockInputsReset', async (tx) => {
@@ -406,8 +444,16 @@ export class Project {
406
444
  );
407
445
  const bpData = await tx.getResourceData(bpRid, false);
408
446
  const config = extractConfig(cachedDeserialize<BlockPackInfo>(notEmpty(bpData.data)).config);
447
+
409
448
  await withProjectAuthored(this.env.projectHelper, tx, this.rid, author, (prj) => {
410
- prj.setStates([{ blockId, args: config.initialArgs, uiState: config.initialUiState }]);
449
+ if (config.modelAPIVersion === 2) {
450
+ // V2+: Reset to initial storage via VM
451
+ prj.resetToInitialStorage(blockId);
452
+ } else {
453
+ // V1: Use legacy state format
454
+ const initialState = { args: config.initialArgs, uiState: config.initialUiState };
455
+ prj.setStates([{ modelAPIVersion: 1, blockId, state: initialState }]);
456
+ }
411
457
  }, { name: 'resetBlockArgsAndUiState', lockId: this.projectLockId });
412
458
  await tx.commit();
413
459
  });
@@ -423,7 +469,7 @@ export class Project {
423
469
  const fullState = Computable.make(
424
470
  (ctx) => {
425
471
  return {
426
- argsAndUiState: blockArgsAndUiState(this.projectTree.entry(), blockId, ctx),
472
+ parameters: getBlockParameters(this.projectTree.entry(), blockId, ctx),
427
473
  outputs,
428
474
  navigationState: this.navigationStates.getState(blockId),
429
475
  overview: this.overview,
@@ -438,10 +484,10 @@ export class Project {
438
484
  : v.outputs;
439
485
 
440
486
  return {
441
- ...v.argsAndUiState,
487
+ ...v.parameters,
442
488
  outputs: newOutputs,
443
489
  navigationState: v.navigationState,
444
- } as BlockStateInternal;
490
+ } as BlockStateInternalV3;
445
491
  },
446
492
  },
447
493
  );
@@ -461,7 +507,7 @@ export class Project {
461
507
  * Returns a computable, that can be used to retrieve and watch full block state,
462
508
  * including outputs, arguments, ui state.
463
509
  * */
464
- public getBlockState(blockId: string): Computable<BlockStateInternal> {
510
+ public getBlockState(blockId: string): Computable<BlockStateInternalV3> {
465
511
  return this.getBlockComputables(blockId).fullState;
466
512
  }
467
513
 
@@ -34,13 +34,11 @@ import { resourceIdToString, type ResourceId } from '@milaboratories/pl-client';
34
34
  import * as R from 'remeda';
35
35
 
36
36
  type BlockInfo = {
37
- argsRid: ResourceId;
37
+ argsRid?: ResourceId;
38
38
  currentArguments: unknown;
39
39
  prod?: ProdState;
40
40
  };
41
41
 
42
- type _CalculationStatus = 'Running' | 'Done';
43
-
44
42
  type ProdState = {
45
43
  finished: boolean;
46
44
 
@@ -87,9 +85,9 @@ export function projectOverview(
87
85
  const cInputs = prj.traverse({
88
86
  field: projectFieldName(id, 'currentArgs'),
89
87
  assertFieldType: 'Dynamic',
90
- errorIfFieldNotSet: true,
88
+ stableIfNotFound: true,
91
89
  });
92
- const currentArguments = cInputs.getDataAsJson() as Record<string, unknown>;
90
+ const currentArguments = cInputs?.getDataAsJson<Record<string, unknown>>();
93
91
 
94
92
  let prod: ProdState | undefined = undefined;
95
93
 
@@ -129,7 +127,7 @@ export function projectOverview(
129
127
  };
130
128
  }
131
129
 
132
- infos.set(id, { currentArguments, prod, argsRid: cInputs.resourceInfo.id });
130
+ infos.set(id, { currentArguments, prod, argsRid: cInputs?.resourceInfo.id });
133
131
  }
134
132
 
135
133
  const currentGraph = productionGraph(structure, (id) => {
@@ -138,7 +136,13 @@ export function projectOverview(
138
136
  const args = bInfo.currentArguments;
139
137
  return {
140
138
  args,
141
- enrichmentTargets: env.projectHelper.getEnrichmentTargets(() => bpInfo.cfg, () => args, { argsRid: bInfo.argsRid, blockPackRid: bpInfo.bpResourceId }),
139
+ enrichmentTargets: bInfo.argsRid
140
+ ? env.projectHelper.getEnrichmentTargets(
141
+ () => bpInfo.cfg,
142
+ () => args,
143
+ { argsRid: bInfo.argsRid, blockPackRid: bpInfo.bpResourceId },
144
+ )
145
+ : undefined,
142
146
  };
143
147
  });
144
148
 
@@ -235,19 +239,24 @@ export function projectOverview(
235
239
  },
236
240
  }) as ComputableStableDefined<string[]>,
237
241
  ),
238
- inputsValid: computableFromCfgOrRF(
239
- env,
240
- blockCtxArgsOnly,
241
- cfg.inputsValid,
242
- codeWithInfo,
243
- bpId,
244
- ).wrap({
245
- recover: (cause) => {
246
- // I'm not sure that we should write an error here, because it just means "Invalid args"
247
- env.logger.error(new Error('Error in block model argsValid', { cause }));
248
- return false;
249
- },
250
- }) as ComputableStableDefined<boolean>,
242
+ // inputsValid: for modelAPIVersion 2, it's true if currentArgs exists (args derivation succeeded)
243
+ // For older blocks, use the inputsValid callback from config
244
+ inputsValid: cfg.modelAPIVersion === 2
245
+ ? info.currentArguments !== undefined
246
+ : cfg.inputsValid
247
+ ? computableFromCfgOrRF(
248
+ env,
249
+ blockCtxArgsOnly,
250
+ cfg.inputsValid,
251
+ codeWithInfo,
252
+ bpId,
253
+ ).wrap({
254
+ recover: (cause) => {
255
+ env.logger.error(new Error('Error in block model inputsValid', { cause }));
256
+ return false;
257
+ },
258
+ }) as ComputableStableDefined<boolean>
259
+ : undefined,
251
260
  sdkVersion: codeWithInfo?.sdkVersion,
252
261
  featureFlags: codeWithInfo?.featureFlags ?? {},
253
262
  isIncompatibleWithRuntime: false,
@@ -262,6 +271,20 @@ export function projectOverview(
262
271
  })
263
272
  .getDataAsJson() as BlockSettings;
264
273
 
274
+ // Get block storage info by calling VM function (only for Model API v2 blocks)
275
+ const blockStorageInfo = ifNotUndef(bp, ({ cfg }) => {
276
+ if (cfg.modelAPIVersion !== 2) {
277
+ return undefined;
278
+ }
279
+ const storageNode = prj.traverse({
280
+ field: projectFieldName(id, 'blockStorage'),
281
+ assertFieldType: 'Dynamic',
282
+ stableIfNotFound: true,
283
+ });
284
+ const rawStorageJson = storageNode?.getDataAsString();
285
+ return env.projectHelper.getStorageInfoInVM(cfg, rawStorageJson);
286
+ });
287
+
265
288
  const updates = ifNotUndef(bp, ({ info }) =>
266
289
  env.blockUpdateWatcher.get({ currentSpec: info.source, settings }),
267
290
  );
@@ -292,6 +315,7 @@ export function projectOverview(
292
315
  featureFlags,
293
316
  isIncompatibleWithRuntime,
294
317
  navigationState: navigationStates.getState(id),
318
+ blockStorageInfo,
295
319
  };
296
320
  });
297
321
 
@@ -25,6 +25,7 @@ export async function withMl(
25
25
  });
26
26
  ml.addRuntimeCapability('requiresUIAPIVersion', 1);
27
27
  ml.addRuntimeCapability('requiresUIAPIVersion', 2);
28
+ ml.addRuntimeCapability('requiresUIAPIVersion', 3);
28
29
  try {
29
30
  await cb(ml, workFolder);
30
31
  } finally {
@@ -159,7 +160,6 @@ test.skip('basic quickjs code', async () => {
159
160
  vm.newFunction('nextId', () => {
160
161
  return vm.newNumber(12); // vm.newArrayBuffer(new Uint8Array([1, 2]));
161
162
  }).consume((fn) => vm.setProp(vm.global, 'nextId', fn));
162
- console.log('asdasdas');
163
163
 
164
164
  const nextId = vm.getString(
165
165
  scope.manage(
@@ -21,7 +21,7 @@ export function computableFromCfgOrRF(
21
21
  ops: Partial<ComputableRenderingOps> = {},
22
22
  ): Computable<unknown> {
23
23
  if (isConfigLambda(cfgOrFh)) {
24
- if (codeWithInfo === undefined) throw new Error('No code bundle.');
24
+ if (codeWithInfo === undefined) throw new Error('computableFromCfgOrRF: No code bundle.');
25
25
  return computableFromRF(env, ctx, cfgOrFh, codeWithInfo, configKey, ops);
26
26
  } else return computableFromCfg(env.driverKit, ctx, cfgOrFh, ops);
27
27
  }
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Block Storage Helper - Middle layer utilities for BlockStorage operations.
3
+ *
4
+ * This module provides the bridge between the SDK's BlockStorage abstraction
5
+ * and the middle layer's storage operations. Block developers never interact
6
+ * with this - they only see `state`.
7
+ *
8
+ * @module block_storage_helper
9
+ */
10
+
11
+ import type {
12
+ BlockStorage,
13
+ BlockStorageHandlers,
14
+ } from '@platforma-sdk/model';
15
+ import {
16
+ BLOCK_STORAGE_KEY,
17
+ BLOCK_STORAGE_SCHEMA_VERSION,
18
+ createBlockStorage,
19
+ defaultBlockStorageHandlers,
20
+ getPluginData,
21
+ getStorageData,
22
+ getStorageDataVersion,
23
+ isBlockStorage,
24
+ normalizeBlockStorage,
25
+ setPluginData,
26
+ updateStorageData,
27
+ updateStorageDataVersion,
28
+ } from '@platforma-sdk/model';
29
+
30
+ // Re-export types for convenience
31
+ export type { BlockStorage, BlockStorageHandlers };
32
+
33
+ // Re-export utilities that middle layer needs
34
+ export {
35
+ BLOCK_STORAGE_KEY,
36
+ BLOCK_STORAGE_SCHEMA_VERSION,
37
+ createBlockStorage,
38
+ defaultBlockStorageHandlers,
39
+ getPluginData,
40
+ getStorageData,
41
+ getStorageDataVersion,
42
+ isBlockStorage,
43
+ normalizeBlockStorage,
44
+ setPluginData,
45
+ updateStorageData,
46
+ updateStorageDataVersion,
47
+ };
48
+
49
+ /**
50
+ * Parses raw blockStorage data (from resource tree) into BlockStorage.
51
+ * Handles both legacy format (raw state) and new format (BlockStorage).
52
+ *
53
+ * @param rawData - Raw data from blockStorage field (may be string or parsed object)
54
+ * @returns Normalized BlockStorage object
55
+ */
56
+ export function parseBlockStorage<TState = unknown>(rawData: unknown): BlockStorage<TState> {
57
+ if (rawData === undefined || rawData === null) {
58
+ return createBlockStorage<TState>({} as TState);
59
+ }
60
+
61
+ // If it's a string, parse it first
62
+ if (typeof rawData === 'string') {
63
+ try {
64
+ const parsed = JSON.parse(rawData);
65
+ return normalizeBlockStorage<TState>(parsed);
66
+ } catch {
67
+ // If parsing fails, treat the string as the state itself
68
+ return createBlockStorage(rawData as unknown as TState);
69
+ }
70
+ }
71
+
72
+ return normalizeBlockStorage<TState>(rawData);
73
+ }
74
+
75
+ /**
76
+ * Extracts the user-facing data from BlockStorage.
77
+ * This is what block developers see in their .args() callbacks.
78
+ *
79
+ * @param storage - The BlockStorage object
80
+ * @returns The data value that developers work with
81
+ */
82
+ export function extractDataForDeveloper<TState>(storage: BlockStorage<TState>): TState {
83
+ return getStorageData(storage);
84
+ }
85
+
86
+ /**
87
+ * Creates a new BlockStorage with updated state.
88
+ * Used when setState is called from the frontend.
89
+ *
90
+ * @param currentStorage - Current BlockStorage (or undefined for new blocks)
91
+ * @param newState - New state from developer
92
+ * @param handlers - Optional custom handlers (defaults to standard handlers)
93
+ * @returns Updated BlockStorage
94
+ */
95
+ export function applyStateUpdate<TState>(
96
+ currentStorage: BlockStorage<TState> | undefined,
97
+ newState: TState,
98
+ handlers: BlockStorageHandlers<TState> = defaultBlockStorageHandlers as BlockStorageHandlers<TState>,
99
+ ): BlockStorage<TState> {
100
+ const storage = currentStorage ?? createBlockStorage<TState>({} as TState);
101
+ const transform = handlers.transformStateForStorage ?? defaultBlockStorageHandlers.transformStateForStorage;
102
+ return transform(storage, newState) as BlockStorage<TState>;
103
+ }
104
+
105
+ /**
106
+ * Serializes BlockStorage for writing to the resource tree.
107
+ *
108
+ * @param storage - The BlockStorage to serialize
109
+ * @returns JSON string
110
+ */
111
+ export function serializeBlockStorage(storage: BlockStorage): string {
112
+ return JSON.stringify(storage);
113
+ }
114
+
115
+ /**
116
+ * Checks if the storage needs migration based on version.
117
+ *
118
+ * @param storage - Current BlockStorage
119
+ * @param targetVersion - Target schema version
120
+ * @returns true if migration is needed
121
+ */
122
+ export function needsMigration(storage: BlockStorage, targetVersion: number): boolean {
123
+ return getStorageDataVersion(storage) < targetVersion;
124
+ }
125
+
126
+ /**
127
+ * Migrates BlockStorage from one version to another.
128
+ *
129
+ * @param storage - Current BlockStorage
130
+ * @param fromVersion - Source version
131
+ * @param toVersion - Target version
132
+ * @param handlers - Optional custom handlers
133
+ * @returns Migrated BlockStorage
134
+ */
135
+ export function migrateBlockStorage<TState>(
136
+ storage: BlockStorage<TState>,
137
+ fromVersion: number,
138
+ toVersion: number,
139
+ handlers: BlockStorageHandlers<TState> = defaultBlockStorageHandlers as BlockStorageHandlers<TState>,
140
+ ): BlockStorage<TState> {
141
+ const migrate = handlers.migrateStorage ?? defaultBlockStorageHandlers.migrateStorage;
142
+ return migrate(storage, fromVersion, toVersion) as BlockStorage<TState>;
143
+ }
144
+
145
+ /**
146
+ * V1/V2 legacy format - state stored as { args, uiState }
147
+ */
148
+ export interface LegacyV1V2State {
149
+ args?: unknown;
150
+ uiState?: unknown;
151
+ }
152
+
153
+ /**
154
+ * Checks if the raw data is in legacy V1/V2 format.
155
+ * V1/V2 blocks stored { args, uiState } directly, not wrapped in BlockStorage.
156
+ *
157
+ * @param rawData - Raw data from blockStorage field
158
+ * @returns true if data is in legacy format
159
+ */
160
+ export function isLegacyV1V2Format(rawData: unknown): rawData is LegacyV1V2State {
161
+ if (rawData === null || typeof rawData !== 'object') return false;
162
+ // If it has the discriminator, it's BlockStorage format
163
+ if (isBlockStorage(rawData)) return false;
164
+
165
+ const obj = rawData as Record<string, unknown>;
166
+ // Legacy format has 'args' and/or 'uiState' at top level, no discriminator
167
+ return ('args' in obj || 'uiState' in obj);
168
+ }
169
+
170
+ /**
171
+ * Converts legacy V1/V2 format to BlockStorage.
172
+ * For backward compatibility with existing blocks.
173
+ *
174
+ * @param legacyData - Data in { args, uiState } format
175
+ * @returns BlockStorage with legacy data as state
176
+ */
177
+ export function convertLegacyToBlockStorage(legacyData: LegacyV1V2State): BlockStorage<LegacyV1V2State> {
178
+ return createBlockStorage(legacyData, 1);
179
+ }
180
+
181
+ /**
182
+ * Smart parser that handles all storage formats:
183
+ * - New BlockStorage format
184
+ * - Legacy V1/V2 format ({ args, uiState })
185
+ * - Raw state (any other format)
186
+ *
187
+ * @param rawData - Raw data from blockStorage field
188
+ * @returns Normalized BlockStorage
189
+ */
190
+ export function parseBlockStorageSmart<TState = unknown>(rawData: unknown): BlockStorage<TState> {
191
+ if (rawData === undefined || rawData === null) {
192
+ return createBlockStorage<TState>({} as TState);
193
+ }
194
+
195
+ // If it's a string, parse it first
196
+ let parsed = rawData;
197
+ if (typeof rawData === 'string') {
198
+ try {
199
+ parsed = JSON.parse(rawData);
200
+ } catch {
201
+ // If parsing fails, treat the string as the state itself
202
+ return createBlockStorage(rawData as unknown as TState);
203
+ }
204
+ }
205
+
206
+ // Check for legacy V1/V2 format
207
+ if (isLegacyV1V2Format(parsed)) {
208
+ return convertLegacyToBlockStorage(parsed) as unknown as BlockStorage<TState>;
209
+ }
210
+
211
+ // Use standard normalization
212
+ return normalizeBlockStorage<TState>(parsed);
213
+ }
@@ -1,2 +1,3 @@
1
1
  export * from './block_pack_spec';
2
- export { type ProjectListEntry } from './project_model';
2
+ export * from './block_storage_helper';
3
+ export { type ProjectListEntry, type ProjectField } from './project_model';