@milaboratories/pl-middle-layer 1.74.19 → 1.76.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.
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -2
- package/dist/middle_layer/block.cjs.map +1 -1
- package/dist/middle_layer/block.js.map +1 -1
- package/dist/middle_layer/block_action_context.cjs +57 -0
- package/dist/middle_layer/block_action_context.cjs.map +1 -0
- package/dist/middle_layer/block_action_context.d.ts +47 -0
- package/dist/middle_layer/block_action_context.d.ts.map +1 -0
- package/dist/middle_layer/block_action_context.js +57 -0
- package/dist/middle_layer/block_action_context.js.map +1 -0
- package/dist/middle_layer/build_stamp.cjs +1 -1
- package/dist/middle_layer/build_stamp.js +1 -1
- package/dist/middle_layer/column_links.cjs +106 -0
- package/dist/middle_layer/column_links.cjs.map +1 -0
- package/dist/middle_layer/column_links.js +104 -0
- package/dist/middle_layer/column_links.js.map +1 -0
- package/dist/middle_layer/index.d.ts +2 -1
- package/dist/middle_layer/middle_layer.cjs +1 -0
- package/dist/middle_layer/middle_layer.cjs.map +1 -1
- package/dist/middle_layer/middle_layer.js +1 -0
- package/dist/middle_layer/middle_layer.js.map +1 -1
- package/dist/middle_layer/project.cjs +53 -19
- package/dist/middle_layer/project.cjs.map +1 -1
- package/dist/middle_layer/project.d.ts +6 -0
- package/dist/middle_layer/project.d.ts.map +1 -1
- package/dist/middle_layer/project.js +54 -20
- package/dist/middle_layer/project.js.map +1 -1
- package/dist/middle_layer/project_overview.cjs +56 -24
- package/dist/middle_layer/project_overview.cjs.map +1 -1
- package/dist/middle_layer/project_overview.js +55 -25
- package/dist/middle_layer/project_overview.js.map +1 -1
- package/dist/model/args_derivation.cjs +153 -0
- package/dist/model/args_derivation.cjs.map +1 -0
- package/dist/model/args_derivation.d.ts +21 -0
- package/dist/model/args_derivation.d.ts.map +1 -0
- package/dist/model/args_derivation.js +148 -0
- package/dist/model/args_derivation.js.map +1 -0
- package/dist/model/project_helper.cjs +34 -8
- package/dist/model/project_helper.cjs.map +1 -1
- package/dist/model/project_helper.d.ts +18 -5
- package/dist/model/project_helper.d.ts.map +1 -1
- package/dist/model/project_helper.js +34 -8
- package/dist/model/project_helper.js.map +1 -1
- package/dist/model/project_model.cjs +2 -1
- package/dist/model/project_model.cjs.map +1 -1
- package/dist/model/project_model.d.ts +1 -1
- package/dist/model/project_model.js +2 -1
- package/dist/model/project_model.js.map +1 -1
- package/dist/mutator/project.cjs +73 -12
- package/dist/mutator/project.cjs.map +1 -1
- package/dist/mutator/project.d.ts +1 -0
- package/dist/mutator/project.d.ts.map +1 -1
- package/dist/mutator/project.js +73 -12
- package/dist/mutator/project.js.map +1 -1
- package/dist/mutator/template/render_block.cjs +35 -2
- package/dist/mutator/template/render_block.cjs.map +1 -1
- package/dist/mutator/template/render_block.js +35 -4
- package/dist/mutator/template/render_block.js.map +1 -1
- package/dist/mutator/template/render_template.cjs +16 -1
- package/dist/mutator/template/render_template.cjs.map +1 -1
- package/dist/mutator/template/render_template.d.ts +12 -1
- package/dist/mutator/template/render_template.d.ts.map +1 -1
- package/dist/mutator/template/render_template.js +16 -2
- package/dist/mutator/template/render_template.js.map +1 -1
- package/package.json +16 -16
- package/src/middle_layer/block.ts +4 -1
- package/src/middle_layer/block_action_context.test.ts +148 -0
- package/src/middle_layer/block_action_context.ts +162 -0
- package/src/middle_layer/column_links.test.ts +350 -0
- package/src/middle_layer/column_links.ts +152 -0
- package/src/middle_layer/index.ts +6 -0
- package/src/middle_layer/middle_layer.ts +2 -0
- package/src/middle_layer/project.ts +52 -5
- package/src/middle_layer/project_overview.test.ts +200 -0
- package/src/middle_layer/project_overview.ts +82 -28
- package/src/model/args.test.ts +39 -1
- package/src/model/args_derivation.test.ts +326 -0
- package/src/model/args_derivation.ts +194 -0
- package/src/model/project_helper.ts +55 -25
- package/src/model/project_model.ts +7 -2
- package/src/mutator/project-v3.test.ts +105 -1
- package/src/mutator/project.ts +128 -37
- package/src/mutator/project_action_context.test.ts +178 -0
- package/src/mutator/template/render_block.ts +70 -5
- package/src/mutator/template/render_template.ts +22 -2
- package/src/test/fake_project.ts +222 -0
|
@@ -110,6 +110,7 @@ export interface ProjectField {
|
|
|
110
110
|
| "prodArgs"
|
|
111
111
|
| "currentArgs"
|
|
112
112
|
| "currentPrerunArgs" // Derived args for staging/prerun rendering (from prerunArgs() or args())
|
|
113
|
+
| "inputsValid" // Model API v2: errors found in currentArgs, none when the block may run, with the fingerprint of those args; present while currentArgs are
|
|
113
114
|
| "prodChainCtx" // Pre-built production context chain node (accumulated prodCtx from all blocks above)
|
|
114
115
|
| "prodCtx"
|
|
115
116
|
| "prodUiCtx"
|
|
@@ -122,7 +123,10 @@ export interface ProjectField {
|
|
|
122
123
|
| "stagingOutput"
|
|
123
124
|
| "stagingCtxPrevious"
|
|
124
125
|
| "stagingUiCtxPrevious"
|
|
125
|
-
| "stagingOutputPrevious"
|
|
126
|
+
| "stagingOutputPrevious"
|
|
127
|
+
// Root action context of the current render. The project holds it so it outlives the render.
|
|
128
|
+
| "prodActionContext"
|
|
129
|
+
| "stagingActionContext";
|
|
126
130
|
}
|
|
127
131
|
|
|
128
132
|
export const FieldsToDuplicate: Set<ProjectField["fieldName"]> = new Set([
|
|
@@ -132,6 +136,7 @@ export const FieldsToDuplicate: Set<ProjectField["fieldName"]> = new Set([
|
|
|
132
136
|
"prodArgs",
|
|
133
137
|
"currentArgs",
|
|
134
138
|
"currentPrerunArgs",
|
|
139
|
+
"inputsValid",
|
|
135
140
|
"prodCtx",
|
|
136
141
|
"prodUiCtx",
|
|
137
142
|
"prodOutput",
|
|
@@ -142,7 +147,7 @@ export function projectFieldName(blockId: string, fieldName: ProjectField["field
|
|
|
142
147
|
}
|
|
143
148
|
|
|
144
149
|
const projectFieldPattern =
|
|
145
|
-
/^(?<blockId>.*)-(?<fieldName>blockPack|blockSettings|blockStorage|inputsValid|prodArgs|currentArgs|currentPrerunArgs|prodChainCtx|prodCtx|prodUiCtx|prodOutput|prodCtxPrevious|prodUiCtxPrevious|prodOutputPrevious|stagingCtx|stagingUiCtx|stagingOutput|stagingCtxPrevious|stagingUiCtxPrevious|stagingOutputPrevious)$/;
|
|
150
|
+
/^(?<blockId>.*)-(?<fieldName>blockPack|blockSettings|blockStorage|inputsValid|prodArgs|currentArgs|currentPrerunArgs|prodChainCtx|prodCtx|prodUiCtx|prodOutput|prodCtxPrevious|prodUiCtxPrevious|prodOutputPrevious|stagingCtx|stagingUiCtx|stagingOutput|stagingCtxPrevious|stagingUiCtxPrevious|stagingOutputPrevious|prodActionContext|stagingActionContext)$/;
|
|
146
151
|
|
|
147
152
|
export function parseProjectField(name: string): ProjectField | undefined {
|
|
148
153
|
const match = name.match(projectFieldPattern);
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { field, poll, TestHelpers, toGlobalResourceId } from "@milaboratories/pl-client";
|
|
1
|
+
import { field, Pl, poll, TestHelpers, toGlobalResourceId } from "@milaboratories/pl-client";
|
|
2
|
+
import { canonicalJsonBytes } from "@milaboratories/ts-helpers";
|
|
2
3
|
import { getQuickJS } from "quickjs-emscripten";
|
|
3
4
|
import { expect, test } from "vitest";
|
|
4
5
|
import { outputRef } from "../model/args";
|
|
6
|
+
import { argsFingerprint } from "../model/args_derivation";
|
|
5
7
|
import { ProjectHelper } from "../model/project_helper";
|
|
6
8
|
import { blockArgsAuthorKey, projectFieldName } from "../model/project_model";
|
|
7
9
|
import { TestBPPreparer } from "../test/block_packs";
|
|
@@ -622,3 +624,105 @@ test("v3 blocks: migrateBlockPack does not crash when args() returns undefined",
|
|
|
622
624
|
});
|
|
623
625
|
});
|
|
624
626
|
});
|
|
627
|
+
|
|
628
|
+
test("v3 blocks: every args derivation records the errors found in the args, none when there are none", async () => {
|
|
629
|
+
const quickJs = await getQuickJS();
|
|
630
|
+
|
|
631
|
+
await TestHelpers.withTempRoot(async (pl) => {
|
|
632
|
+
const prj = await pl.withWriteTx("CreatingProject", async (tx) => {
|
|
633
|
+
const prjRef = await createProject(tx);
|
|
634
|
+
tx.createField(field(tx.clientRoot, "prj"), "Dynamic", prjRef);
|
|
635
|
+
await tx.commit();
|
|
636
|
+
return await toGlobalResourceId(prjRef);
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
await pl.withWriteTx("AddBlock", async (tx) => {
|
|
640
|
+
const mut = await ProjectMutator.load(new ProjectHelper(quickJs), tx, prj);
|
|
641
|
+
mut.addBlock(
|
|
642
|
+
{ id: "enter1", label: "Enter Numbers V3", renderingMode: "Heavy" },
|
|
643
|
+
{ storageMode: "fromModel", blockPack: await TestBPPreparer.prepare(BPSpecEnterV3) },
|
|
644
|
+
);
|
|
645
|
+
mut.setStates([
|
|
646
|
+
{
|
|
647
|
+
modelAPIVersion: 2,
|
|
648
|
+
blockId: "enter1",
|
|
649
|
+
payload: { operation: "update-block-data", value: { numbers: [3, 1, 2] } },
|
|
650
|
+
},
|
|
651
|
+
]);
|
|
652
|
+
mut.save();
|
|
653
|
+
await tx.commit();
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
await poll(pl, async (tx) => {
|
|
657
|
+
const prjR = await tx.get(prj);
|
|
658
|
+
const record = await prjR.get(projectFieldName("enter1", "inputsValid"));
|
|
659
|
+
expect(jsonOf(record.data.data)).toStrictEqual({
|
|
660
|
+
argsFingerprint: argsFingerprint({ numbers: [1, 2, 3] }),
|
|
661
|
+
errors: [],
|
|
662
|
+
});
|
|
663
|
+
});
|
|
664
|
+
});
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
test("v3 blocks: args without an args errors record of their own are kept and run, as before the record", async () => {
|
|
668
|
+
const quickJs = await getQuickJS();
|
|
669
|
+
|
|
670
|
+
await TestHelpers.withTempRoot(async (pl) => {
|
|
671
|
+
const prj = await pl.withWriteTx("CreatingProject", async (tx) => {
|
|
672
|
+
const prjRef = await createProject(tx);
|
|
673
|
+
tx.createField(field(tx.clientRoot, "prj"), "Dynamic", prjRef);
|
|
674
|
+
await tx.commit();
|
|
675
|
+
return await toGlobalResourceId(prjRef);
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
await pl.withWriteTx("AddBlock", async (tx) => {
|
|
679
|
+
const mut = await ProjectMutator.load(new ProjectHelper(quickJs), tx, prj);
|
|
680
|
+
mut.addBlock(
|
|
681
|
+
{ id: "enter1", label: "Enter Numbers V3", renderingMode: "Heavy" },
|
|
682
|
+
{ storageMode: "fromModel", blockPack: await TestBPPreparer.prepare(BPSpecEnterV3) },
|
|
683
|
+
);
|
|
684
|
+
mut.setStates([
|
|
685
|
+
{
|
|
686
|
+
modelAPIVersion: 2,
|
|
687
|
+
blockId: "enter1",
|
|
688
|
+
payload: { operation: "update-block-data", value: { numbers: [3, 1, 2] } },
|
|
689
|
+
},
|
|
690
|
+
]);
|
|
691
|
+
mut.save();
|
|
692
|
+
await tx.commit();
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
// What a middle layer that does not record args errors leaves behind: current args it wrote
|
|
696
|
+
// itself, and no record of their args errors.
|
|
697
|
+
await pl.withWriteTx("WriteArgsWithoutRecord", async (tx) => {
|
|
698
|
+
tx.removeField(field(prj, projectFieldName("enter1", "inputsValid")));
|
|
699
|
+
tx.setField(
|
|
700
|
+
field(prj, projectFieldName("enter1", "currentArgs")),
|
|
701
|
+
tx.createValue(Pl.JsonObject, canonicalJsonBytes({ numbers: [9] })),
|
|
702
|
+
);
|
|
703
|
+
await tx.commit();
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
await pl.withWriteTx("Run", async (tx) => {
|
|
707
|
+
const mut = await ProjectMutator.load(new ProjectHelper(quickJs), tx, prj);
|
|
708
|
+
mut.renderProduction(["enter1"]);
|
|
709
|
+
mut.save();
|
|
710
|
+
await tx.commit();
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
await poll(pl, async (tx) => {
|
|
714
|
+
const prjR = await tx.get(prj);
|
|
715
|
+
const currentArgs = await prjR.get(projectFieldName("enter1", "currentArgs"));
|
|
716
|
+
expect(jsonOf(currentArgs.data.data)).toStrictEqual({ numbers: [9] });
|
|
717
|
+
const prodArgs = await prjR.get(projectFieldName("enter1", "prodArgs"));
|
|
718
|
+
expect(jsonOf(prodArgs.data.data)).toStrictEqual({ numbers: [9] });
|
|
719
|
+
});
|
|
720
|
+
});
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
// Internals
|
|
724
|
+
|
|
725
|
+
function jsonOf(data: Uint8Array | undefined): unknown {
|
|
726
|
+
if (data === undefined) throw new Error("The field holds no data");
|
|
727
|
+
return JSON.parse(Buffer.from(data).toString());
|
|
728
|
+
}
|
package/src/mutator/project.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from "@milaboratories/pl-client";
|
|
23
23
|
import {
|
|
24
24
|
createRenderHeavyBlock,
|
|
25
|
+
type BlockActionContextBaggage,
|
|
25
26
|
createBContextFromUpstreams,
|
|
26
27
|
createBContextEnd,
|
|
27
28
|
} from "./template/render_block";
|
|
@@ -77,6 +78,14 @@ import {
|
|
|
77
78
|
} from "@milaboratories/ts-helpers";
|
|
78
79
|
import { gzipSync } from "node:zlib";
|
|
79
80
|
import type { ProjectHelper } from "../model/project_helper";
|
|
81
|
+
import {
|
|
82
|
+
derivedArgsValue,
|
|
83
|
+
argsErrorsFieldChange,
|
|
84
|
+
argsErrorsOfBlock,
|
|
85
|
+
type DerivedArgs,
|
|
86
|
+
type ArgsErrorsFieldChange,
|
|
87
|
+
} from "../model/args_derivation";
|
|
88
|
+
import type { ArgsIssue } from "@milaboratories/pl-model-common";
|
|
80
89
|
import {
|
|
81
90
|
extractConfig,
|
|
82
91
|
UiError,
|
|
@@ -273,6 +282,24 @@ class BlockInfo {
|
|
|
273
282
|
return this.currentPrerunArgsC();
|
|
274
283
|
}
|
|
275
284
|
|
|
285
|
+
private readonly argsErrorsC = cached(
|
|
286
|
+
() => `${this.fields.inputsValid?.modCount}:${this.fields.currentArgs?.modCount}`,
|
|
287
|
+
() =>
|
|
288
|
+
argsErrorsOfBlock({
|
|
289
|
+
modelAPIVersion: this.config.modelAPIVersion,
|
|
290
|
+
inputsValid: this.fields.inputsValid,
|
|
291
|
+
currentArgs: this.currentArgs,
|
|
292
|
+
}),
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* The errors the block's `argsIssues()` found in its current args. A block whose model API
|
|
297
|
+
* version has no args issues, or that has no current args, has none.
|
|
298
|
+
*/
|
|
299
|
+
get argsErrors(): readonly ArgsIssue[] {
|
|
300
|
+
return this.argsErrorsC();
|
|
301
|
+
}
|
|
302
|
+
|
|
276
303
|
get stagingRendered(): boolean {
|
|
277
304
|
return this.fields.stagingCtx !== undefined;
|
|
278
305
|
}
|
|
@@ -706,6 +733,15 @@ export class ProjectMutator {
|
|
|
706
733
|
return { ref, value: jsonBytes, status: "Ready" };
|
|
707
734
|
}
|
|
708
735
|
|
|
736
|
+
/** Baggage of the root action context of a block render. */
|
|
737
|
+
private blockActionContextBaggage(blockId: string, info: BlockInfo): BlockActionContextBaggage {
|
|
738
|
+
return {
|
|
739
|
+
name: this.getBlock(blockId).label,
|
|
740
|
+
blockPack: JSON.stringify(info.source),
|
|
741
|
+
blockId,
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
|
|
709
745
|
private getBlock(blockId: string): Block {
|
|
710
746
|
for (const block of allBlocks(this.struct)) if (block.id === blockId) return block;
|
|
711
747
|
throw new Error("block not found");
|
|
@@ -756,6 +792,32 @@ export class ProjectMutator {
|
|
|
756
792
|
return deleted;
|
|
757
793
|
}
|
|
758
794
|
|
|
795
|
+
/**
|
|
796
|
+
* Brings the block's `inputsValid` field in line with what its args derivation found: the run
|
|
797
|
+
* errors of its args, none when it may run with them, and no field when it has no args.
|
|
798
|
+
*/
|
|
799
|
+
private setArgsErrors(blockId: string, derivedArgs: DerivedArgs): void {
|
|
800
|
+
this.applyArgsErrorsChange(
|
|
801
|
+
blockId,
|
|
802
|
+
argsErrorsFieldChange(this.getBlockInfo(blockId).fields.inputsValid, derivedArgs),
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
private applyArgsErrorsChange(blockId: string, change: ArgsErrorsFieldChange): void {
|
|
807
|
+
switch (change.type) {
|
|
808
|
+
case "keep":
|
|
809
|
+
return;
|
|
810
|
+
case "delete":
|
|
811
|
+
this.deleteBlockFields(blockId, "inputsValid");
|
|
812
|
+
return;
|
|
813
|
+
case "write": {
|
|
814
|
+
const ref = this.tx.createValue(Pl.JsonObject, change.data);
|
|
815
|
+
this.setBlockField(blockId, "inputsValid", ref, "Ready", change.data);
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
759
821
|
private updateLastModified() {
|
|
760
822
|
this.lastModified = Date.now();
|
|
761
823
|
this.lastModifiedChanged = true;
|
|
@@ -782,6 +844,7 @@ export class ProjectMutator {
|
|
|
782
844
|
this.setBlockFieldObj(blockId, "stagingCtxPrevious", fields.stagingCtx!);
|
|
783
845
|
this.setBlockFieldObj(blockId, "stagingUiCtxPrevious", fields.stagingUiCtx!);
|
|
784
846
|
}
|
|
847
|
+
this.deleteBlockFields(blockId, "stagingActionContext");
|
|
785
848
|
if (this.deleteBlockFields(blockId, "stagingOutput", "stagingCtx", "stagingUiCtx"))
|
|
786
849
|
this.resetStagingRefreshTimestamp();
|
|
787
850
|
}
|
|
@@ -797,7 +860,14 @@ export class ProjectMutator {
|
|
|
797
860
|
this.setBlockFieldObj(blockId, "prodCtxPrevious", fields.prodCtx!);
|
|
798
861
|
this.setBlockFieldObj(blockId, "prodUiCtxPrevious", fields.prodUiCtx!);
|
|
799
862
|
}
|
|
800
|
-
this.deleteBlockFields(
|
|
863
|
+
this.deleteBlockFields(
|
|
864
|
+
blockId,
|
|
865
|
+
"prodOutput",
|
|
866
|
+
"prodCtx",
|
|
867
|
+
"prodUiCtx",
|
|
868
|
+
"prodArgs",
|
|
869
|
+
"prodActionContext",
|
|
870
|
+
);
|
|
801
871
|
}
|
|
802
872
|
|
|
803
873
|
/** Running blocks are reset, settled ones (Ready or finished Error) moved to limbo. Returns if
|
|
@@ -826,6 +896,7 @@ export class ProjectMutator {
|
|
|
826
896
|
return true;
|
|
827
897
|
} else {
|
|
828
898
|
// reset - clean up any partial/inconsistent production stat
|
|
899
|
+
this.deleteBlockFields(blockId, "prodActionContext");
|
|
829
900
|
return this.deleteBlockFields(
|
|
830
901
|
blockId,
|
|
831
902
|
"prodOutput",
|
|
@@ -908,21 +979,16 @@ export class ProjectMutator {
|
|
|
908
979
|
}
|
|
909
980
|
|
|
910
981
|
// Derive args from storage - set or clear currentArgs based on derivation result
|
|
911
|
-
const
|
|
912
|
-
blockConfig,
|
|
913
|
-
initialStorageJson,
|
|
914
|
-
);
|
|
982
|
+
const derivedArgs = this.projectHelper.deriveArgsFromStorage(blockConfig, initialStorageJson);
|
|
915
983
|
// args() legitimately returns undefined when inputs are incomplete/invalid;
|
|
916
984
|
// treat that like a derivation failure and clear the field (never JSON-encode undefined).
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
"currentArgs",
|
|
921
|
-
this.createJsonFieldValue(deriveArgsResult.value),
|
|
922
|
-
);
|
|
985
|
+
const args = derivedArgsValue(derivedArgs);
|
|
986
|
+
if (args !== undefined) {
|
|
987
|
+
this.setBlockFieldObj(blockId, "currentArgs", this.createJsonFieldValue(args));
|
|
923
988
|
} else {
|
|
924
989
|
this.deleteBlockFields(blockId, "currentArgs");
|
|
925
990
|
}
|
|
991
|
+
this.setArgsErrors(blockId, derivedArgs);
|
|
926
992
|
}
|
|
927
993
|
|
|
928
994
|
/** Optimally sets inputs for multiple blocks in one go */
|
|
@@ -974,11 +1040,12 @@ export class ProjectMutator {
|
|
|
974
1040
|
);
|
|
975
1041
|
|
|
976
1042
|
// Derive args from storage (VM extracts data internally)
|
|
977
|
-
const
|
|
1043
|
+
const derivedArgs = this.projectHelper.deriveArgsFromStorage(
|
|
978
1044
|
blockConfig,
|
|
979
1045
|
updatedStorageJson,
|
|
980
1046
|
);
|
|
981
|
-
args =
|
|
1047
|
+
args = derivedArgsValue(derivedArgs);
|
|
1048
|
+
this.setArgsErrors(req.blockId, derivedArgs);
|
|
982
1049
|
} else {
|
|
983
1050
|
this.setBlockFieldObj(req.blockId, "blockStorage", this.createGzJsonFieldValue(req.state));
|
|
984
1051
|
if (req.state !== null && typeof req.state === "object" && "args" in req.state) {
|
|
@@ -1166,12 +1233,17 @@ export class ProjectMutator {
|
|
|
1166
1233
|
const tpl = info.getTemplate(this.tx);
|
|
1167
1234
|
|
|
1168
1235
|
// Use currentPrerunArgs for staging rendering
|
|
1169
|
-
const results = createRenderHeavyBlock(
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1236
|
+
const results = createRenderHeavyBlock(
|
|
1237
|
+
this.tx,
|
|
1238
|
+
tpl,
|
|
1239
|
+
{
|
|
1240
|
+
args: prerunArgsRef,
|
|
1241
|
+
blockId: this.tx.createValue(Pl.JsonString, JSON.stringify(blockId)),
|
|
1242
|
+
isProduction: this.tx.createValue(Pl.JsonBool, JSON.stringify(false)),
|
|
1243
|
+
context: ctx,
|
|
1244
|
+
},
|
|
1245
|
+
this.blockActionContextBaggage(blockId, info),
|
|
1246
|
+
);
|
|
1175
1247
|
|
|
1176
1248
|
// Here we set the staging ctx to the input context of the staging workflow, not the output because exports
|
|
1177
1249
|
// of one staging context should stay within the same block, and not travel downstream.
|
|
@@ -1182,6 +1254,8 @@ export class ProjectMutator {
|
|
|
1182
1254
|
// thus creating a certain discrepancy between staging workflow context behavior and desktop's result pool.
|
|
1183
1255
|
this.setBlockField(blockId, "stagingUiCtx", this.exportCtx(results.context), "NotReady");
|
|
1184
1256
|
this.setBlockField(blockId, "stagingOutput", results.result, "NotReady");
|
|
1257
|
+
if (results.actionContext !== undefined)
|
|
1258
|
+
this.setBlockField(blockId, "stagingActionContext", results.actionContext, "NotReady");
|
|
1185
1259
|
}
|
|
1186
1260
|
|
|
1187
1261
|
private renderProductionFor(blockId: string) {
|
|
@@ -1194,6 +1268,13 @@ export class ProjectMutator {
|
|
|
1194
1268
|
throw new Error(`Can't render production for block ${blockId}: currentArgs not set`);
|
|
1195
1269
|
}
|
|
1196
1270
|
|
|
1271
|
+
// Nor if its argsIssues() found errors in them
|
|
1272
|
+
const argsErrors = info.argsErrors;
|
|
1273
|
+
if (argsErrors.length > 0) {
|
|
1274
|
+
const messages = argsErrors.map(({ message }) => message).join("; ");
|
|
1275
|
+
throw new Error(`Can't render production for block ${blockId}: ${messages}`);
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1197
1278
|
const ctx = this.createProdCtx(this.getPendingProductionGraph().nodes.get(blockId)!.upstream);
|
|
1198
1279
|
|
|
1199
1280
|
if (this.getBlock(blockId).renderingMode === "Light")
|
|
@@ -1201,12 +1282,17 @@ export class ProjectMutator {
|
|
|
1201
1282
|
|
|
1202
1283
|
const tpl = info.getTemplate(this.tx);
|
|
1203
1284
|
|
|
1204
|
-
const results = createRenderHeavyBlock(
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1285
|
+
const results = createRenderHeavyBlock(
|
|
1286
|
+
this.tx,
|
|
1287
|
+
tpl,
|
|
1288
|
+
{
|
|
1289
|
+
args: info.fields.currentArgs.ref!,
|
|
1290
|
+
blockId: this.tx.createValue(Pl.JsonString, JSON.stringify(blockId)),
|
|
1291
|
+
isProduction: this.tx.createValue(Pl.JsonBool, JSON.stringify(true)),
|
|
1292
|
+
context: ctx,
|
|
1293
|
+
},
|
|
1294
|
+
this.blockActionContextBaggage(blockId, info),
|
|
1295
|
+
);
|
|
1210
1296
|
this.setBlockField(
|
|
1211
1297
|
blockId,
|
|
1212
1298
|
"prodCtx",
|
|
@@ -1215,6 +1301,8 @@ export class ProjectMutator {
|
|
|
1215
1301
|
);
|
|
1216
1302
|
this.setBlockField(blockId, "prodUiCtx", this.exportCtx(results.context), "NotReady");
|
|
1217
1303
|
this.setBlockField(blockId, "prodOutput", results.result, "NotReady");
|
|
1304
|
+
if (results.actionContext !== undefined)
|
|
1305
|
+
this.setBlockField(blockId, "prodActionContext", results.actionContext, "NotReady");
|
|
1218
1306
|
|
|
1219
1307
|
// saving inputs for which we rendered the production
|
|
1220
1308
|
this.setBlockFieldObj(blockId, "prodArgs", info.fields.currentArgs);
|
|
@@ -1266,11 +1354,9 @@ export class ProjectMutator {
|
|
|
1266
1354
|
prerunArgs = this.projectHelper.derivePrerunArgsFromStorage(blockConfig, storageToWrite);
|
|
1267
1355
|
|
|
1268
1356
|
// Derive args from storage (VM extracts data internally)
|
|
1269
|
-
const
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
);
|
|
1273
|
-
args = deriveArgsResult.error ? undefined : deriveArgsResult.value;
|
|
1357
|
+
const derivedArgs = this.projectHelper.deriveArgsFromStorage(blockConfig, storageToWrite);
|
|
1358
|
+
args = derivedArgsValue(derivedArgs);
|
|
1359
|
+
this.setArgsErrors(blockId, derivedArgs);
|
|
1274
1360
|
} else if (spec.storageMode === "legacy") {
|
|
1275
1361
|
// Model API v1: use legacyState from spec
|
|
1276
1362
|
const parsedState = JSON.parse(spec.legacyState);
|
|
@@ -1560,18 +1646,16 @@ export class ProjectMutator {
|
|
|
1560
1646
|
this.deleteBlockFields(blockId, "currentPrerunArgs");
|
|
1561
1647
|
}
|
|
1562
1648
|
|
|
1563
|
-
const
|
|
1649
|
+
const derivedArgs = this.projectHelper.deriveArgsFromStorage(newConfig, storageJson);
|
|
1564
1650
|
// args() legitimately returns undefined when inputs are incomplete/invalid;
|
|
1565
1651
|
// treat that like a derivation failure and clear the field (never JSON-encode undefined).
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
"currentArgs",
|
|
1570
|
-
this.createJsonFieldValue(deriveArgsResult.value),
|
|
1571
|
-
);
|
|
1652
|
+
const args = derivedArgsValue(derivedArgs);
|
|
1653
|
+
if (args !== undefined) {
|
|
1654
|
+
this.setBlockFieldObj(blockId, "currentArgs", this.createJsonFieldValue(args));
|
|
1572
1655
|
} else {
|
|
1573
1656
|
this.deleteBlockFields(blockId, "currentArgs");
|
|
1574
1657
|
}
|
|
1658
|
+
this.setArgsErrors(blockId, derivedArgs);
|
|
1575
1659
|
};
|
|
1576
1660
|
|
|
1577
1661
|
if (newClearState !== undefined) {
|
|
@@ -1587,6 +1671,9 @@ export class ProjectMutator {
|
|
|
1587
1671
|
} else {
|
|
1588
1672
|
// V1: Use setStates with legacy state format
|
|
1589
1673
|
persistBlockPack();
|
|
1674
|
+
// A V1 model tells whether the block may run by itself; args errors of an earlier V2 model
|
|
1675
|
+
// no longer apply.
|
|
1676
|
+
this.deleteBlockFields(blockId, "inputsValid");
|
|
1590
1677
|
this.setStates([{ modelAPIVersion: 1, blockId, state: newClearState.state }]);
|
|
1591
1678
|
}
|
|
1592
1679
|
} else {
|
|
@@ -1617,6 +1704,9 @@ export class ProjectMutator {
|
|
|
1617
1704
|
} else {
|
|
1618
1705
|
// Legacy blocks (modelAPIVersion 1): persist block pack, set prerunArgs = currentArgs
|
|
1619
1706
|
persistBlockPack();
|
|
1707
|
+
// A V1 model tells whether the block may run by itself; args errors of an earlier V2 model
|
|
1708
|
+
// no longer apply.
|
|
1709
|
+
this.deleteBlockFields(blockId, "inputsValid");
|
|
1620
1710
|
if (info.fields.currentArgs !== undefined) {
|
|
1621
1711
|
this.setBlockFieldObj(blockId, "currentPrerunArgs", info.fields.currentArgs);
|
|
1622
1712
|
}
|
|
@@ -1737,6 +1827,7 @@ export class ProjectMutator {
|
|
|
1737
1827
|
// skipping finished blocks
|
|
1738
1828
|
continue;
|
|
1739
1829
|
|
|
1830
|
+
this.deleteBlockFields(blockId, "prodActionContext");
|
|
1740
1831
|
if (this.deleteBlockFields(blockId, "prodOutput", "prodCtx", "prodUiCtx", "prodArgs")) {
|
|
1741
1832
|
// was actually stopped
|
|
1742
1833
|
stopped.push(blockId);
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import type { PlClient, SignedResourceId } from "@milaboratories/pl-client";
|
|
2
|
+
import { ActionContextLinkCapability } from "./template/render_block";
|
|
3
|
+
import {
|
|
4
|
+
ContinuePolling,
|
|
5
|
+
field,
|
|
6
|
+
isNullSignedResourceId,
|
|
7
|
+
parseSignedResourceId,
|
|
8
|
+
poll,
|
|
9
|
+
TestHelpers,
|
|
10
|
+
toGlobalResourceId,
|
|
11
|
+
} from "@milaboratories/pl-client";
|
|
12
|
+
import { getQuickJS } from "quickjs-emscripten";
|
|
13
|
+
import { expect, test } from "vitest";
|
|
14
|
+
import { ProjectHelper } from "../model/project_helper";
|
|
15
|
+
import { projectFieldName } from "../model/project_model";
|
|
16
|
+
import { BPSpecEnterV041NotPrepared, TestBPPreparer } from "../test/block_packs";
|
|
17
|
+
import { readBlockActionContextTree } from "../middle_layer/block_action_context";
|
|
18
|
+
import { createProject, ProjectMutator } from "./project";
|
|
19
|
+
|
|
20
|
+
type ActionContextBaggage = Record<string, string>;
|
|
21
|
+
|
|
22
|
+
/** Reads the action context that prodActionContext points to, or undefined if the field is absent.
|
|
23
|
+
* Polls until the field reference is resolved: it resolves asynchronously after the commit. */
|
|
24
|
+
async function readActionContext(
|
|
25
|
+
pl: PlClient,
|
|
26
|
+
prj: Awaited<ReturnType<typeof toGlobalResourceId>>,
|
|
27
|
+
blockId: string,
|
|
28
|
+
): Promise<
|
|
29
|
+
{ contextId: string; contextRef: SignedResourceId; baggage: ActionContextBaggage } | undefined
|
|
30
|
+
> {
|
|
31
|
+
return await poll(pl, async ({ tx }) => {
|
|
32
|
+
const project = await tx.getResourceData(prj, true);
|
|
33
|
+
const actionContextField = project.fields.find(
|
|
34
|
+
(f) => f.name === projectFieldName(blockId, "prodActionContext"),
|
|
35
|
+
);
|
|
36
|
+
if (actionContextField === undefined) return undefined;
|
|
37
|
+
|
|
38
|
+
const contextRef = actionContextField.value;
|
|
39
|
+
if (isNullSignedResourceId(contextRef)) throw new ContinuePolling();
|
|
40
|
+
const context = await tx.getResourceData(contextRef, false);
|
|
41
|
+
expect(context.type.name).toEqual("ActionContext");
|
|
42
|
+
return {
|
|
43
|
+
contextId: String(parseSignedResourceId(contextRef).globalId),
|
|
44
|
+
contextRef,
|
|
45
|
+
baggage: JSON.parse(Buffer.from(context.data!).toString()) as ActionContextBaggage,
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Returns the render an action context belongs to: the resource of the Running transition that the
|
|
51
|
+
* workflow controller records when it evaluates the render. The key names the decimal resource ID.
|
|
52
|
+
* Polls until the transition lands: reports are applied asynchronously after the commit. */
|
|
53
|
+
async function contextRender(pl: PlClient, contextRef: SignedResourceId): Promise<string> {
|
|
54
|
+
return await poll(pl, async ({ tx }) => {
|
|
55
|
+
const kv = await tx.listKeyValuesString(contextRef);
|
|
56
|
+
const running = kv.find((e) => e.key.startsWith("transition/render/"));
|
|
57
|
+
if (running === undefined) throw new ContinuePolling();
|
|
58
|
+
expect(JSON.parse(running.value)).toMatchObject({ state: "Running" });
|
|
59
|
+
return running.key.split("/")[2];
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
test("block action context outlives its renderer and is replaced on re-render", async () => {
|
|
64
|
+
//
|
|
65
|
+
// Scenario:
|
|
66
|
+
// 1. Create a project and render production of one block.
|
|
67
|
+
// 2. Wait until the production output is final.
|
|
68
|
+
// 3. Without a link capability the render has no action context. Otherwise read the action
|
|
69
|
+
// context through prodActionContext and check its baggage.
|
|
70
|
+
// 4. The context has the Running transition of the render. Wait until the render is garbage
|
|
71
|
+
// collected; the context must stay readable.
|
|
72
|
+
// 5. Re-render production: prodActionContext must point at a new context with a new run-id.
|
|
73
|
+
// 6. Render and stop production in one transaction: prodActionContext must be removed.
|
|
74
|
+
//
|
|
75
|
+
|
|
76
|
+
const quickJs = await getQuickJS();
|
|
77
|
+
|
|
78
|
+
await TestHelpers.withTempRoot(async (pl) => {
|
|
79
|
+
// 1. Create a project and render production of one block.
|
|
80
|
+
const prj = await pl.withWriteTx("CreatingProject", async (tx) => {
|
|
81
|
+
const prjRef = await createProject(tx);
|
|
82
|
+
tx.createField(field(tx.clientRoot, "prj"), "Dynamic", prjRef);
|
|
83
|
+
await tx.commit();
|
|
84
|
+
return await toGlobalResourceId(prjRef);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const renderBlock = async (numbers: number[], addBlock: boolean) =>
|
|
88
|
+
await pl.withWriteTx("RenderBlock", async (tx) => {
|
|
89
|
+
const mut = await ProjectMutator.load(new ProjectHelper(quickJs), tx, prj);
|
|
90
|
+
if (addBlock)
|
|
91
|
+
mut.addBlock(
|
|
92
|
+
{ id: "block1", label: "Block1", renderingMode: "Heavy" },
|
|
93
|
+
{
|
|
94
|
+
storageMode: "legacy",
|
|
95
|
+
legacyState: JSON.stringify({ args: { numbers } }),
|
|
96
|
+
blockPack: await TestBPPreparer.prepare(BPSpecEnterV041NotPrepared),
|
|
97
|
+
},
|
|
98
|
+
);
|
|
99
|
+
else
|
|
100
|
+
mut.setStates([
|
|
101
|
+
{ modelAPIVersion: 1, blockId: "block1", state: { args: { numbers }, uiState: {} } },
|
|
102
|
+
]);
|
|
103
|
+
mut.renderProduction(["block1"], true);
|
|
104
|
+
mut.save();
|
|
105
|
+
await tx.commit();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
await renderBlock([1, 2, 3], true);
|
|
109
|
+
|
|
110
|
+
// 2. Wait until the production output is final.
|
|
111
|
+
await poll(pl, async (tx) => {
|
|
112
|
+
const prjR = await tx.get(prj);
|
|
113
|
+
await prjR.get(projectFieldName("block1", "prodOutput")).then((r) => r.final());
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// 3. Read the action context through prodActionContext and check its baggage.
|
|
117
|
+
if (!pl.hasCapability(ActionContextLinkCapability)) {
|
|
118
|
+
expect(await readActionContext(pl, prj, "block1")).toBeUndefined();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const first = (await readActionContext(pl, prj, "block1"))!;
|
|
122
|
+
expect(first).toBeDefined();
|
|
123
|
+
expect(Object.keys(first.baggage).sort()).toEqual(["block-id", "block-pack", "name", "run-id"]);
|
|
124
|
+
expect(first.baggage["block-id"]).toEqual("block1");
|
|
125
|
+
expect(first.baggage["name"]).toEqual("Block1");
|
|
126
|
+
expect(JSON.parse(first.baggage["block-pack"])).toBeTypeOf("object");
|
|
127
|
+
expect(first.baggage["run-id"]).toMatch(/^[0-9a-f-]{36}$/);
|
|
128
|
+
|
|
129
|
+
// 4. The Running transition names the render; the context outlives it. The render's own end is
|
|
130
|
+
// awaited through its Completed report, which every reader can see: reading the render
|
|
131
|
+
// itself needs a signed reference that no field of the project holds once it is done.
|
|
132
|
+
const firstRender = await contextRender(pl, first.contextRef);
|
|
133
|
+
await poll(pl, async ({ tx }) => {
|
|
134
|
+
const kv = await tx.listKeyValuesString(first.contextRef);
|
|
135
|
+
const completed = kv.some(
|
|
136
|
+
(e) =>
|
|
137
|
+
e.key.startsWith(`transition/render/${firstRender}/`) &&
|
|
138
|
+
JSON.parse(e.value).state === "Completed",
|
|
139
|
+
);
|
|
140
|
+
if (!completed) throw new ContinuePolling();
|
|
141
|
+
});
|
|
142
|
+
const afterRenderEnd = (await readActionContext(pl, prj, "block1"))!;
|
|
143
|
+
expect(afterRenderEnd.contextId).toEqual(first.contextId);
|
|
144
|
+
expect(afterRenderEnd.baggage).toEqual(first.baggage);
|
|
145
|
+
|
|
146
|
+
// 4b. The read-back sees the same context with its baggage, and names the render.
|
|
147
|
+
const tree = (await readBlockActionContextTree(pl, prj, "block1", "prod"))!;
|
|
148
|
+
expect(tree).toBeDefined();
|
|
149
|
+
expect(tree.contextId).toEqual(first.contextId);
|
|
150
|
+
expect(tree.baggage).toEqual(first.baggage);
|
|
151
|
+
expect(tree.resource).toEqual(firstRender);
|
|
152
|
+
expect(tree.transitions.map((t) => [t.namespace, t.resource, t.state])).toContainEqual([
|
|
153
|
+
"render",
|
|
154
|
+
firstRender,
|
|
155
|
+
"Running",
|
|
156
|
+
]);
|
|
157
|
+
|
|
158
|
+
// 5. Re-render production: prodActionContext must point at a new context with a new run-id.
|
|
159
|
+
await renderBlock([4, 5, 6], false);
|
|
160
|
+
const second = (await readActionContext(pl, prj, "block1"))!;
|
|
161
|
+
expect(second.contextId).not.toEqual(first.contextId);
|
|
162
|
+
expect(second.baggage["run-id"]).not.toEqual(first.baggage["run-id"]);
|
|
163
|
+
expect(await contextRender(pl, second.contextRef)).not.toEqual(firstRender);
|
|
164
|
+
|
|
165
|
+
// 6. Render and stop production in one transaction: prodActionContext must be removed.
|
|
166
|
+
await pl.withWriteTx("RenderAndStop", async (tx) => {
|
|
167
|
+
const mut = await ProjectMutator.load(new ProjectHelper(quickJs), tx, prj);
|
|
168
|
+
mut.setStates([
|
|
169
|
+
{ modelAPIVersion: 1, blockId: "block1", state: { args: { numbers: [7] }, uiState: {} } },
|
|
170
|
+
]);
|
|
171
|
+
mut.renderProduction(["block1"], true);
|
|
172
|
+
mut.stopProduction("block1");
|
|
173
|
+
mut.save();
|
|
174
|
+
await tx.commit();
|
|
175
|
+
});
|
|
176
|
+
expect(await readActionContext(pl, prj, "block1")).toBeUndefined();
|
|
177
|
+
});
|
|
178
|
+
});
|