@milaboratories/pl-middle-layer 1.74.18 → 1.75.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/middle_layer/block.cjs.map +1 -1
- package/dist/middle_layer/block.js.map +1 -1
- 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/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 +45 -19
- package/dist/middle_layer/project.cjs.map +1 -1
- package/dist/middle_layer/project.d.ts.map +1 -1
- package/dist/middle_layer/project.js +46 -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 +1 -0
- package/dist/model/project_model.cjs.map +1 -1
- package/dist/model/project_model.d.ts +1 -1
- package/dist/model/project_model.js +1 -0
- package/dist/model/project_model.js.map +1 -1
- package/dist/mutator/project.cjs +54 -8
- 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 +54 -8
- package/dist/mutator/project.js.map +1 -1
- package/package.json +16 -16
- package/src/middle_layer/block.ts +4 -1
- package/src/middle_layer/column_links.test.ts +350 -0
- package/src/middle_layer/column_links.ts +152 -0
- package/src/middle_layer/middle_layer.ts +2 -0
- package/src/middle_layer/project.ts +39 -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 +2 -0
- package/src/mutator/project-v3.test.ts +105 -1
- package/src/mutator/project.ts +81 -24
- 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"
|
|
@@ -132,6 +133,7 @@ export const FieldsToDuplicate: Set<ProjectField["fieldName"]> = new Set([
|
|
|
132
133
|
"prodArgs",
|
|
133
134
|
"currentArgs",
|
|
134
135
|
"currentPrerunArgs",
|
|
136
|
+
"inputsValid",
|
|
135
137
|
"prodCtx",
|
|
136
138
|
"prodUiCtx",
|
|
137
139
|
"prodOutput",
|
|
@@ -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
|
@@ -77,6 +77,14 @@ import {
|
|
|
77
77
|
} from "@milaboratories/ts-helpers";
|
|
78
78
|
import { gzipSync } from "node:zlib";
|
|
79
79
|
import type { ProjectHelper } from "../model/project_helper";
|
|
80
|
+
import {
|
|
81
|
+
derivedArgsValue,
|
|
82
|
+
argsErrorsFieldChange,
|
|
83
|
+
argsErrorsOfBlock,
|
|
84
|
+
type DerivedArgs,
|
|
85
|
+
type ArgsErrorsFieldChange,
|
|
86
|
+
} from "../model/args_derivation";
|
|
87
|
+
import type { ArgsIssue } from "@milaboratories/pl-model-common";
|
|
80
88
|
import {
|
|
81
89
|
extractConfig,
|
|
82
90
|
UiError,
|
|
@@ -273,6 +281,24 @@ class BlockInfo {
|
|
|
273
281
|
return this.currentPrerunArgsC();
|
|
274
282
|
}
|
|
275
283
|
|
|
284
|
+
private readonly argsErrorsC = cached(
|
|
285
|
+
() => `${this.fields.inputsValid?.modCount}:${this.fields.currentArgs?.modCount}`,
|
|
286
|
+
() =>
|
|
287
|
+
argsErrorsOfBlock({
|
|
288
|
+
modelAPIVersion: this.config.modelAPIVersion,
|
|
289
|
+
inputsValid: this.fields.inputsValid,
|
|
290
|
+
currentArgs: this.currentArgs,
|
|
291
|
+
}),
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* The errors the block's `argsIssues()` found in its current args. A block whose model API
|
|
296
|
+
* version has no args issues, or that has no current args, has none.
|
|
297
|
+
*/
|
|
298
|
+
get argsErrors(): readonly ArgsIssue[] {
|
|
299
|
+
return this.argsErrorsC();
|
|
300
|
+
}
|
|
301
|
+
|
|
276
302
|
get stagingRendered(): boolean {
|
|
277
303
|
return this.fields.stagingCtx !== undefined;
|
|
278
304
|
}
|
|
@@ -756,6 +782,32 @@ export class ProjectMutator {
|
|
|
756
782
|
return deleted;
|
|
757
783
|
}
|
|
758
784
|
|
|
785
|
+
/**
|
|
786
|
+
* Brings the block's `inputsValid` field in line with what its args derivation found: the run
|
|
787
|
+
* errors of its args, none when it may run with them, and no field when it has no args.
|
|
788
|
+
*/
|
|
789
|
+
private setArgsErrors(blockId: string, derivedArgs: DerivedArgs): void {
|
|
790
|
+
this.applyArgsErrorsChange(
|
|
791
|
+
blockId,
|
|
792
|
+
argsErrorsFieldChange(this.getBlockInfo(blockId).fields.inputsValid, derivedArgs),
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
private applyArgsErrorsChange(blockId: string, change: ArgsErrorsFieldChange): void {
|
|
797
|
+
switch (change.type) {
|
|
798
|
+
case "keep":
|
|
799
|
+
return;
|
|
800
|
+
case "delete":
|
|
801
|
+
this.deleteBlockFields(blockId, "inputsValid");
|
|
802
|
+
return;
|
|
803
|
+
case "write": {
|
|
804
|
+
const ref = this.tx.createValue(Pl.JsonObject, change.data);
|
|
805
|
+
this.setBlockField(blockId, "inputsValid", ref, "Ready", change.data);
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
759
811
|
private updateLastModified() {
|
|
760
812
|
this.lastModified = Date.now();
|
|
761
813
|
this.lastModifiedChanged = true;
|
|
@@ -908,21 +960,16 @@ export class ProjectMutator {
|
|
|
908
960
|
}
|
|
909
961
|
|
|
910
962
|
// Derive args from storage - set or clear currentArgs based on derivation result
|
|
911
|
-
const
|
|
912
|
-
blockConfig,
|
|
913
|
-
initialStorageJson,
|
|
914
|
-
);
|
|
963
|
+
const derivedArgs = this.projectHelper.deriveArgsFromStorage(blockConfig, initialStorageJson);
|
|
915
964
|
// args() legitimately returns undefined when inputs are incomplete/invalid;
|
|
916
965
|
// 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
|
-
);
|
|
966
|
+
const args = derivedArgsValue(derivedArgs);
|
|
967
|
+
if (args !== undefined) {
|
|
968
|
+
this.setBlockFieldObj(blockId, "currentArgs", this.createJsonFieldValue(args));
|
|
923
969
|
} else {
|
|
924
970
|
this.deleteBlockFields(blockId, "currentArgs");
|
|
925
971
|
}
|
|
972
|
+
this.setArgsErrors(blockId, derivedArgs);
|
|
926
973
|
}
|
|
927
974
|
|
|
928
975
|
/** Optimally sets inputs for multiple blocks in one go */
|
|
@@ -974,11 +1021,12 @@ export class ProjectMutator {
|
|
|
974
1021
|
);
|
|
975
1022
|
|
|
976
1023
|
// Derive args from storage (VM extracts data internally)
|
|
977
|
-
const
|
|
1024
|
+
const derivedArgs = this.projectHelper.deriveArgsFromStorage(
|
|
978
1025
|
blockConfig,
|
|
979
1026
|
updatedStorageJson,
|
|
980
1027
|
);
|
|
981
|
-
args =
|
|
1028
|
+
args = derivedArgsValue(derivedArgs);
|
|
1029
|
+
this.setArgsErrors(req.blockId, derivedArgs);
|
|
982
1030
|
} else {
|
|
983
1031
|
this.setBlockFieldObj(req.blockId, "blockStorage", this.createGzJsonFieldValue(req.state));
|
|
984
1032
|
if (req.state !== null && typeof req.state === "object" && "args" in req.state) {
|
|
@@ -1194,6 +1242,13 @@ export class ProjectMutator {
|
|
|
1194
1242
|
throw new Error(`Can't render production for block ${blockId}: currentArgs not set`);
|
|
1195
1243
|
}
|
|
1196
1244
|
|
|
1245
|
+
// Nor if its argsIssues() found errors in them
|
|
1246
|
+
const argsErrors = info.argsErrors;
|
|
1247
|
+
if (argsErrors.length > 0) {
|
|
1248
|
+
const messages = argsErrors.map(({ message }) => message).join("; ");
|
|
1249
|
+
throw new Error(`Can't render production for block ${blockId}: ${messages}`);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1197
1252
|
const ctx = this.createProdCtx(this.getPendingProductionGraph().nodes.get(blockId)!.upstream);
|
|
1198
1253
|
|
|
1199
1254
|
if (this.getBlock(blockId).renderingMode === "Light")
|
|
@@ -1266,11 +1321,9 @@ export class ProjectMutator {
|
|
|
1266
1321
|
prerunArgs = this.projectHelper.derivePrerunArgsFromStorage(blockConfig, storageToWrite);
|
|
1267
1322
|
|
|
1268
1323
|
// Derive args from storage (VM extracts data internally)
|
|
1269
|
-
const
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
);
|
|
1273
|
-
args = deriveArgsResult.error ? undefined : deriveArgsResult.value;
|
|
1324
|
+
const derivedArgs = this.projectHelper.deriveArgsFromStorage(blockConfig, storageToWrite);
|
|
1325
|
+
args = derivedArgsValue(derivedArgs);
|
|
1326
|
+
this.setArgsErrors(blockId, derivedArgs);
|
|
1274
1327
|
} else if (spec.storageMode === "legacy") {
|
|
1275
1328
|
// Model API v1: use legacyState from spec
|
|
1276
1329
|
const parsedState = JSON.parse(spec.legacyState);
|
|
@@ -1560,18 +1613,16 @@ export class ProjectMutator {
|
|
|
1560
1613
|
this.deleteBlockFields(blockId, "currentPrerunArgs");
|
|
1561
1614
|
}
|
|
1562
1615
|
|
|
1563
|
-
const
|
|
1616
|
+
const derivedArgs = this.projectHelper.deriveArgsFromStorage(newConfig, storageJson);
|
|
1564
1617
|
// args() legitimately returns undefined when inputs are incomplete/invalid;
|
|
1565
1618
|
// 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
|
-
);
|
|
1619
|
+
const args = derivedArgsValue(derivedArgs);
|
|
1620
|
+
if (args !== undefined) {
|
|
1621
|
+
this.setBlockFieldObj(blockId, "currentArgs", this.createJsonFieldValue(args));
|
|
1572
1622
|
} else {
|
|
1573
1623
|
this.deleteBlockFields(blockId, "currentArgs");
|
|
1574
1624
|
}
|
|
1625
|
+
this.setArgsErrors(blockId, derivedArgs);
|
|
1575
1626
|
};
|
|
1576
1627
|
|
|
1577
1628
|
if (newClearState !== undefined) {
|
|
@@ -1587,6 +1638,9 @@ export class ProjectMutator {
|
|
|
1587
1638
|
} else {
|
|
1588
1639
|
// V1: Use setStates with legacy state format
|
|
1589
1640
|
persistBlockPack();
|
|
1641
|
+
// A V1 model tells whether the block may run by itself; args errors of an earlier V2 model
|
|
1642
|
+
// no longer apply.
|
|
1643
|
+
this.deleteBlockFields(blockId, "inputsValid");
|
|
1590
1644
|
this.setStates([{ modelAPIVersion: 1, blockId, state: newClearState.state }]);
|
|
1591
1645
|
}
|
|
1592
1646
|
} else {
|
|
@@ -1617,6 +1671,9 @@ export class ProjectMutator {
|
|
|
1617
1671
|
} else {
|
|
1618
1672
|
// Legacy blocks (modelAPIVersion 1): persist block pack, set prerunArgs = currentArgs
|
|
1619
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");
|
|
1620
1677
|
if (info.fields.currentArgs !== undefined) {
|
|
1621
1678
|
this.setBlockFieldObj(blockId, "currentPrerunArgs", info.fields.currentArgs);
|
|
1622
1679
|
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createSignedResourceId,
|
|
3
|
+
DefaultFinalResourceDataPredicate,
|
|
4
|
+
NullSignedResourceId,
|
|
5
|
+
type FieldData,
|
|
6
|
+
type FieldType,
|
|
7
|
+
type SignedResourceId,
|
|
8
|
+
} from "@milaboratories/pl-client";
|
|
9
|
+
import type { PColumnSpec, ArgsIssue } from "@milaboratories/pl-model-common";
|
|
10
|
+
import { PlTreeState, type ExtendedResourceData } from "@milaboratories/pl-tree";
|
|
11
|
+
import { argsFingerprint, type ArgsErrorsRecord } from "../model/args_derivation";
|
|
12
|
+
import {
|
|
13
|
+
projectFieldName,
|
|
14
|
+
ProjectStructureKey,
|
|
15
|
+
type ProjectField,
|
|
16
|
+
type ProjectStructure,
|
|
17
|
+
} from "../model/project_model";
|
|
18
|
+
|
|
19
|
+
/** What a project field holds: a resource, an error, or nothing yet. */
|
|
20
|
+
export type FieldContent = SignedResourceId | { error: SignedResourceId } | "unresolved";
|
|
21
|
+
|
|
22
|
+
/** Columns of a result list by name: the spec of each and the cells of its data. */
|
|
23
|
+
export type Columns = Record<string, { spec: PColumnSpec; cells: string[] }>;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* State of a result list: `locked` — complete; `open` — more columns may come; `failed` — it has
|
|
27
|
+
* an error and will never lock.
|
|
28
|
+
*/
|
|
29
|
+
export type FrameState = { state: "locked" | "open" | "failed" };
|
|
30
|
+
|
|
31
|
+
/** The block fields a {@link FakeProject} holds. */
|
|
32
|
+
export type FakeProjectField = Extract<
|
|
33
|
+
ProjectField["fieldName"],
|
|
34
|
+
| "currentArgs"
|
|
35
|
+
| "inputsValid"
|
|
36
|
+
| "prodArgs"
|
|
37
|
+
| "prodCtx"
|
|
38
|
+
| "prodUiCtx"
|
|
39
|
+
| "stagingCtx"
|
|
40
|
+
| "stagingUiCtx"
|
|
41
|
+
>;
|
|
42
|
+
|
|
43
|
+
/** A project resource tree built in memory: its blocks, in project order, and their fields. */
|
|
44
|
+
export class FakeProject {
|
|
45
|
+
private readonly nodes: ExtendedResourceData[] = [];
|
|
46
|
+
private sent = 0;
|
|
47
|
+
private readonly blocks = new Map<string, Partial<Record<FakeProjectField, FieldContent>>>();
|
|
48
|
+
private readonly frameFields = new Map<SignedResourceId, FieldData[]>();
|
|
49
|
+
|
|
50
|
+
/** Adds a block below the others, or replaces the fields of one already added. */
|
|
51
|
+
block(blockId: string, fields: Partial<Record<FakeProjectField, FieldContent>>): void {
|
|
52
|
+
this.blocks.set(blockId, fields);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Args of a block. */
|
|
56
|
+
args(args: unknown): SignedResourceId {
|
|
57
|
+
return this.value("json/object", args);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Args errors of a block found in the args `forArgs`, as its `inputsValid` field holds them. */
|
|
61
|
+
argsErrors(errors: readonly ArgsIssue[], forArgs: unknown): SignedResourceId {
|
|
62
|
+
const record: ArgsErrorsRecord = { argsFingerprint: argsFingerprint(forArgs), errors };
|
|
63
|
+
return this.value("json/object", record);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A ready value resource holding `json`. */
|
|
67
|
+
value(type: string, json: unknown): SignedResourceId {
|
|
68
|
+
const id = this.newId();
|
|
69
|
+
this.nodes.push({
|
|
70
|
+
...READY,
|
|
71
|
+
id,
|
|
72
|
+
kind: "Value",
|
|
73
|
+
type: { name: type, version: "1" },
|
|
74
|
+
data: Buffer.from(JSON.stringify(json)),
|
|
75
|
+
fields: [],
|
|
76
|
+
});
|
|
77
|
+
return id;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** An error a resource or field can hold. */
|
|
81
|
+
error(message: string): SignedResourceId {
|
|
82
|
+
return this.value("json/resourceError", { message });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** A result list exporting the given columns, each with JSON data holding `cells`. */
|
|
86
|
+
frame(columns: Columns, state: FrameState = { state: "locked" }): SignedResourceId {
|
|
87
|
+
const id = this.newId();
|
|
88
|
+
this.frameFields.set(id, []);
|
|
89
|
+
this.extend(id, columns, state);
|
|
90
|
+
return id;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Adds columns to a result list made by {@link frame}, and moves it to `state`. */
|
|
94
|
+
extend(id: SignedResourceId, columns: Columns, { state }: FrameState): void {
|
|
95
|
+
const fields = this.frameFields.get(id);
|
|
96
|
+
if (fields === undefined) throw new Error("Not a result list of this project");
|
|
97
|
+
for (const [name, { spec, cells }] of Object.entries(columns)) {
|
|
98
|
+
const data = Object.fromEntries(cells.map((cell, i) => [`["s${i}"]`, cell]));
|
|
99
|
+
fields.push(field("Input", `${name}.spec`, this.value("PColumnSpec", spec)));
|
|
100
|
+
fields.push(
|
|
101
|
+
field("Input", `${name}.data`, this.value("PColumnData/Json", { keyLength: 1, data })),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
const error = state === "failed" ? this.error("render failed") : NullSignedResourceId;
|
|
105
|
+
this.nodes.push({
|
|
106
|
+
...READY,
|
|
107
|
+
id,
|
|
108
|
+
kind: "Structural",
|
|
109
|
+
type: { name: "PFrame", version: "1" },
|
|
110
|
+
inputsLocked: state === "locked",
|
|
111
|
+
resourceReady: state === "locked",
|
|
112
|
+
final: state === "locked",
|
|
113
|
+
error,
|
|
114
|
+
fields: [...fields],
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The tree of the project as it is now. */
|
|
119
|
+
tree(): PlTreeState {
|
|
120
|
+
const tree = new PlTreeState(ROOT_ID, DefaultFinalResourceDataPredicate);
|
|
121
|
+
tree.updateFromResourceData(this.changes());
|
|
122
|
+
return tree;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The project resource and the resources added or changed since the last call: a final
|
|
127
|
+
* resource already in the tree is not sent again.
|
|
128
|
+
*/
|
|
129
|
+
changes(): ExtendedResourceData[] {
|
|
130
|
+
const structure: ProjectStructure = {
|
|
131
|
+
groups: [
|
|
132
|
+
{
|
|
133
|
+
id: "main",
|
|
134
|
+
label: "Main",
|
|
135
|
+
blocks: [...this.blocks.keys()].map((id) => ({ id, label: id, renderingMode: "Heavy" })),
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
};
|
|
139
|
+
const fields = [...this.blocks].flatMap(([blockId, contents]) =>
|
|
140
|
+
PROJECT_FIELDS.flatMap((name) => {
|
|
141
|
+
const content = contents[name];
|
|
142
|
+
return content === undefined
|
|
143
|
+
? []
|
|
144
|
+
: [projectField(projectFieldName(blockId, name), content)];
|
|
145
|
+
}),
|
|
146
|
+
);
|
|
147
|
+
const root: ExtendedResourceData = {
|
|
148
|
+
...READY,
|
|
149
|
+
final: false,
|
|
150
|
+
id: ROOT_ID,
|
|
151
|
+
kind: "Structural",
|
|
152
|
+
type: { name: "UserProject", version: "2" },
|
|
153
|
+
fields,
|
|
154
|
+
kv: [{ key: ProjectStructureKey, value: Buffer.from(JSON.stringify(structure)) }],
|
|
155
|
+
};
|
|
156
|
+
const added = this.nodes.slice(this.sent);
|
|
157
|
+
this.sent = this.nodes.length;
|
|
158
|
+
return [root, ...added];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private newId(): SignedResourceId {
|
|
162
|
+
return createSignedResourceId(nextResourceId++);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Internals
|
|
167
|
+
|
|
168
|
+
const ROOT_ID = createSignedResourceId(1_000_001n);
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Resource ids stay unique across tests, as they are on a backend: the middle layer caches the
|
|
172
|
+
* columns an args resource references by its id.
|
|
173
|
+
*/
|
|
174
|
+
let nextResourceId = 100n;
|
|
175
|
+
|
|
176
|
+
const READY: Omit<ExtendedResourceData, "id" | "kind" | "type" | "fields"> = {
|
|
177
|
+
originalResourceId: NullSignedResourceId,
|
|
178
|
+
error: NullSignedResourceId,
|
|
179
|
+
inputsLocked: true,
|
|
180
|
+
outputsLocked: true,
|
|
181
|
+
resourceReady: true,
|
|
182
|
+
final: true,
|
|
183
|
+
kv: [],
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const PROJECT_FIELDS: readonly FakeProjectField[] = [
|
|
187
|
+
"currentArgs",
|
|
188
|
+
"inputsValid",
|
|
189
|
+
"prodArgs",
|
|
190
|
+
"prodCtx",
|
|
191
|
+
"prodUiCtx",
|
|
192
|
+
"stagingCtx",
|
|
193
|
+
"stagingUiCtx",
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
function field(type: FieldType, name: string, value: SignedResourceId): FieldData {
|
|
197
|
+
return { name, type, value, error: NullSignedResourceId, status: "Resolved", valueIsFinal: true };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function projectField(name: string, content: FieldContent): FieldData {
|
|
201
|
+
if (content === "unresolved") {
|
|
202
|
+
return {
|
|
203
|
+
name,
|
|
204
|
+
type: "Dynamic",
|
|
205
|
+
value: NullSignedResourceId,
|
|
206
|
+
error: NullSignedResourceId,
|
|
207
|
+
status: "Empty",
|
|
208
|
+
valueIsFinal: false,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
if (typeof content === "object" && "error" in content) {
|
|
212
|
+
return {
|
|
213
|
+
name,
|
|
214
|
+
type: "Dynamic",
|
|
215
|
+
value: NullSignedResourceId,
|
|
216
|
+
error: content.error,
|
|
217
|
+
status: "Resolved",
|
|
218
|
+
valueIsFinal: true,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
return field("Dynamic", name, content);
|
|
222
|
+
}
|