@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
|
@@ -24,10 +24,15 @@ import { finalityStopRules } from "./finality_stop_rules";
|
|
|
24
24
|
import type { ComputableStableDefined, ComputableValueOrErrors } from "@milaboratories/computable";
|
|
25
25
|
import { Computable } from "@milaboratories/computable";
|
|
26
26
|
import { projectOverview } from "./project_overview";
|
|
27
|
+
import { blockDependencies, blockDependents, labelBlockColumnLinks } from "./column_links";
|
|
27
28
|
import type { BlockPackSpecAny } from "../model";
|
|
28
29
|
import { randomUUID } from "node:crypto";
|
|
29
30
|
import { withProject, withProjectAuthored } from "../mutator/project";
|
|
30
|
-
import type {
|
|
31
|
+
import type {
|
|
32
|
+
ExtendedResourceData,
|
|
33
|
+
PlTreeNodeAccessor,
|
|
34
|
+
PruningFunction,
|
|
35
|
+
} from "@milaboratories/pl-tree";
|
|
31
36
|
import {
|
|
32
37
|
SynchronizedTreeState,
|
|
33
38
|
treeDumpStats,
|
|
@@ -38,6 +43,9 @@ import { setTimeout } from "node:timers/promises";
|
|
|
38
43
|
import { frontendData } from "./frontend_path";
|
|
39
44
|
import type { NavigationState } from "@milaboratories/pl-model-common";
|
|
40
45
|
import { getBlockParameters, blockOutputs } from "./block";
|
|
46
|
+
import type { BlockActionContextMode, BlockActionContextNode } from "./block_action_context";
|
|
47
|
+
import { readBlockActionContextTree } from "./block_action_context";
|
|
48
|
+
import { getBlockPackInfo } from "./util";
|
|
41
49
|
import type { FrontendData } from "../model/frontend";
|
|
42
50
|
import type { ProjectId, ProjectStructure } from "../model/project_model";
|
|
43
51
|
import { projectFieldName } from "../model/project_model";
|
|
@@ -56,9 +64,10 @@ import type {
|
|
|
56
64
|
BlockSettings,
|
|
57
65
|
BlockStateInternalV3,
|
|
58
66
|
} from "@milaboratories/pl-model-middle-layer";
|
|
67
|
+
import type { ColumnLink } from "@milaboratories/pl-model-common";
|
|
59
68
|
import { activeConfigs } from "./active_cfg";
|
|
60
69
|
import { NavigationStates } from "./navigation_states";
|
|
61
|
-
import { extractConfig, BLOCK_STORAGE_FACADE_VERSION } from "@platforma-sdk/model";
|
|
70
|
+
import { checkBlockFlag, extractConfig, BLOCK_STORAGE_FACADE_VERSION } from "@platforma-sdk/model";
|
|
62
71
|
import fs from "node:fs/promises";
|
|
63
72
|
import canonicalize from "canonicalize";
|
|
64
73
|
import type { ProjectOverviewLight } from "./project_overview_light";
|
|
@@ -68,6 +77,9 @@ import { cacheBlockPackTemplate } from "../mutator/template/template_cache";
|
|
|
68
77
|
|
|
69
78
|
type BlockStateComputables = {
|
|
70
79
|
readonly fullState: Computable<BlockStateInternalV3>;
|
|
80
|
+
/** Column links of the block, nested into its full state. */
|
|
81
|
+
readonly dependencies: Computable<ColumnLink[] | undefined>;
|
|
82
|
+
readonly dependents: Computable<ColumnLink[] | undefined>;
|
|
71
83
|
};
|
|
72
84
|
|
|
73
85
|
function stringifyForDump(object: unknown): string {
|
|
@@ -94,6 +106,13 @@ function stringifyForDump(object: unknown): string {
|
|
|
94
106
|
});
|
|
95
107
|
}
|
|
96
108
|
|
|
109
|
+
/** Releases what the computables of a block hold. */
|
|
110
|
+
function resetBlockState(computables: BlockStateComputables): void {
|
|
111
|
+
computables.fullState.resetState();
|
|
112
|
+
computables.dependencies.resetState();
|
|
113
|
+
computables.dependents.resetState();
|
|
114
|
+
}
|
|
115
|
+
|
|
97
116
|
/** Data access object, to manipulate and read single opened (!) project data. */
|
|
98
117
|
export class Project {
|
|
99
118
|
/** Data for the left panel, contain basic information about block status. */
|
|
@@ -292,8 +311,8 @@ export class Project {
|
|
|
292
311
|
// Doing cleanup for deleted blocks
|
|
293
312
|
for (const blockId of this.blockComputables.keys()) {
|
|
294
313
|
if (!existingBlocks.has(blockId)) {
|
|
295
|
-
const
|
|
296
|
-
if (
|
|
314
|
+
const computables = this.blockComputables.get(blockId);
|
|
315
|
+
if (computables !== undefined && computables !== null) resetBlockState(computables);
|
|
297
316
|
this.blockComputables.set(blockId, null);
|
|
298
317
|
}
|
|
299
318
|
}
|
|
@@ -749,13 +768,19 @@ export class Project {
|
|
|
749
768
|
if (cached === undefined) {
|
|
750
769
|
// state consists of inputs (args + ui state) and outputs
|
|
751
770
|
const outputs = blockOutputs(this.projectTree.entry(), blockId, this.env);
|
|
771
|
+
const dependencies = blockDependencies(this.projectTree.entry(), blockId, this.env);
|
|
772
|
+
const dependents = blockDependents(this.projectTree.entry(), blockId, this.env);
|
|
752
773
|
const fullState = Computable.make(
|
|
753
774
|
(ctx) => {
|
|
775
|
+
// Links are computed only for a block whose UI reads them.
|
|
776
|
+
const linked = readsColumnLinks(ctx.accessor(this.projectTree.entry()).node(), blockId);
|
|
754
777
|
return {
|
|
755
778
|
parameters: getBlockParameters(this.projectTree.entry(), blockId, ctx),
|
|
756
779
|
outputs,
|
|
757
780
|
navigationState: this.navigationStates.getState(blockId),
|
|
758
781
|
overview: this.overview,
|
|
782
|
+
dependencies: linked ? dependencies : undefined,
|
|
783
|
+
dependents: linked ? dependents : undefined,
|
|
759
784
|
};
|
|
760
785
|
},
|
|
761
786
|
{
|
|
@@ -770,6 +795,9 @@ export class Project {
|
|
|
770
795
|
...v.parameters,
|
|
771
796
|
outputs: newOutputs,
|
|
772
797
|
navigationState: v.navigationState,
|
|
798
|
+
// The block state already nests the overview, so labelling by its titles adds
|
|
799
|
+
// nothing to watch.
|
|
800
|
+
columnLinks: labelBlockColumnLinks({ ...v, blockId }, v.overview?.blocks ?? []),
|
|
773
801
|
} as BlockStateInternalV3;
|
|
774
802
|
},
|
|
775
803
|
},
|
|
@@ -777,6 +805,8 @@ export class Project {
|
|
|
777
805
|
|
|
778
806
|
const computables: BlockStateComputables = {
|
|
779
807
|
fullState: fullState.withPreCalculatedValueTree(),
|
|
808
|
+
dependencies,
|
|
809
|
+
dependents,
|
|
780
810
|
};
|
|
781
811
|
|
|
782
812
|
this.blockComputables.set(blockId, computables);
|
|
@@ -790,6 +820,17 @@ export class Project {
|
|
|
790
820
|
* Returns a computable, that can be used to retrieve and watch full block state,
|
|
791
821
|
* including outputs, arguments, ui state.
|
|
792
822
|
* */
|
|
823
|
+
/**
|
|
824
|
+
* Reads the block action context tree of a block render (see readBlockActionContextTree): current state, not
|
|
825
|
+
* watched. Undefined when the block has no action context.
|
|
826
|
+
*/
|
|
827
|
+
public async getBlockActionContext(
|
|
828
|
+
blockId: string,
|
|
829
|
+
mode: BlockActionContextMode = "prod",
|
|
830
|
+
): Promise<BlockActionContextNode | undefined> {
|
|
831
|
+
return await readBlockActionContextTree(this.env.pl, this.rid, blockId, mode);
|
|
832
|
+
}
|
|
833
|
+
|
|
793
834
|
public getBlockState(blockId: string): Computable<BlockStateInternalV3> {
|
|
794
835
|
return this.getBlockComputables(blockId).fullState;
|
|
795
836
|
}
|
|
@@ -839,7 +880,7 @@ export class Project {
|
|
|
839
880
|
this.overview.resetState();
|
|
840
881
|
this.blockFrontends.forEach((c) => c.resetState());
|
|
841
882
|
this.blockComputables.forEach((c) => {
|
|
842
|
-
if (c !== null) c
|
|
883
|
+
if (c !== null) resetBlockState(c);
|
|
843
884
|
});
|
|
844
885
|
this.activeConfigs.resetState();
|
|
845
886
|
}
|
|
@@ -1022,6 +1063,12 @@ export function projectTreeTraverseStopRules(): Filter {
|
|
|
1022
1063
|
return finalityStopRules(TreeFinality);
|
|
1023
1064
|
}
|
|
1024
1065
|
|
|
1066
|
+
/** Whether the block's model declares that its UI reads the column links of the block state. */
|
|
1067
|
+
function readsColumnLinks(prj: PlTreeNodeAccessor, blockId: string): boolean {
|
|
1068
|
+
const info = getBlockPackInfo(prj, blockId);
|
|
1069
|
+
return info !== undefined && checkBlockFlag(info.cfg.featureFlags, "supportsColumnLinks");
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1025
1072
|
/** Returns true if sdk version of the block is old and we need to convert
|
|
1026
1073
|
* ErrorLike errors to strings like it was.
|
|
1027
1074
|
* We need it for keeping old blocks and new UI compatibility. */
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { Computable } from "@milaboratories/computable";
|
|
2
|
+
import type { ArgsIssue } from "@milaboratories/pl-model-common";
|
|
3
|
+
import type { PlTreeState } from "@milaboratories/pl-tree";
|
|
4
|
+
import { describe, expect, test } from "vitest";
|
|
5
|
+
import { projectFieldName } from "../model/project_model";
|
|
6
|
+
import { FakeProject } from "../test/fake_project";
|
|
7
|
+
import { argsAllowRun, type RunStateSource, withRunState } from "./project_overview";
|
|
8
|
+
|
|
9
|
+
const setThreshold: ArgsIssue = { severity: "error", message: "Set a threshold" };
|
|
10
|
+
|
|
11
|
+
describe("argsAllowRun", () => {
|
|
12
|
+
test("a block whose argsIssues found no errors in its current args may run", async () => {
|
|
13
|
+
const prj = new FakeProject();
|
|
14
|
+
prj.block("x", { currentArgs: prj.args({ n: 1 }), inputsValid: prj.argsErrors([], { n: 1 }) });
|
|
15
|
+
|
|
16
|
+
expect(await allowRun(prj.tree(), "x")).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("a block with current args and no args errors record may run, as before the record", async () => {
|
|
20
|
+
// What a project saved by an older middle layer holds: current args and no `inputsValid`.
|
|
21
|
+
const prj = new FakeProject();
|
|
22
|
+
prj.block("x", { currentArgs: prj.args({ n: 1 }) });
|
|
23
|
+
|
|
24
|
+
expect(await allowRun(prj.tree(), "x")).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("a block without current args may not run", async () => {
|
|
28
|
+
const prj = new FakeProject();
|
|
29
|
+
prj.block("x", { prodArgs: prj.args({ n: 1 }) });
|
|
30
|
+
|
|
31
|
+
expect(await allowRun(prj.tree(), "x")).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("a block whose argsIssues found errors in its current args may not run", async () => {
|
|
35
|
+
const prj = new FakeProject();
|
|
36
|
+
prj.block("x", {
|
|
37
|
+
currentArgs: prj.args({ n: 1 }),
|
|
38
|
+
inputsValid: prj.argsErrors([setThreshold], { n: 1 }),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
expect(await allowRun(prj.tree(), "x")).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("errors found in the same args, whatever their key order, still apply", async () => {
|
|
45
|
+
const prj = new FakeProject();
|
|
46
|
+
prj.block("x", {
|
|
47
|
+
currentArgs: prj.args({ a: 1, b: { c: 2, d: 3 } }),
|
|
48
|
+
inputsValid: prj.argsErrors([setThreshold], { b: { d: 3, c: 2 }, a: 1 }),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(await allowRun(prj.tree(), "x")).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("errors recorded for other args do not keep the block from running", async () => {
|
|
55
|
+
// What an older middle layer leaves behind: it writes new current args and never touches
|
|
56
|
+
// the `inputsValid` field.
|
|
57
|
+
const prj = new FakeProject();
|
|
58
|
+
prj.block("x", {
|
|
59
|
+
currentArgs: prj.args({ n: 2 }),
|
|
60
|
+
inputsValid: prj.argsErrors([setThreshold], { n: 1 }),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(await allowRun(prj.tree(), "x")).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("a field that cannot be read does not keep the block from running", async () => {
|
|
67
|
+
const prj = new FakeProject();
|
|
68
|
+
prj.block("x", {
|
|
69
|
+
currentArgs: prj.args({ n: 1 }),
|
|
70
|
+
inputsValid: prj.value("json/object", [setThreshold]),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(await allowRun(prj.tree(), "x")).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("may run again once the args and their record have no errors", async () => {
|
|
77
|
+
const prj = new FakeProject();
|
|
78
|
+
prj.block("x", {
|
|
79
|
+
currentArgs: prj.args({ n: 1 }),
|
|
80
|
+
inputsValid: prj.argsErrors([setThreshold], { n: 1 }),
|
|
81
|
+
});
|
|
82
|
+
const tree = prj.tree();
|
|
83
|
+
const computable = allowRunComputable(tree, "x");
|
|
84
|
+
expect(await computable.getValue()).toBe(false);
|
|
85
|
+
|
|
86
|
+
prj.block("x", { currentArgs: prj.args({ n: 2 }), inputsValid: prj.argsErrors([], { n: 2 }) });
|
|
87
|
+
tree.updateFromResourceData(prj.changes());
|
|
88
|
+
|
|
89
|
+
expect(await computable.getValue()).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("current args changed without their record leave the old errors behind", async () => {
|
|
93
|
+
const prj = new FakeProject();
|
|
94
|
+
const argsErrors = prj.argsErrors([setThreshold], { n: 1 });
|
|
95
|
+
prj.block("x", { currentArgs: prj.args({ n: 1 }), inputsValid: argsErrors });
|
|
96
|
+
const tree = prj.tree();
|
|
97
|
+
const computable = allowRunComputable(tree, "x");
|
|
98
|
+
expect(await computable.getValue()).toBe(false);
|
|
99
|
+
|
|
100
|
+
prj.block("x", { currentArgs: prj.args({ n: 2 }), inputsValid: argsErrors });
|
|
101
|
+
tree.updateFromResourceData(prj.changes());
|
|
102
|
+
|
|
103
|
+
expect(await computable.getValue()).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("withRunState", () => {
|
|
108
|
+
test("a block whose results are up to date lets a changed dependent run despite its own args errors", () => {
|
|
109
|
+
const [upstream, dependent] = withRunState([
|
|
110
|
+
block({ id: "a", inputsValid: false }),
|
|
111
|
+
block({ id: "b", stale: true, upstreams: ["a"] }),
|
|
112
|
+
]);
|
|
113
|
+
expect(upstream.canRun).toBe(false);
|
|
114
|
+
expect(dependent.canRun).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("a changed block with args errors keeps its dependents from running", () => {
|
|
118
|
+
const [upstream, dependent, further] = withRunState([
|
|
119
|
+
block({ id: "a", stale: true, inputsValid: false }),
|
|
120
|
+
block({ id: "b", stale: true, upstreams: ["a"] }),
|
|
121
|
+
block({ id: "c", upstreams: ["a", "b"] }),
|
|
122
|
+
]);
|
|
123
|
+
expect(upstream.canRun).toBe(false);
|
|
124
|
+
expect(dependent.canRun).toBe(false);
|
|
125
|
+
expect(further).toMatchObject({ stale: true, canRun: false });
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("a block with failed outputs and args errors keeps its dependents from running", () => {
|
|
129
|
+
const [, dependent] = withRunState([
|
|
130
|
+
block({ id: "a", outputErrors: true, inputsValid: false }),
|
|
131
|
+
block({ id: "b", stale: true, upstreams: ["a"] }),
|
|
132
|
+
]);
|
|
133
|
+
expect(dependent.canRun).toBe(false);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("a block becomes stale and runnable when an upstream block is stale", () => {
|
|
137
|
+
const [upstream, dependent] = withRunState([
|
|
138
|
+
block({ id: "a", stale: true }),
|
|
139
|
+
block({ id: "b", upstreams: ["a"] }),
|
|
140
|
+
]);
|
|
141
|
+
expect(upstream).toMatchObject({ stale: true, canRun: true });
|
|
142
|
+
expect(dependent).toMatchObject({ stale: true, canRun: true });
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("a block with up-to-date results has nothing to run", () => {
|
|
146
|
+
const [b] = withRunState([block({ id: "a" })]);
|
|
147
|
+
expect(b).toMatchObject({ stale: false, canRun: false });
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("an up-to-date block whose validity is not about its args keeps its dependents from running", () => {
|
|
151
|
+
// A block whose code cannot run here, or whose model predates args issues and checks more
|
|
152
|
+
// than its args, as before args issues.
|
|
153
|
+
const [upstream, dependent] = withRunState([
|
|
154
|
+
block({ id: "a", inputsValid: undefined, inputsValidFromArgs: false }),
|
|
155
|
+
block({ id: "b", stale: true, upstreams: ["a"] }),
|
|
156
|
+
]);
|
|
157
|
+
expect(upstream.canRun).toBe(false);
|
|
158
|
+
expect(dependent.canRun).toBe(false);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("a changed block that references a missing block cannot run", () => {
|
|
162
|
+
const [b] = withRunState([block({ id: "a", stale: true, missingReference: true })]);
|
|
163
|
+
expect(b.canRun).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Internals
|
|
168
|
+
|
|
169
|
+
/** Whether the block may run, read the way the project overview reads it. */
|
|
170
|
+
function allowRunComputable(tree: PlTreeState, blockId: string): Computable<boolean> {
|
|
171
|
+
const entry = tree.entry();
|
|
172
|
+
return Computable.make((ctx) => {
|
|
173
|
+
const prj = ctx.accessor(entry).node();
|
|
174
|
+
const currentArgs = prj
|
|
175
|
+
.traverse({
|
|
176
|
+
field: projectFieldName(blockId, "currentArgs"),
|
|
177
|
+
assertFieldType: "Dynamic",
|
|
178
|
+
stableIfNotFound: true,
|
|
179
|
+
})
|
|
180
|
+
?.getDataAsJson();
|
|
181
|
+
return argsAllowRun(prj, blockId, currentArgs);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function allowRun(tree: PlTreeState, blockId: string): Promise<boolean> {
|
|
186
|
+
return allowRunComputable(tree, blockId).getValue();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** A block with model API version 2 that ran with its current args, without errors, with no upstream blocks. */
|
|
190
|
+
function block(overrides: Partial<RunStateSource> & Pick<RunStateSource, "id">): RunStateSource {
|
|
191
|
+
return {
|
|
192
|
+
stale: false,
|
|
193
|
+
outputErrors: false,
|
|
194
|
+
inputsValid: true,
|
|
195
|
+
inputsValidFromArgs: true,
|
|
196
|
+
missingReference: false,
|
|
197
|
+
upstreams: [],
|
|
198
|
+
...overrides,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PlTreeEntry } from "@milaboratories/pl-tree";
|
|
1
|
+
import type { PlTreeEntry, PlTreeNodeAccessor } from "@milaboratories/pl-tree";
|
|
2
2
|
import type { ComputableStableDefined } from "@milaboratories/computable";
|
|
3
3
|
import { Computable } from "@milaboratories/computable";
|
|
4
4
|
import type { ProjectRenderingState, ProjectStructure } from "../model/project_model";
|
|
@@ -31,6 +31,7 @@ import { getBlockPackInfo } from "./util";
|
|
|
31
31
|
import { resourceIdToString, type SignedResourceId } from "@milaboratories/pl-client";
|
|
32
32
|
import { omitBy, isEqual } from "es-toolkit";
|
|
33
33
|
import { getDebugFlags } from "../debug";
|
|
34
|
+
import { readArgsErrorsField } from "../model/args_derivation";
|
|
34
35
|
|
|
35
36
|
type BlockInfo = {
|
|
36
37
|
argsRid?: SignedResourceId;
|
|
@@ -168,6 +169,7 @@ export function projectOverview(
|
|
|
168
169
|
subtitle,
|
|
169
170
|
tags,
|
|
170
171
|
inputsValid,
|
|
172
|
+
inputsValidFromArgs,
|
|
171
173
|
sdkVersion,
|
|
172
174
|
featureFlags,
|
|
173
175
|
isIncompatibleWithRuntime,
|
|
@@ -260,10 +262,11 @@ export function projectOverview(
|
|
|
260
262
|
}) as ComputableStableDefined<string[]>,
|
|
261
263
|
),
|
|
262
264
|
// inputsValid: for modelAPIVersion 2, it's true if currentArgs exists (args derivation succeeded)
|
|
265
|
+
// and the block's argsIssues() is known to have found no errors in them.
|
|
263
266
|
// For older blocks, use the inputsValid callback from config
|
|
264
267
|
inputsValid:
|
|
265
268
|
cfg.modelAPIVersion === BLOCK_STORAGE_FACADE_VERSION
|
|
266
|
-
? info.currentArguments
|
|
269
|
+
? argsAllowRun(prj, id, info.currentArguments)
|
|
267
270
|
: cfg.inputsValid
|
|
268
271
|
? (computableFromCfgOrRF(
|
|
269
272
|
env,
|
|
@@ -280,6 +283,7 @@ export function projectOverview(
|
|
|
280
283
|
},
|
|
281
284
|
}) as ComputableStableDefined<boolean>)
|
|
282
285
|
: undefined,
|
|
286
|
+
inputsValidFromArgs: cfg.modelAPIVersion === BLOCK_STORAGE_FACADE_VERSION,
|
|
283
287
|
sdkVersion: codeWithInfo?.sdkVersion,
|
|
284
288
|
featureFlags: codeWithInfo?.featureFlags ?? {},
|
|
285
289
|
isIncompatibleWithRuntime: false,
|
|
@@ -317,6 +321,7 @@ export function projectOverview(
|
|
|
317
321
|
settings,
|
|
318
322
|
sections,
|
|
319
323
|
inputsValid,
|
|
324
|
+
inputsValidFromArgs: inputsValidFromArgs === true,
|
|
320
325
|
updateInfo: {},
|
|
321
326
|
currentBlockPack: bp?.info?.source,
|
|
322
327
|
updates,
|
|
@@ -336,32 +341,14 @@ export function projectOverview(
|
|
|
336
341
|
};
|
|
337
342
|
},
|
|
338
343
|
{
|
|
339
|
-
postprocessValue: (value) => {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const stale = b.stale || b.upstreams.findIndex((u) => staleBlocks.has(u)) !== -1;
|
|
348
|
-
const canRun =
|
|
349
|
-
(stale || b.outputErrors) &&
|
|
350
|
-
Boolean(b.inputsValid) &&
|
|
351
|
-
!b.missingReference &&
|
|
352
|
-
b.upstreams.findIndex((u) => cantRun.has(u)) === -1;
|
|
353
|
-
const bb = {
|
|
354
|
-
...b,
|
|
355
|
-
canRun,
|
|
356
|
-
stale,
|
|
357
|
-
updateSuggestions: b.updates?.suggestions ?? [],
|
|
358
|
-
updatedBlockPack: b.updates?.mainSuggestion,
|
|
359
|
-
};
|
|
360
|
-
delete bb["updates"];
|
|
361
|
-
return bb;
|
|
362
|
-
}),
|
|
363
|
-
};
|
|
364
|
-
},
|
|
344
|
+
postprocessValue: (value) => ({
|
|
345
|
+
...value,
|
|
346
|
+
blocks: withRunState(value.blocks).map(({ updates, inputsValidFromArgs: _, ...b }) => ({
|
|
347
|
+
...b,
|
|
348
|
+
updateSuggestions: updates?.suggestions ?? [],
|
|
349
|
+
updatedBlockPack: updates?.mainSuggestion,
|
|
350
|
+
})),
|
|
351
|
+
}),
|
|
365
352
|
onRecalculation: getDebugFlags().logProjectOverviewStat
|
|
366
353
|
? (stats) => {
|
|
367
354
|
console.log(`[projectOverview] ${JSON.stringify(stats)}`);
|
|
@@ -370,3 +357,70 @@ export function projectOverview(
|
|
|
370
357
|
},
|
|
371
358
|
).withStableType();
|
|
372
359
|
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Whether the args of a block with model API version 2 let it run: the block has current args,
|
|
363
|
+
* and its `inputsValid` field records no errors its `argsIssues()` found in them (see
|
|
364
|
+
* `readArgsErrorsField`).
|
|
365
|
+
*
|
|
366
|
+
* @param currentArgs - The block's current args as decoded from its `currentArgs` field;
|
|
367
|
+
* `undefined` when it has none
|
|
368
|
+
*/
|
|
369
|
+
export function argsAllowRun(
|
|
370
|
+
prj: PlTreeNodeAccessor,
|
|
371
|
+
blockId: string,
|
|
372
|
+
currentArgs: unknown,
|
|
373
|
+
): boolean {
|
|
374
|
+
if (currentArgs === undefined) return false;
|
|
375
|
+
const argsErrors = prj.getField({
|
|
376
|
+
field: projectFieldName(blockId, "inputsValid"),
|
|
377
|
+
assertFieldType: "Dynamic",
|
|
378
|
+
stableIfNotFound: true,
|
|
379
|
+
});
|
|
380
|
+
return readArgsErrorsField(argsErrors?.value?.getData(), currentArgs).length === 0;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** What the run state of a block is derived from. */
|
|
384
|
+
export type RunStateSource = {
|
|
385
|
+
id: string;
|
|
386
|
+
/** Whether the block's own args differ from those its results were rendered with. */
|
|
387
|
+
stale: boolean;
|
|
388
|
+
outputErrors: boolean;
|
|
389
|
+
inputsValid: boolean | undefined;
|
|
390
|
+
/**
|
|
391
|
+
* Whether `inputsValid` tells only whether the block's current args may run: a block with model
|
|
392
|
+
* API version 2 this runtime is compatible with. Otherwise it also stands for a block whose code
|
|
393
|
+
* cannot run here, or for checks of a model that predates args issues.
|
|
394
|
+
*/
|
|
395
|
+
inputsValidFromArgs: boolean;
|
|
396
|
+
missingReference: boolean;
|
|
397
|
+
/** Every block upstream of this one, direct or not. */
|
|
398
|
+
upstreams: string[];
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Adds the run state to each block of a project, the blocks listed upstream first. A block is
|
|
403
|
+
* stale when its own args changed or an upstream block is stale. It can run when it needs to run
|
|
404
|
+
* (it is stale or its outputs failed), its args allow it, it references no missing block, and no
|
|
405
|
+
* upstream block that needs to run is kept from running. An upstream block whose `inputsValid`
|
|
406
|
+
* comes from its args and whose results are up to date feeds its dependents even when its args
|
|
407
|
+
* could not run again; any other upstream block that is not valid keeps them from running.
|
|
408
|
+
*/
|
|
409
|
+
export function withRunState<B extends RunStateSource>(
|
|
410
|
+
blocks: B[],
|
|
411
|
+
): (B & { stale: boolean; canRun: boolean })[] {
|
|
412
|
+
const cantRun = new Set<string>();
|
|
413
|
+
const staleBlocks = new Set<string>();
|
|
414
|
+
return blocks.map((b) => {
|
|
415
|
+
if (b.stale) staleBlocks.add(b.id);
|
|
416
|
+
const stale = b.stale || b.upstreams.some((u) => staleBlocks.has(u));
|
|
417
|
+
const needsRun = stale || b.outputErrors;
|
|
418
|
+
if (!b.inputsValid && (needsRun || !b.inputsValidFromArgs)) cantRun.add(b.id);
|
|
419
|
+
const canRun =
|
|
420
|
+
needsRun &&
|
|
421
|
+
Boolean(b.inputsValid) &&
|
|
422
|
+
!b.missingReference &&
|
|
423
|
+
!b.upstreams.some((u) => cantRun.has(u));
|
|
424
|
+
return { ...b, stale, canRun };
|
|
425
|
+
});
|
|
426
|
+
}
|
package/src/model/args.test.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { describe, expect, it, test } from "vitest";
|
|
2
2
|
import canonicalize from "canonicalize";
|
|
3
3
|
import type { ColumnUniversalId } from "@milaboratories/pl-model-common";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
canonicalizeJson,
|
|
6
|
+
createColumnDiscoveredId,
|
|
7
|
+
createColumnValueId,
|
|
8
|
+
createGlobalPObjectId,
|
|
9
|
+
} from "@milaboratories/pl-model-common";
|
|
5
10
|
import { inferAllReferencedBlocks, outputRef } from "./args";
|
|
6
11
|
|
|
7
12
|
describe("inferAllReferencedBlocks", () => {
|
|
@@ -158,3 +163,36 @@ test("a block id in a map KEY is an upstream too", () => {
|
|
|
158
163
|
|
|
159
164
|
expect([...result.upstreams].sort()).toEqual(["A", "B", "D"]);
|
|
160
165
|
});
|
|
166
|
+
|
|
167
|
+
describe("a column value id names its producer as an upstream", () => {
|
|
168
|
+
const value = createColumnValueId({
|
|
169
|
+
column: createGlobalPObjectId("samples", "metadata.condition"),
|
|
170
|
+
value: "treated",
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("as a value", () => {
|
|
174
|
+
const result = inferAllReferencedBlocks({ target: value });
|
|
175
|
+
|
|
176
|
+
expect(result.upstreams).toEqual(new Set(["samples"]));
|
|
177
|
+
expect(result.missingReferences).toBe(false);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("as a map key", () => {
|
|
181
|
+
const result = inferAllReferencedBlocks({ ranks: { [value]: 1 } });
|
|
182
|
+
|
|
183
|
+
expect(result.upstreams).toEqual(new Set(["samples"]));
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("under one extra stringify pass", () => {
|
|
187
|
+
const result = inferAllReferencedBlocks({ blob: JSON.stringify(value) });
|
|
188
|
+
|
|
189
|
+
expect(result.upstreams).toEqual(new Set(["samples"]));
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("outside the allowed set it is a missing reference", () => {
|
|
193
|
+
const result = inferAllReferencedBlocks({ target: value }, new Set(["other"]));
|
|
194
|
+
|
|
195
|
+
expect(result.upstreams.size).toBe(0);
|
|
196
|
+
expect(result.missingReferences).toBe(true);
|
|
197
|
+
});
|
|
198
|
+
});
|