@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
|
@@ -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,7 @@ 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 { getBlockPackInfo } from "./util";
|
|
41
47
|
import type { FrontendData } from "../model/frontend";
|
|
42
48
|
import type { ProjectId, ProjectStructure } from "../model/project_model";
|
|
43
49
|
import { projectFieldName } from "../model/project_model";
|
|
@@ -56,9 +62,10 @@ import type {
|
|
|
56
62
|
BlockSettings,
|
|
57
63
|
BlockStateInternalV3,
|
|
58
64
|
} from "@milaboratories/pl-model-middle-layer";
|
|
65
|
+
import type { ColumnLink } from "@milaboratories/pl-model-common";
|
|
59
66
|
import { activeConfigs } from "./active_cfg";
|
|
60
67
|
import { NavigationStates } from "./navigation_states";
|
|
61
|
-
import { extractConfig, BLOCK_STORAGE_FACADE_VERSION } from "@platforma-sdk/model";
|
|
68
|
+
import { checkBlockFlag, extractConfig, BLOCK_STORAGE_FACADE_VERSION } from "@platforma-sdk/model";
|
|
62
69
|
import fs from "node:fs/promises";
|
|
63
70
|
import canonicalize from "canonicalize";
|
|
64
71
|
import type { ProjectOverviewLight } from "./project_overview_light";
|
|
@@ -68,6 +75,9 @@ import { cacheBlockPackTemplate } from "../mutator/template/template_cache";
|
|
|
68
75
|
|
|
69
76
|
type BlockStateComputables = {
|
|
70
77
|
readonly fullState: Computable<BlockStateInternalV3>;
|
|
78
|
+
/** Column links of the block, nested into its full state. */
|
|
79
|
+
readonly dependencies: Computable<ColumnLink[] | undefined>;
|
|
80
|
+
readonly dependents: Computable<ColumnLink[] | undefined>;
|
|
71
81
|
};
|
|
72
82
|
|
|
73
83
|
function stringifyForDump(object: unknown): string {
|
|
@@ -94,6 +104,13 @@ function stringifyForDump(object: unknown): string {
|
|
|
94
104
|
});
|
|
95
105
|
}
|
|
96
106
|
|
|
107
|
+
/** Releases what the computables of a block hold. */
|
|
108
|
+
function resetBlockState(computables: BlockStateComputables): void {
|
|
109
|
+
computables.fullState.resetState();
|
|
110
|
+
computables.dependencies.resetState();
|
|
111
|
+
computables.dependents.resetState();
|
|
112
|
+
}
|
|
113
|
+
|
|
97
114
|
/** Data access object, to manipulate and read single opened (!) project data. */
|
|
98
115
|
export class Project {
|
|
99
116
|
/** Data for the left panel, contain basic information about block status. */
|
|
@@ -292,8 +309,8 @@ export class Project {
|
|
|
292
309
|
// Doing cleanup for deleted blocks
|
|
293
310
|
for (const blockId of this.blockComputables.keys()) {
|
|
294
311
|
if (!existingBlocks.has(blockId)) {
|
|
295
|
-
const
|
|
296
|
-
if (
|
|
312
|
+
const computables = this.blockComputables.get(blockId);
|
|
313
|
+
if (computables !== undefined && computables !== null) resetBlockState(computables);
|
|
297
314
|
this.blockComputables.set(blockId, null);
|
|
298
315
|
}
|
|
299
316
|
}
|
|
@@ -749,13 +766,19 @@ export class Project {
|
|
|
749
766
|
if (cached === undefined) {
|
|
750
767
|
// state consists of inputs (args + ui state) and outputs
|
|
751
768
|
const outputs = blockOutputs(this.projectTree.entry(), blockId, this.env);
|
|
769
|
+
const dependencies = blockDependencies(this.projectTree.entry(), blockId, this.env);
|
|
770
|
+
const dependents = blockDependents(this.projectTree.entry(), blockId, this.env);
|
|
752
771
|
const fullState = Computable.make(
|
|
753
772
|
(ctx) => {
|
|
773
|
+
// Links are computed only for a block whose UI reads them.
|
|
774
|
+
const linked = readsColumnLinks(ctx.accessor(this.projectTree.entry()).node(), blockId);
|
|
754
775
|
return {
|
|
755
776
|
parameters: getBlockParameters(this.projectTree.entry(), blockId, ctx),
|
|
756
777
|
outputs,
|
|
757
778
|
navigationState: this.navigationStates.getState(blockId),
|
|
758
779
|
overview: this.overview,
|
|
780
|
+
dependencies: linked ? dependencies : undefined,
|
|
781
|
+
dependents: linked ? dependents : undefined,
|
|
759
782
|
};
|
|
760
783
|
},
|
|
761
784
|
{
|
|
@@ -770,6 +793,9 @@ export class Project {
|
|
|
770
793
|
...v.parameters,
|
|
771
794
|
outputs: newOutputs,
|
|
772
795
|
navigationState: v.navigationState,
|
|
796
|
+
// The block state already nests the overview, so labelling by its titles adds
|
|
797
|
+
// nothing to watch.
|
|
798
|
+
columnLinks: labelBlockColumnLinks({ ...v, blockId }, v.overview?.blocks ?? []),
|
|
773
799
|
} as BlockStateInternalV3;
|
|
774
800
|
},
|
|
775
801
|
},
|
|
@@ -777,6 +803,8 @@ export class Project {
|
|
|
777
803
|
|
|
778
804
|
const computables: BlockStateComputables = {
|
|
779
805
|
fullState: fullState.withPreCalculatedValueTree(),
|
|
806
|
+
dependencies,
|
|
807
|
+
dependents,
|
|
780
808
|
};
|
|
781
809
|
|
|
782
810
|
this.blockComputables.set(blockId, computables);
|
|
@@ -839,7 +867,7 @@ export class Project {
|
|
|
839
867
|
this.overview.resetState();
|
|
840
868
|
this.blockFrontends.forEach((c) => c.resetState());
|
|
841
869
|
this.blockComputables.forEach((c) => {
|
|
842
|
-
if (c !== null) c
|
|
870
|
+
if (c !== null) resetBlockState(c);
|
|
843
871
|
});
|
|
844
872
|
this.activeConfigs.resetState();
|
|
845
873
|
}
|
|
@@ -1022,6 +1050,12 @@ export function projectTreeTraverseStopRules(): Filter {
|
|
|
1022
1050
|
return finalityStopRules(TreeFinality);
|
|
1023
1051
|
}
|
|
1024
1052
|
|
|
1053
|
+
/** Whether the block's model declares that its UI reads the column links of the block state. */
|
|
1054
|
+
function readsColumnLinks(prj: PlTreeNodeAccessor, blockId: string): boolean {
|
|
1055
|
+
const info = getBlockPackInfo(prj, blockId);
|
|
1056
|
+
return info !== undefined && checkBlockFlag(info.cfg.featureFlags, "supportsColumnLinks");
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1025
1059
|
/** Returns true if sdk version of the block is old and we need to convert
|
|
1026
1060
|
* ErrorLike errors to strings like it was.
|
|
1027
1061
|
* 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
|
+
});
|