@milaboratories/pl-middle-layer 1.49.0 → 1.50.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 (43) hide show
  1. package/dist/index.cjs +16 -2
  2. package/dist/index.d.ts +4 -2
  3. package/dist/index.js +4 -1
  4. package/dist/middle_layer/index.cjs +1 -0
  5. package/dist/middle_layer/index.d.ts +2 -1
  6. package/dist/middle_layer/index.js +1 -0
  7. package/dist/middle_layer/ops.cjs +1 -2
  8. package/dist/middle_layer/ops.cjs.map +1 -1
  9. package/dist/middle_layer/ops.d.ts +3 -6
  10. package/dist/middle_layer/ops.js +1 -2
  11. package/dist/middle_layer/ops.js.map +1 -1
  12. package/dist/middle_layer/project.cjs +1 -1
  13. package/dist/middle_layer/project.cjs.map +1 -1
  14. package/dist/middle_layer/project.js +1 -1
  15. package/dist/middle_layer/project.js.map +1 -1
  16. package/dist/middle_layer/project_list.d.ts +12 -0
  17. package/dist/model/index.d.ts +1 -1
  18. package/dist/model/project_model.cjs +4 -2
  19. package/dist/model/project_model.cjs.map +1 -1
  20. package/dist/model/project_model.d.ts +11 -2
  21. package/dist/model/project_model.js +4 -3
  22. package/dist/model/project_model.js.map +1 -1
  23. package/dist/mutator/block-pack/block_pack.d.ts +1 -1
  24. package/dist/mutator/migration.cjs +6 -2
  25. package/dist/mutator/migration.cjs.map +1 -1
  26. package/dist/mutator/migration.js +7 -3
  27. package/dist/mutator/migration.js.map +1 -1
  28. package/dist/mutator/project.cjs +94 -46
  29. package/dist/mutator/project.cjs.map +1 -1
  30. package/dist/mutator/project.d.ts +27 -0
  31. package/dist/mutator/project.js +96 -48
  32. package/dist/mutator/project.js.map +1 -1
  33. package/dist/mutator/template/render_block.cjs +1 -0
  34. package/dist/mutator/template/render_block.js +1 -1
  35. package/package.json +7 -7
  36. package/src/index.ts +3 -0
  37. package/src/middle_layer/index.ts +1 -0
  38. package/src/middle_layer/ops.ts +2 -8
  39. package/src/middle_layer/project.ts +1 -1
  40. package/src/model/index.ts +12 -0
  41. package/src/model/project_model.ts +4 -2
  42. package/src/mutator/migration.ts +13 -2
  43. package/src/mutator/project.ts +168 -74
@@ -74,7 +74,8 @@ export const ProjectResourceType: ResourceType = { name: "UserProject", version:
74
74
 
75
75
  export const SchemaVersionKey = "SchemaVersion";
76
76
  export const SchemaVersionV2 = "2";
77
- export const SchemaVersionCurrent = "3";
77
+ export const SchemaVersionV3 = "3";
78
+ export const SchemaVersionCurrent = "4";
78
79
 
79
80
  export const ProjectCreatedTimestamp = "ProjectCreated";
80
81
  export const ProjectLastModifiedTimestamp = "ProjectLastModified";
@@ -106,6 +107,7 @@ export interface ProjectField {
106
107
  | "prodArgs"
107
108
  | "currentArgs"
108
109
  | "currentPrerunArgs" // Derived args for staging/prerun rendering (from prerunArgs() or args())
110
+ | "prodChainCtx" // Pre-built production context chain node (accumulated prodCtx from all blocks above)
109
111
  | "prodCtx"
110
112
  | "prodUiCtx"
111
113
  | "prodOutput"
@@ -137,7 +139,7 @@ export function projectFieldName(blockId: string, fieldName: ProjectField["field
137
139
  }
138
140
 
139
141
  const projectFieldPattern =
140
- /^(?<blockId>.*)-(?<fieldName>blockPack|blockSettings|blockStorage|inputsValid|prodArgs|currentArgs|currentPrerunArgs|prodCtx|prodUiCtx|prodOutput|prodCtxPrevious|prodUiCtxPrevious|prodOutputPrevious|stagingCtx|stagingUiCtx|stagingOutput|stagingCtxPrevious|stagingUiCtxPrevious|stagingOutputPrevious)$/;
142
+ /^(?<blockId>.*)-(?<fieldName>blockPack|blockSettings|blockStorage|inputsValid|prodArgs|currentArgs|currentPrerunArgs|prodChainCtx|prodCtx|prodUiCtx|prodOutput|prodCtxPrevious|prodUiCtxPrevious|prodOutputPrevious|stagingCtx|stagingUiCtx|stagingOutput|stagingCtxPrevious|stagingUiCtxPrevious|stagingOutputPrevious)$/;
141
143
 
142
144
  export function parseProjectField(name: string): ProjectField | undefined {
143
145
  const match = name.match(projectFieldPattern);
@@ -6,6 +6,7 @@ import {
6
6
  SchemaVersionCurrent,
7
7
  SchemaVersionKey,
8
8
  SchemaVersionV2,
9
+ SchemaVersionV3,
9
10
  } from "../model/project_model";
10
11
  import { BlockFrontendStateKeyPrefixV1, SchemaVersionV1 } from "../model/project_model_v1";
11
12
  import { field, isNullResourceId } from "@milaboratories/pl-client";
@@ -31,8 +32,18 @@ export async function applyProjectMigrations(pl: PlClient, rid: ResourceId) {
31
32
 
32
33
  if (schemaVersion === SchemaVersionV2) {
33
34
  await migrateV2ToV3(tx, rid);
34
- } else if (schemaVersion !== SchemaVersionV1) {
35
- // If we got here and it's not v1 (which was handled above), it's unknown
35
+ schemaVersion = SchemaVersionV3;
36
+ }
37
+
38
+ if (schemaVersion === SchemaVersionV3) {
39
+ // V3 → V4: production context chain + staging re-render.
40
+ // The actual chain building and staging reset happens in fixProblemsAndMigrate()
41
+ // (called from ProjectMutator.load). This migration step just bumps the schema
42
+ // to prevent older clients from operating on the new project structure.
43
+ schemaVersion = SchemaVersionCurrent;
44
+ }
45
+
46
+ if (schemaVersion !== SchemaVersionCurrent) {
36
47
  throw new Error(`Unknown project schema version: ${schemaVersion}`);
37
48
  }
38
49
 
@@ -18,7 +18,11 @@ import {
18
18
  Pl,
19
19
  PlClient,
20
20
  } from "@milaboratories/pl-client";
21
- import { createRenderHeavyBlock, createBContextFromUpstreams } from "./template/render_block";
21
+ import {
22
+ createRenderHeavyBlock,
23
+ createBContextFromUpstreams,
24
+ createBContextEnd,
25
+ } from "./template/render_block";
22
26
  import type {
23
27
  Block,
24
28
  ProjectStructure,
@@ -32,6 +36,7 @@ import {
32
36
  projectFieldName,
33
37
  SchemaVersionCurrent,
34
38
  SchemaVersionKey,
39
+ SchemaVersionV3,
35
40
  ProjectResourceType,
36
41
  InitialBlockStructure,
37
42
  InitialProjectRenderingState,
@@ -298,6 +303,9 @@ export type ClearState = {
298
303
  };
299
304
 
300
305
  export class ProjectMutator {
306
+ /** Max number of blocks to render staging for in a single background refresh pass. */
307
+ private static readonly STAGING_REFRESH_MAX_BATCH = 10;
308
+
301
309
  private globalModCount = 0;
302
310
  private fieldsChanged: boolean = false;
303
311
 
@@ -379,6 +387,18 @@ export class ProjectMutator {
379
387
  }
380
388
  });
381
389
 
390
+ // Migration: build production context chain if not present,
391
+ // and reset all stagings so they re-render using the new chain
392
+ const needsChainBuild = [...this.blockInfos.values()].some(
393
+ (info) => info.fields.prodChainCtx === undefined,
394
+ );
395
+ if (needsChainBuild && this.blockInfos.size > 0) {
396
+ this.rebuildProdChain(0);
397
+ this.blockInfos.forEach((blockInfo) => {
398
+ this.resetStaging(blockInfo.id);
399
+ });
400
+ }
401
+
382
402
  // Validate after fixes
383
403
  this.blockInfos.forEach((info) => info.check());
384
404
  }
@@ -848,8 +868,18 @@ export class ProjectMutator {
848
868
  }
849
869
  }
850
870
 
851
- // resetting staging outputs for all downstream blocks
852
- this.getStagingGraph().traverse("downstream", changedArgs, ({ id }) => this.resetStaging(id));
871
+ // Render staging inline for blocks with changed prerunArgs — no downstream cascade.
872
+ // Each block's staging uses only the pre-built production context chain (prodChainCtx),
873
+ // which doesn't change on arg edits, so downstream blocks are unaffected.
874
+ for (const blockId of changedArgs) {
875
+ try {
876
+ this.renderStagingFor(blockId);
877
+ } catch (e) {
878
+ this.projectHelper.logger.error(
879
+ new Error(`[setStates] inline staging render failed for ${blockId}`, { cause: e }),
880
+ );
881
+ }
882
+ }
853
883
 
854
884
  if (somethingChanged) this.updateLastModified();
855
885
  }
@@ -870,22 +900,67 @@ export class ProjectMutator {
870
900
  return createBContextFromUpstreams(this.tx, upstreamContexts);
871
901
  }
872
902
 
873
- private createStagingCtx(upstream: Set<string>): AnyRef {
874
- const upstreamContexts: AnyRef[] = [];
875
- upstream.forEach((id) => {
876
- const info = this.getBlockInfo(id);
877
- if (info.fields["stagingCtx"]?.ref !== undefined) {
878
- upstreamContexts.push(Pl.unwrapHolder(this.tx, info.fields["stagingCtx"].ref));
879
- } else if (info.fields.currentPrerunArgs !== undefined) {
880
- // Upstream has currentPrerunArgs but no staging this is an inconsistency
881
- throw new Error(`Upstream ${id} staging is not rendered but has currentPrerunArgs set.`);
903
+ /**
904
+ * Rebuilds the production context chain from `fromBlockIndex` to the end.
905
+ * Each block gets a `prodChainCtx` field containing the accumulated production
906
+ * contexts from all blocks above it in project order.
907
+ *
908
+ * Construction rule for block at position K:
909
+ * - If block K-1 has prodCtx: chainNode[K] = BContext(chainNode[K-1], K-1.prodCtx)
910
+ * - If block K-1 has no prodCtx: chainNode[K] = chainNode[K-1] (passthrough)
911
+ * - Block 0: chainNode[0] = BContextEnd
912
+ */
913
+ private rebuildProdChain(fromBlockIndex: number = 0): void {
914
+ const blocks = [...allBlocks(this.struct)];
915
+ if (fromBlockIndex >= blocks.length) return;
916
+
917
+ // Get the inner chain context ref of the block before fromBlockIndex
918
+ let prevChainCtx: AnyRef | undefined;
919
+ if (fromBlockIndex > 0) {
920
+ const prevHolder = this.getBlockInfo(blocks[fromBlockIndex - 1].id).fields.prodChainCtx?.ref;
921
+ if (prevHolder === undefined) {
922
+ throw new Error(
923
+ `rebuildProdChain(${fromBlockIndex}): block ${blocks[fromBlockIndex - 1].id} at position ${fromBlockIndex - 1} has no prodChainCtx — chain must be built from 0 first`,
924
+ );
882
925
  }
883
- // Blocks without currentPrerunArgs (e.g. block model doesn't define prerunArgs, or args
884
- // derivation failed) never get stagingCtx. Use prodCtx if available.
885
- if (info.fields["prodCtx"]?.ref !== undefined)
886
- upstreamContexts.push(Pl.unwrapHolder(this.tx, info.fields["prodCtx"].ref));
887
- });
888
- return createBContextFromUpstreams(this.tx, upstreamContexts);
926
+ prevChainCtx = Pl.unwrapHolder(this.tx, prevHolder);
927
+ }
928
+
929
+ for (let i = fromBlockIndex; i < blocks.length; i++) {
930
+ const blockId = blocks[i].id;
931
+
932
+ let newChainCtx: AnyRef;
933
+
934
+ if (i === 0) {
935
+ // First block: nothing above
936
+ newChainCtx = createBContextEnd(this.tx);
937
+ } else {
938
+ const prevBlockId = blocks[i - 1].id;
939
+ const prevInfo = this.getBlockInfo(prevBlockId);
940
+ const prevProdCtxHolder = prevInfo.fields.prodCtx?.ref;
941
+
942
+ if (prevProdCtxHolder !== undefined) {
943
+ // Block above has production: accumulate into chain
944
+ const upstreams: AnyRef[] = [];
945
+ upstreams.push(prevChainCtx!);
946
+ upstreams.push(Pl.unwrapHolder(this.tx, prevProdCtxHolder));
947
+ newChainCtx = createBContextFromUpstreams(this.tx, upstreams);
948
+ } else {
949
+ // Passthrough: reuse inner context from previous block
950
+ newChainCtx = prevChainCtx!;
951
+ }
952
+ }
953
+
954
+ // Store chain node (wrapped in holder)
955
+ this.setBlockField(
956
+ blockId,
957
+ "prodChainCtx",
958
+ Pl.wrapInEphHolder(this.tx, newChainCtx),
959
+ "NotReady",
960
+ );
961
+
962
+ prevChainCtx = newChainCtx;
963
+ }
889
964
  }
890
965
 
891
966
  private exportCtx(ctx: AnyRef): AnyRef {
@@ -899,10 +974,7 @@ export class ProjectMutator {
899
974
  private renderStagingFor(blockId: string) {
900
975
  const info = this.getBlockInfo(blockId);
901
976
 
902
- // Check BEFORE resetStaging: if currentPrerunArgs is not set (e.g. prerunArgs() returned undefined
903
- // because inputs aren't ready, or args derivation failed), skip without clearing existing staging.
904
- // Otherwise resetStaging would delete stagingCtx, and downstream blocks that reference this block
905
- // as an upstream would fail in createStagingCtx.
977
+ // Skip if currentPrerunArgs is not set (prerunArgs() returned undefined or args derivation failed)
906
978
  const prerunArgsRef = info.fields.currentPrerunArgs?.ref;
907
979
  if (prerunArgsRef === undefined) {
908
980
  return;
@@ -910,7 +982,14 @@ export class ProjectMutator {
910
982
 
911
983
  this.resetStaging(blockId);
912
984
 
913
- const ctx = this.createStagingCtx(this.getStagingGraph().nodes.get(blockId)!.upstream);
985
+ // Use the pre-built production context chain node
986
+ const chainCtxHolder = info.fields.prodChainCtx?.ref;
987
+ if (chainCtxHolder === undefined) {
988
+ throw new Error(
989
+ `[renderStagingFor] block ${blockId} has no prodChainCtx — chain must be built before staging render`,
990
+ );
991
+ }
992
+ const ctx = Pl.unwrapHolder(this.tx, chainCtxHolder);
914
993
 
915
994
  if (this.getBlock(blockId).renderingMode !== "Heavy") throw new Error("not supported yet");
916
995
 
@@ -1174,6 +1253,19 @@ export class ProjectMutator {
1174
1253
  this.pendingProductionGraph = undefined;
1175
1254
  this.actualProductionGraph = undefined;
1176
1255
 
1256
+ // Rebuild production chain — structure (and thus block order) may have changed.
1257
+ // Find the first position that changed to avoid rebuilding the entire chain.
1258
+ const newBlocks = [...allBlocks(newStructure)];
1259
+ const oldBlocks = [...currentStagingGraph.nodes.keys()];
1260
+ const n = Math.min(newBlocks.length, oldBlocks.length);
1261
+ let firstChangedIdx = 0;
1262
+ while (firstChangedIdx < n && newBlocks[firstChangedIdx].id === oldBlocks[firstChangedIdx]) {
1263
+ firstChangedIdx++;
1264
+ }
1265
+ if (firstChangedIdx < newBlocks.length) {
1266
+ this.rebuildProdChain(firstChangedIdx);
1267
+ }
1268
+
1177
1269
  this.updateLastModified();
1178
1270
  }
1179
1271
 
@@ -1356,9 +1448,6 @@ export class ProjectMutator {
1356
1448
  }
1357
1449
 
1358
1450
  this.blocksWithChangedInputs.add(blockId);
1359
-
1360
- // resetting staging outputs for all downstream blocks
1361
- this.getStagingGraph().traverse("downstream", [blockId], ({ id }) => this.resetStaging(id));
1362
1451
  }
1363
1452
 
1364
1453
  // also reset or limbo all downstream productions
@@ -1367,6 +1456,17 @@ export class ProjectMutator {
1367
1456
  this.resetOrLimboProduction(id),
1368
1457
  );
1369
1458
 
1459
+ // Rebuild chain from this block onward (production may have been reset/limboed)
1460
+ // and reset staging for the migrated block + everything below
1461
+ const blocksList = [...allBlocks(this.struct)];
1462
+ const blockIdx = blocksList.findIndex((b) => b.id === blockId);
1463
+ if (blockIdx >= 0) {
1464
+ this.rebuildProdChain(blockIdx + 1);
1465
+ for (let i = blockIdx; i < blocksList.length; i++) {
1466
+ this.resetStaging(blocksList[i].id);
1467
+ }
1468
+ }
1469
+
1370
1470
  this.updateLastModified();
1371
1471
  }
1372
1472
 
@@ -1424,11 +1524,20 @@ export class ProjectMutator {
1424
1524
  this.resetOrLimboProduction(node.id);
1425
1525
  });
1426
1526
 
1427
- // resetting staging outputs for all downstream blocks
1428
- this.getStagingGraph().traverse("downstream", renderedArray, ({ id }) => {
1429
- // don't reset staging of the first rendered block
1430
- if (renderedArray[0] !== id) this.resetStaging(id);
1431
- });
1527
+ // Rebuild production chain and reset downstream staging
1528
+ if (rendered.size > 0) {
1529
+ const blocksList = [...allBlocks(this.struct)];
1530
+ const firstRenderedIdx = blocksList.findIndex((b) => rendered.has(b.id));
1531
+ if (firstRenderedIdx >= 0) {
1532
+ // Chain from firstRenderedIdx+1 onward changes (rendered blocks have new prodCtx)
1533
+ this.rebuildProdChain(firstRenderedIdx + 1);
1534
+ // Reset staging for all blocks after the first rendered block
1535
+ // (their chain node changed; the first rendered block's chain is unaffected)
1536
+ for (let i = firstRenderedIdx + 1; i < blocksList.length; i++) {
1537
+ this.resetStaging(blocksList[i].id);
1538
+ }
1539
+ }
1540
+ }
1432
1541
 
1433
1542
  if (rendered.size > 0) this.updateLastModified();
1434
1543
 
@@ -1470,54 +1579,39 @@ export class ProjectMutator {
1470
1579
  for (const blockId of activeProdGraph.traverseIdsExcludingRoots("downstream", ...stopped))
1471
1580
  this.resetOrLimboProduction(blockId);
1472
1581
 
1473
- // reset staging outputs for all downstream blocks
1474
- this.getStagingGraph().traverse("downstream", stopped, ({ id }) => this.resetStaging(id));
1475
- }
1476
-
1477
- private traverseWithStagingLag(cb: (blockId: string, lag: number) => void) {
1478
- const lags = new Map<string, number>();
1479
- const stagingGraph = this.getStagingGraph();
1480
- stagingGraph.nodes.forEach((node) => {
1481
- const info = this.getBlockInfo(node.id);
1482
- // Use requireStagingRendering to check both: staging exists AND prerunArgs hasn't changed
1483
- const requiresRendering = info.requireStagingRendering;
1484
- let lag = requiresRendering ? 1 : 0;
1485
- node.upstream.forEach((upstream) => {
1486
- const upstreamLag = lags.get(upstream)!;
1487
- if (upstreamLag === 0) return;
1488
- lag = Math.max(upstreamLag + 1, lag);
1489
- });
1490
- if (!requiresRendering && info.stagingRendered) {
1491
- // console.log(`[traverseWithStagingLag] SKIP staging for ${node.id} - prerunArgs unchanged`);
1582
+ // Rebuild chain and reset staging for stopped blocks and everything below
1583
+ if (stopped.length > 0) {
1584
+ const blocksList = [...allBlocks(this.struct)];
1585
+ const stoppedSet = new Set(stopped);
1586
+ const firstStoppedIdx = blocksList.findIndex((b) => stoppedSet.has(b.id));
1587
+ if (firstStoppedIdx >= 0) {
1588
+ // Stopped blocks lost prodCtx — chain below them changes
1589
+ this.rebuildProdChain(firstStoppedIdx + 1);
1590
+ for (let i = firstStoppedIdx; i < blocksList.length; i++) {
1591
+ this.resetStaging(blocksList[i].id);
1592
+ }
1492
1593
  }
1493
- cb(node.id, lag);
1494
- lags.set(node.id, lag);
1495
- });
1594
+ }
1496
1595
  }
1497
1596
 
1498
- /** @param stagingRenderingRate rate in blocks per second */
1499
- private refreshStagings(stagingRenderingRate?: number) {
1500
- const elapsed = Date.now() - this.renderingState.stagingRefreshTimestamp;
1501
- const lagThreshold =
1502
- stagingRenderingRate === undefined
1503
- ? undefined
1504
- : 1 + Math.max(0, (elapsed * stagingRenderingRate) / 1000);
1597
+ /** Renders staging for blocks that need it (have currentPrerunArgs but no staging). */
1598
+ private refreshStagings() {
1599
+ const maxBatch = ProjectMutator.STAGING_REFRESH_MAX_BATCH;
1505
1600
  let rendered = 0;
1506
- this.traverseWithStagingLag((blockId, lag) => {
1507
- if (lag === 0)
1508
- // meaning staging already rendered
1509
- return;
1510
- if (lagThreshold === undefined || lag <= lagThreshold) {
1601
+ for (const block of allBlocks(this.struct)) {
1602
+ if (rendered >= maxBatch) break;
1603
+ const info = this.getBlockInfo(block.id);
1604
+ if (info.requireStagingRendering) {
1511
1605
  try {
1512
- this.renderStagingFor(blockId);
1606
+ this.renderStagingFor(block.id);
1513
1607
  rendered++;
1514
1608
  } catch (e) {
1515
1609
  this.projectHelper.logger.error(
1516
- new Error(`[refreshStagings] renderStagingFor failed for ${blockId}`, { cause: e }),
1610
+ new Error(`[refreshStagings] renderStagingFor failed for ${block.id}`, { cause: e }),
1517
1611
  );
1518
1612
  }
1519
1613
  }
1520
- });
1614
+ }
1521
1615
  if (rendered > 0) this.resetStagingRefreshTimestamp();
1522
1616
  }
1523
1617
 
@@ -1536,9 +1630,9 @@ export class ProjectMutator {
1536
1630
  // Maintenance
1537
1631
  //
1538
1632
 
1539
- /** @param stagingRenderingRate rate in blocks per second */
1540
- public doRefresh(stagingRenderingRate?: number) {
1541
- this.refreshStagings(stagingRenderingRate);
1633
+ /** Background maintenance: render pending stagings + GC of Previous fields. */
1634
+ public doRefresh() {
1635
+ this.refreshStagings();
1542
1636
  this.blockInfos.forEach((blockInfo) => {
1543
1637
  if (
1544
1638
  blockInfo.fields.prodCtx?.status === "Ready" &&
@@ -1847,13 +1941,13 @@ export async function duplicateProject(
1847
1941
  const sourceData = await sourceDataP;
1848
1942
  const sourceKVs = await sourceKVsP;
1849
1943
 
1850
- // Validate schema version
1944
+ // Validate schema version (accept current and previous version that can be migrated on open)
1851
1945
  const schemaKV = sourceKVs.find((kv) => kv.key === SchemaVersionKey);
1852
1946
  const schema = schemaKV ? JSON.parse(schemaKV.value) : undefined;
1853
- if (schema !== SchemaVersionCurrent) {
1947
+ if (schema !== SchemaVersionCurrent && schema !== SchemaVersionV3) {
1854
1948
  throw new UiError(
1855
1949
  `Cannot duplicate project with schema version ${schema ?? "unknown"}. ` +
1856
- `Only schema version ${SchemaVersionCurrent} is supported. ` +
1950
+ `Only schema versions ${SchemaVersionV3} and ${SchemaVersionCurrent} are supported. ` +
1857
1951
  `Try opening the project first to trigger migration.`,
1858
1952
  );
1859
1953
  }