@milaboratories/pl-middle-layer 1.45.5 → 1.46.1

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 +15 -15
  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
@@ -5,7 +5,8 @@ import type {
5
5
  PlTransaction,
6
6
  ResourceData,
7
7
  ResourceId,
8
- TxOps } from '@milaboratories/pl-client';
8
+ TxOps,
9
+ } from '@milaboratories/pl-client';
9
10
  import {
10
11
  ensureResourceIdNotNull,
11
12
  field,
@@ -65,7 +66,7 @@ import {
65
66
  import Denque from 'denque';
66
67
  import { exportContext, getPreparedExportTemplateEnvelope } from './context_export';
67
68
  import { loadTemplate } from './template/template_loading';
68
- import { cachedDeserialize, notEmpty, canonicalJsonGzBytes, canonicalJsonBytes } from '@milaboratories/ts-helpers';
69
+ import { cachedDeserialize, notEmpty, canonicalJsonBytes, cachedDecode } from '@milaboratories/ts-helpers';
69
70
  import type { ProjectHelper } from '../model/project_helper';
70
71
  import { extractConfig, UiError, type BlockConfig } from '@platforma-sdk/model';
71
72
  import { getDebugFlags } from '../debug';
@@ -141,12 +142,34 @@ class BlockInfo {
141
142
 
142
143
  if (this.fields.blockPack === undefined) throw new Error('no block pack field');
143
144
 
144
- if (this.fields.currentArgs === undefined) throw new Error('no current args field');
145
+ if (this.fields.blockStorage === undefined) throw new Error('no block storage field');
145
146
  }
146
147
 
147
148
  private readonly currentArgsC = cached(
148
- () => this.fields.currentArgs!.modCount,
149
- () => cachedDeserialize(this.fields.currentArgs!.value!),
149
+ () => this.fields.currentArgs?.modCount,
150
+ () => {
151
+ const bin = this.fields.currentArgs?.value;
152
+ if (bin === undefined) return undefined;
153
+ return cachedDeserialize(bin);
154
+ },
155
+ );
156
+
157
+ private readonly blockStorageC = cached(
158
+ () => this.fields.blockStorage!.modCount,
159
+ () => {
160
+ const bin = this.fields.blockStorage?.value;
161
+ if (bin === undefined) return undefined;
162
+ return cachedDeserialize<Record<string, unknown>>(bin);
163
+ },
164
+ );
165
+
166
+ private readonly blockStorageJ = cached(
167
+ () => this.fields.blockStorage!.modCount,
168
+ () => {
169
+ const bin = this.fields.blockStorage?.value;
170
+ if (bin === undefined) return undefined;
171
+ return cachedDecode(bin);
172
+ },
150
173
  );
151
174
 
152
175
  private readonly prodArgsC = cached(
@@ -158,10 +181,36 @@ class BlockInfo {
158
181
  },
159
182
  );
160
183
 
184
+ private readonly currentPrerunArgsC = cached(
185
+ () => this.fields.currentPrerunArgs?.modCount,
186
+ () => {
187
+ const bin = this.fields.currentPrerunArgs?.value;
188
+ if (bin === undefined) return undefined;
189
+ return cachedDeserialize(bin);
190
+ },
191
+ );
192
+
161
193
  get currentArgs(): unknown {
162
194
  return this.currentArgsC();
163
195
  }
164
196
 
197
+ get blockStorage(): unknown {
198
+ try {
199
+ return this.blockStorageC();
200
+ } catch (e) {
201
+ console.error('Error getting blockStorage:', e);
202
+ return undefined;
203
+ }
204
+ }
205
+
206
+ get blockStorageJson() {
207
+ return this.blockStorageJ();
208
+ }
209
+
210
+ get currentPrerunArgs(): unknown {
211
+ return this.currentPrerunArgsC();
212
+ }
213
+
165
214
  get stagingRendered(): boolean {
166
215
  return this.fields.stagingCtx !== undefined;
167
216
  }
@@ -181,14 +230,17 @@ class BlockInfo {
181
230
  || Buffer.compare(this.fields.currentArgs!.value!, this.fields.prodArgs.value!) !== 0,
182
231
  );
183
232
 
184
- // get productionStale(): boolean {
185
- // return this.productionRendered && this.productionStaleC() && ;
186
- // }
187
-
188
233
  get requireProductionRendering(): boolean {
189
234
  return !this.productionRendered || this.productionStaleC() || this.productionHasErrors;
190
235
  }
191
236
 
237
+ /** Returns true if staging should be re-rendered (stagingCtx is not set) */
238
+ get requireStagingRendering(): boolean {
239
+ // No staging needed if currentPrerunArgs is undefined (args derivation failed)
240
+ if (this.fields.currentPrerunArgs === undefined) return false;
241
+ return !this.stagingRendered;
242
+ }
243
+
192
244
  get prodArgs(): unknown {
193
245
  return this.prodArgsC();
194
246
  }
@@ -202,33 +254,38 @@ class BlockInfo {
202
254
  }
203
255
  }
204
256
 
205
- export interface NewBlockSpec {
206
- blockPack: BlockPackSpecPrepared;
207
- args: string;
208
- uiState: string;
209
- }
257
+ /**
258
+ * Specification for creating a new block.
259
+ * Discriminated union based on `storageMode`.
260
+ */
261
+ /** Specification for creating a new block. Discriminated union based on `storageMode`. */
262
+ export type NewBlockSpec =
263
+ | { storageMode: 'fromModel'; blockPack: BlockPackSpecPrepared }
264
+ | { storageMode: 'legacy'; blockPack: BlockPackSpecPrepared; legacyState: string };
210
265
 
211
266
  const NoNewBlocks = (blockId: string) => {
212
267
  throw new Error(`No new block info for ${blockId}`);
213
268
  };
214
269
 
215
- export interface SetStatesRequest {
270
+ /**
271
+ * Request to set block state using unified state format.
272
+ * For v3 blocks: state is the block's state
273
+ * For v1/v2 blocks: state should be { args, uiState } format
274
+ */
275
+ export type SetStatesRequest = {
216
276
  blockId: string;
217
- args?: unknown;
218
- uiState?: unknown;
219
- }
277
+ /** The unified state to set */
278
+ state: unknown;
279
+ modelAPIVersion: 1;
280
+ } | {
281
+ blockId: string;
282
+ /** Storage operation payload - middle layer is agnostic to specific operations */
283
+ payload: { operation: string; value: unknown };
284
+ modelAPIVersion: 2;
285
+ };
220
286
 
221
- type _GraphInfoFields =
222
- | 'stagingUpstream'
223
- | 'stagingDownstream'
224
- | 'futureProductionUpstream'
225
- | 'futureProductionDownstream'
226
- | 'actualProductionUpstream'
227
- | 'actualProductionDownstream';
228
-
229
- export type ArgsAndUiState = {
230
- args: unknown;
231
- uiState: unknown;
287
+ export type ClearState = {
288
+ state: unknown;
232
289
  };
233
290
 
234
291
  export class ProjectMutator {
@@ -295,7 +352,6 @@ export class ProjectMutator {
295
352
  }
296
353
 
297
354
  get structure(): ProjectStructure {
298
- // clone
299
355
  return JSON.parse(JSON.stringify(this.struct)) as ProjectStructure;
300
356
  }
301
357
 
@@ -315,7 +371,7 @@ export class ProjectMutator {
315
371
  private getProductionGraphBlockInfo(blockId: string, prod: boolean): ProductionGraphBlockInfo | undefined {
316
372
  const bInfo = this.getBlockInfo(blockId);
317
373
 
318
- let argsField: BlockFieldState;
374
+ let argsField: BlockFieldState | undefined;
319
375
  let args: unknown;
320
376
 
321
377
  if (prod) {
@@ -323,10 +379,15 @@ export class ProjectMutator {
323
379
  argsField = bInfo.fields.prodArgs;
324
380
  args = bInfo.prodArgs;
325
381
  } else {
326
- argsField = notEmpty(bInfo.fields.currentArgs);
382
+ argsField = bInfo.fields.currentArgs;
327
383
  args = bInfo.currentArgs;
328
384
  }
329
385
 
386
+ // Can't compute enrichment targets without args
387
+ if (argsField === undefined) {
388
+ return { args, enrichmentTargets: undefined };
389
+ }
390
+
330
391
  const blockPackField = notEmpty(bInfo.fields.blockPack);
331
392
 
332
393
  if (isResourceId(argsField.ref!) && isResourceId(blockPackField.ref!))
@@ -371,6 +432,7 @@ export class ProjectMutator {
371
432
  }
372
433
 
373
434
  private createJsonFieldValueByContent(content: string): BlockFieldStateValue {
435
+ if (content === undefined) throw new Error('content is undefined');
374
436
  const value = Buffer.from(content);
375
437
  const ref = this.tx.createValue(Pl.JsonObject, value);
376
438
  return { ref, value, status: 'Ready' };
@@ -508,6 +570,70 @@ export class ProjectMutator {
508
570
  }
509
571
  }
510
572
 
573
+ /**
574
+ * Gets current block state and merges with partial updates.
575
+ * Used by legacy v1/v2 methods like setBlockArgs and setUiState.
576
+ *
577
+ * @param blockId The block to get state for
578
+ * @param partialUpdate Partial state to merge (e.g. { args } or { uiState })
579
+ * @returns Merged state in unified format { args, uiState }
580
+ */
581
+ public mergeBlockState(
582
+ blockId: string,
583
+ partialUpdate: { args?: unknown; uiState?: unknown },
584
+ ): { args?: unknown; uiState?: unknown } {
585
+ const info = this.getBlockInfo(blockId);
586
+ const currentState = info.blockStorage as { args?: unknown; uiState?: unknown } | undefined;
587
+ return { ...currentState, ...partialUpdate };
588
+ }
589
+
590
+ /**
591
+ * Sets raw block storage content directly (for testing purposes).
592
+ * This bypasses all normalization and VM transformations.
593
+ *
594
+ * @param blockId The block to set storage for
595
+ * @param rawStorageJson Raw storage as JSON string
596
+ */
597
+ public setBlockStorageRaw(blockId: string, rawStorageJson: string): void {
598
+ this.setBlockFieldObj(blockId, 'blockStorage', this.createJsonFieldValueByContent(rawStorageJson));
599
+ this.blocksWithChangedInputs.add(blockId);
600
+ this.updateLastModified();
601
+ }
602
+
603
+ /**
604
+ * Resets a v2+ block to its initial storage state.
605
+ * Gets initial storage from VM and derives args from it.
606
+ *
607
+ * For v1 blocks, use setStates() instead.
608
+ *
609
+ * @param blockId The block to reset
610
+ */
611
+ public resetToInitialStorage(blockId: string): void {
612
+ const info = this.getBlockInfo(blockId);
613
+ const blockConfig = info.config;
614
+
615
+ if (blockConfig.modelAPIVersion !== 2) {
616
+ throw new Error('resetToInitialStorage is only supported for model API version 2');
617
+ }
618
+
619
+ // Get initial storage from VM
620
+ const initialStorageJson = this.projectHelper.getInitialStorageInVM(blockConfig);
621
+ this.setBlockStorageRaw(blockId, initialStorageJson);
622
+
623
+ // Derive args from storage - set or clear currentArgs based on derivation result
624
+ const deriveArgsResult = this.projectHelper.deriveArgsFromStorage(blockConfig, initialStorageJson);
625
+ if (!deriveArgsResult.error) {
626
+ this.setBlockFieldObj(blockId, 'currentArgs', this.createJsonFieldValue(deriveArgsResult.value));
627
+ // Derive prerunArgs from storage
628
+ const prerunArgs = this.projectHelper.derivePrerunArgsFromStorage(blockConfig, initialStorageJson);
629
+ if (prerunArgs !== undefined) {
630
+ this.setBlockFieldObj(blockId, 'currentPrerunArgs', this.createJsonFieldValue(prerunArgs));
631
+ }
632
+ } else {
633
+ this.deleteBlockFields(blockId, 'currentArgs');
634
+ }
635
+ }
636
+
511
637
  /** Optimally sets inputs for multiple blocks in one go */
512
638
  public setStates(requests: SetStatesRequest[]) {
513
639
  const changedArgs: string[] = [];
@@ -515,34 +641,87 @@ export class ProjectMutator {
515
641
  for (const req of requests) {
516
642
  const info = this.getBlockInfo(req.blockId);
517
643
  let blockChanged = false;
518
- for (const stateKey of ['args', 'uiState'] as const) {
519
- if (!(stateKey in req)) continue;
520
- const statePart = req[stateKey];
521
- if (statePart === undefined || statePart === null)
522
- throw new Error(
523
- `Can't set ${stateKey} to null or undefined, please omit the key if you don't want to change it`,
524
- );
525
-
526
- const fieldName = stateKey === 'args' ? 'currentArgs' : 'uiState';
527
-
528
- let data: Uint8Array;
529
- let gzipped: boolean = false;
530
- if (stateKey === 'args') {
531
- // don't gzip args, workflow code can't uncompress gzip yet
532
- data = canonicalJsonBytes(statePart);
644
+
645
+ const blockConfig = info.config;
646
+ // modelAPIVersion === 2 means BlockModelV3 with .args() lambda for deriving args
647
+
648
+ if (req.modelAPIVersion !== blockConfig.modelAPIVersion) {
649
+ throw new Error(`Model API version mismatch for block ${req.blockId}: ${req.modelAPIVersion} !== ${blockConfig.modelAPIVersion}`);
650
+ }
651
+
652
+ // Derive args from storage using the block's config.args() callback
653
+ let args: unknown;
654
+ let prerunArgs: unknown;
655
+
656
+ if (req.modelAPIVersion === 2) {
657
+ const currentStorageJson = info.blockStorageJson;
658
+ if (currentStorageJson === undefined) {
659
+ throw new Error(`Block ${req.blockId} has no blockStorage - this should not happen`);
660
+ }
661
+
662
+ // Apply the state update to storage
663
+ const updatedStorageJson = this.projectHelper.applyStorageUpdateInVM(
664
+ blockConfig,
665
+ currentStorageJson,
666
+ req.payload,
667
+ );
668
+
669
+ this.setBlockFieldObj(req.blockId, 'blockStorage', this.createJsonFieldValueByContent(updatedStorageJson));
670
+
671
+ // Derive args directly from storage (VM extracts data internally)
672
+ const derivedArgsResult = this.projectHelper.deriveArgsFromStorage(blockConfig, updatedStorageJson);
673
+ if (derivedArgsResult.error) {
674
+ args = undefined;
675
+ prerunArgs = undefined;
533
676
  } else {
534
- const { data: binary, isGzipped } = canonicalJsonGzBytes(statePart);
535
- data = binary;
536
- gzipped = isGzipped;
677
+ args = derivedArgsResult.value;
678
+ // Derive prerunArgs from storage, or fall back to args
679
+ prerunArgs = this.projectHelper.derivePrerunArgsFromStorage(blockConfig, updatedStorageJson);
680
+ }
681
+ } else {
682
+ this.setBlockFieldObj(req.blockId, 'blockStorage', this.createJsonFieldValue(req.state));
683
+ if (req.state !== null && typeof req.state === 'object' && 'args' in req.state) {
684
+ args = (req.state as { args: unknown }).args;
685
+ } else {
686
+ args = req.state;
687
+ }
688
+ // For the legacy blocks, prerunArgs = args (same as production args)
689
+ prerunArgs = args;
690
+ }
691
+
692
+ // Set or clear currentArgs based on derivation result
693
+ if (args !== undefined) {
694
+ const currentArgsData = canonicalJsonBytes(args);
695
+ const argsPartRef = this.tx.createValue(Pl.JsonObject, currentArgsData);
696
+ this.setBlockField(req.blockId, 'currentArgs', argsPartRef, 'Ready', currentArgsData);
697
+ } else {
698
+ this.deleteBlockFields(req.blockId, 'currentArgs');
699
+ }
700
+
701
+ // Set currentPrerunArgs field and check if it actually changed
702
+ let prerunArgsChanged = false;
703
+ if (prerunArgs !== undefined) {
704
+ const prerunArgsData = canonicalJsonBytes(prerunArgs);
705
+ const oldPrerunArgsData = info.fields.currentPrerunArgs?.value;
706
+ // Check if prerunArgs actually changed
707
+ if (oldPrerunArgsData === undefined || Buffer.compare(oldPrerunArgsData, prerunArgsData) !== 0) {
708
+ prerunArgsChanged = true;
709
+ }
710
+ const prerunArgsRef = this.tx.createValue(Pl.JsonObject, prerunArgsData);
711
+ this.setBlockField(req.blockId, 'currentPrerunArgs', prerunArgsRef, 'Ready', prerunArgsData);
712
+ } else {
713
+ // prerunArgs is undefined - check if we previously had one
714
+ if (info.fields.currentPrerunArgs !== undefined) {
715
+ prerunArgsChanged = true;
537
716
  }
538
- if (Buffer.compare(info.fields[fieldName]!.value!, data) === 0) continue;
539
- // console.log('setting', fieldName, gzipped, data.length);
540
- const statePartRef = this.tx.createValue(gzipped ? Pl.JsonGzObject : Pl.JsonObject, data);
541
- this.setBlockField(req.blockId, fieldName, statePartRef, 'Ready', data);
717
+ }
542
718
 
543
- blockChanged = true;
544
- if (stateKey === 'args') changedArgs.push(req.blockId);
719
+ blockChanged = true;
720
+ // Only add to changedArgs if prerunArgs changed - this controls staging reset
721
+ if (prerunArgsChanged) {
722
+ changedArgs.push(req.blockId);
545
723
  }
724
+
546
725
  if (blockChanged) {
547
726
  // will be assigned our author marker
548
727
  this.blocksWithChangedInputs.add(req.blockId);
@@ -589,19 +768,30 @@ export class ProjectMutator {
589
768
  return exportContext(this.tx, Pl.unwrapHolder(this.tx, this.ctxExportTplHolder), ctx);
590
769
  }
591
770
 
771
+ /**
772
+ * Renders staging for a block using currentPrerunArgs.
773
+ * If currentPrerunArgs is not set (prerunArgs returned undefined), skips staging for this block.
774
+ */
592
775
  private renderStagingFor(blockId: string) {
593
776
  this.resetStaging(blockId);
594
777
 
595
778
  const info = this.getBlockInfo(blockId);
596
779
 
780
+ // If currentPrerunArgs is not set (prerunArgs returned undefined), skip staging for this block
781
+ const prerunArgsRef = info.fields.currentPrerunArgs?.ref;
782
+ if (prerunArgsRef === undefined) {
783
+ return;
784
+ }
785
+
597
786
  const ctx = this.createStagingCtx(this.getStagingGraph().nodes.get(blockId)!.upstream);
598
787
 
599
788
  if (this.getBlock(blockId).renderingMode !== 'Heavy') throw new Error('not supported yet');
600
789
 
601
790
  const tpl = info.getTemplate(this.tx);
602
791
 
792
+ // Use currentPrerunArgs for staging rendering
603
793
  const results = createRenderHeavyBlock(this.tx, tpl, {
604
- args: info.fields.currentArgs!.ref!,
794
+ args: prerunArgsRef,
605
795
  blockId: this.tx.createValue(Pl.JsonString, JSON.stringify(blockId)),
606
796
  isProduction: this.tx.createValue(Pl.JsonBool, JSON.stringify(false)),
607
797
  context: ctx,
@@ -628,6 +818,11 @@ export class ProjectMutator {
628
818
 
629
819
  const info = this.getBlockInfo(blockId);
630
820
 
821
+ // Can't render production if currentArgs is not set
822
+ if (info.fields.currentArgs === undefined) {
823
+ throw new Error(`Can't render production for block ${blockId}: currentArgs not set`);
824
+ }
825
+
631
826
  const ctx = this.createProdCtx(this.getPendingProductionGraph().nodes.get(blockId)!.upstream);
632
827
 
633
828
  if (this.getBlock(blockId).renderingMode === 'Light')
@@ -636,7 +831,7 @@ export class ProjectMutator {
636
831
  const tpl = info.getTemplate(this.tx);
637
832
 
638
833
  const results = createRenderHeavyBlock(this.tx, tpl, {
639
- args: info.fields.currentArgs!.ref!,
834
+ args: info.fields.currentArgs.ref!,
640
835
  blockId: this.tx.createValue(Pl.JsonString, JSON.stringify(blockId)),
641
836
  isProduction: this.tx.createValue(Pl.JsonBool, JSON.stringify(true)),
642
837
  context: ctx,
@@ -651,7 +846,7 @@ export class ProjectMutator {
651
846
  this.setBlockField(blockId, 'prodOutput', results.result, 'NotReady');
652
847
 
653
848
  // saving inputs for which we rendered the production
654
- this.setBlockFieldObj(blockId, 'prodArgs', info.fields.currentArgs!);
849
+ this.setBlockFieldObj(blockId, 'prodArgs', info.fields.currentArgs);
655
850
 
656
851
  // removing block from limbo as we juts rendered fresh production for it
657
852
  if (this.blocksInLimbo.delete(blockId)) this.renderingStateChanged = true;
@@ -676,11 +871,50 @@ export class ProjectMutator {
676
871
  this.createJsonFieldValue(InitialBlockSettings),
677
872
  );
678
873
 
679
- // args
680
- this.setBlockFieldObj(blockId, 'currentArgs', this.createJsonFieldValueByContent(spec.args));
874
+ const blockConfig = info.config;
875
+
876
+ let args: unknown;
877
+ let prerunArgs: unknown;
878
+ let storageToWrite: string;
879
+
880
+ if (spec.storageMode === 'fromModel') {
881
+ // Model API v2+: get initial storage and derive args from it
882
+ storageToWrite = this.projectHelper.getInitialStorageInVM(blockConfig);
883
+
884
+ // Derive args directly from storage (VM extracts data internally)
885
+ const deriveArgsResult = this.projectHelper.deriveArgsFromStorage(blockConfig, storageToWrite);
886
+ if (deriveArgsResult.error) {
887
+ args = undefined;
888
+ prerunArgs = undefined;
889
+ } else {
890
+ args = deriveArgsResult.value;
891
+ prerunArgs = this.projectHelper.derivePrerunArgsFromStorage(blockConfig, storageToWrite);
892
+ }
893
+ } else if (spec.storageMode === 'legacy') {
894
+ // Model API v1: use legacyState from spec
895
+ const parsedState = JSON.parse(spec.legacyState);
896
+ args = parsedState.args;
897
+ if (args === undefined) {
898
+ throw new Error('args is undefined in legacyState');
899
+ }
900
+ prerunArgs = args;
901
+ storageToWrite = spec.legacyState;
902
+ } else {
903
+ throw new Error(`Unknown storageMode: ${(spec as NewBlockSpec).storageMode}`);
904
+ }
905
+
906
+ // currentArgs
907
+ if (args !== undefined) {
908
+ this.setBlockFieldObj(blockId, 'currentArgs', this.createJsonFieldValue(args));
909
+ }
681
910
 
682
- // uiState
683
- this.setBlockFieldObj(blockId, 'uiState', this.createJsonFieldValueByContent(spec.uiState ?? '{}'));
911
+ // currentPrerunArgs
912
+ if (prerunArgs !== undefined) {
913
+ this.setBlockFieldObj(blockId, 'currentPrerunArgs', this.createJsonFieldValue(prerunArgs));
914
+ }
915
+
916
+ // blockStorage
917
+ this.setBlockFieldObj(blockId, 'blockStorage', this.createJsonFieldValueByContent(storageToWrite));
684
918
 
685
919
  // checking structure
686
920
  info.check();
@@ -888,8 +1122,9 @@ export class ProjectMutator {
888
1122
  // Block-pack migration
889
1123
  //
890
1124
 
891
- public migrateBlockPack(blockId: string, spec: BlockPackSpecPrepared, newArgsAndUiState?: ArgsAndUiState): void {
1125
+ public migrateBlockPack(blockId: string, spec: BlockPackSpecPrepared, newClearState?: ClearState): void {
892
1126
  const info = this.getBlockInfo(blockId);
1127
+ const newConfig = extractConfig(spec.config);
893
1128
 
894
1129
  this.setBlockField(
895
1130
  blockId,
@@ -898,10 +1133,52 @@ export class ProjectMutator {
898
1133
  'NotReady',
899
1134
  );
900
1135
 
901
- if (newArgsAndUiState !== undefined) {
902
- // this will also reset all downstream stagings
903
- this.setStates([{ blockId, args: newArgsAndUiState.args, uiState: newArgsAndUiState.uiState }]);
1136
+ if (newClearState !== undefined) {
1137
+ // State is being reset - no migration needed
1138
+ const supportsStorageFromVM = newConfig.modelAPIVersion === 2;
1139
+
1140
+ if (supportsStorageFromVM) {
1141
+ // V2+: Get initial storage directly from VM and derive args from it
1142
+ const initialStorageJson = this.projectHelper.getInitialStorageInVM(newConfig);
1143
+ this.setBlockStorageRaw(blockId, initialStorageJson);
1144
+
1145
+ // Derive args from storage - only set currentArgs if derivation succeeds
1146
+ const deriveArgsResult = this.projectHelper.deriveArgsFromStorage(newConfig, initialStorageJson);
1147
+ if (!deriveArgsResult.error) {
1148
+ this.setBlockFieldObj(blockId, 'currentArgs', this.createJsonFieldValue(deriveArgsResult.value));
1149
+ // Derive prerunArgs from storage
1150
+ const prerunArgs = this.projectHelper.derivePrerunArgsFromStorage(newConfig, initialStorageJson);
1151
+ if (prerunArgs !== undefined) {
1152
+ this.setBlockFieldObj(blockId, 'currentPrerunArgs', this.createJsonFieldValue(prerunArgs));
1153
+ }
1154
+ }
1155
+ this.blocksWithChangedInputs.add(blockId);
1156
+ this.updateLastModified();
1157
+ } else {
1158
+ // V1: Use setStates with legacy state format
1159
+ this.setStates([{ modelAPIVersion: 1, blockId, state: newClearState.state }]);
1160
+ }
904
1161
  } else {
1162
+ // State is being preserved - run migrations if needed via VM
1163
+ // Only Model API v2 blocks support migrations
1164
+ const supportsStateMigrations = newConfig.modelAPIVersion === 2;
1165
+
1166
+ if (supportsStateMigrations) {
1167
+ const currentStorageJson = info.blockStorageJson;
1168
+
1169
+ const migrationResult = this.projectHelper.migrateStorageInVM(newConfig, currentStorageJson);
1170
+
1171
+ if (migrationResult.error !== undefined) {
1172
+ console.error(`[migrateBlockPack] Block ${blockId} migration error: ${migrationResult.error}`);
1173
+ } else {
1174
+ console.log(`[migrateBlockPack] Block ${blockId}: ${migrationResult.info}`);
1175
+ if (migrationResult.warn) {
1176
+ console.warn(`[migrateBlockPack] Block ${blockId} migration warning: ${migrationResult.warn}`);
1177
+ }
1178
+ this.setBlockStorageRaw(blockId, migrationResult.newStorageJson);
1179
+ }
1180
+ }
1181
+
905
1182
  // resetting staging outputs for all downstream blocks
906
1183
  this.getStagingGraph().traverse('downstream', [blockId], ({ id }) => this.resetStaging(id));
907
1184
  }
@@ -1023,12 +1300,17 @@ export class ProjectMutator {
1023
1300
  const stagingGraph = this.getStagingGraph();
1024
1301
  stagingGraph.nodes.forEach((node) => {
1025
1302
  const info = this.getBlockInfo(node.id);
1026
- let lag = info.stagingRendered ? 0 : 1;
1303
+ // Use requireStagingRendering to check both: staging exists AND prerunArgs hasn't changed
1304
+ const requiresRendering = info.requireStagingRendering;
1305
+ let lag = requiresRendering ? 1 : 0;
1027
1306
  node.upstream.forEach((upstream) => {
1028
1307
  const upstreamLag = lags.get(upstream)!;
1029
1308
  if (upstreamLag === 0) return;
1030
1309
  lag = Math.max(upstreamLag + 1, lag);
1031
1310
  });
1311
+ if (!requiresRendering && info.stagingRendered) {
1312
+ // console.log(`[traverseWithStagingLag] SKIP staging for ${node.id} - prerunArgs unchanged`);
1313
+ }
1032
1314
  cb(node.id, lag);
1033
1315
  lags.set(node.id, lag);
1034
1316
  });
@@ -1047,6 +1329,7 @@ export class ProjectMutator {
1047
1329
  // meaning staging already rendered
1048
1330
  return;
1049
1331
  if (lagThreshold === undefined || lag <= lagThreshold) {
1332
+ // console.log(`[refreshStagings] RENDER staging for ${blockId} (lag=${lag})`);
1050
1333
  this.renderStagingFor(blockId);
1051
1334
  rendered++;
1052
1335
  }
@@ -85,11 +85,13 @@ export class ResultPool {
85
85
  if (result !== undefined) return result;
86
86
  result = block.staging?.results?.get(exportName)?.spec;
87
87
  if (result !== undefined) return result;
88
- if (block.staging === undefined) this.ctx.markUnstable(`staging_not_rendered:${blockId}`);
89
- else if (!block.staging.locked) this.ctx.markUnstable(`staging_not_locked:${blockId}`);
88
+ // Note: Don't mark unstable when staging is absent - it may be intentionally skipped
89
+ // for v3 blocks with undefined prerunArgs.
90
+ if (block.staging !== undefined && !block.staging.locked)
91
+ this.ctx.markUnstable(`staging_not_locked:${blockId}`);
90
92
  else if (block.prod !== undefined && !block.prod.locked)
91
93
  this.ctx.markUnstable(`prod_not_locked:${blockId}`);
92
- // if prod is absent, returned undefined value is considered stable
94
+ // if neither prod nor staging is present, returned undefined value is considered stable
93
95
  return undefined;
94
96
  }
95
97
 
@@ -224,7 +226,10 @@ export class ResultPool {
224
226
  });
225
227
  exportsProcessed.add(exportName);
226
228
  }
227
- } else markUnstable(`staging_not_rendered:${blockId}`); // because staging will be inevitably rendered soon
229
+ }
230
+ // Note: Don't mark unstable when staging is absent - it may be intentionally skipped
231
+ // for v3 blocks with undefined prerunArgs. If prod exists, use it; if neither exists,
232
+ // the block simply has no specs to contribute.
228
233
 
229
234
  if (block.prod !== undefined) {
230
235
  if (!block.prod.locked) markUnstable(`prod_not_locked:${blockId}`);
@@ -296,9 +301,11 @@ export class ResultPool {
296
301
  field: projectFieldName(blockInfo.id, 'stagingCtx'),
297
302
  ignoreError: true,
298
303
  pureFieldErrorToUndefined: true,
304
+ stableIfNotFound: true,
299
305
  }) !== undefined,
300
306
  prj.traverseOrError({
301
307
  field: projectFieldName(blockInfo.id, 'stagingUiCtx'),
308
+ stableIfNotFound: true,
302
309
  }),
303
310
  );
304
311