@milaboratories/pl-middle-layer 1.75.0 → 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_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/index.d.ts +2 -1
- package/dist/middle_layer/project.cjs +8 -0
- 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 +8 -0
- package/dist/middle_layer/project.js.map +1 -1
- package/dist/model/project_model.cjs +1 -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 +1 -1
- package/dist/model/project_model.js.map +1 -1
- package/dist/mutator/project.cjs +19 -4
- package/dist/mutator/project.cjs.map +1 -1
- package/dist/mutator/project.d.ts.map +1 -1
- package/dist/mutator/project.js +19 -4
- 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 +7 -7
- 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/index.ts +6 -0
- package/src/middle_layer/project.ts +13 -0
- package/src/model/project_model.ts +5 -2
- package/src/mutator/project.ts +47 -13
- 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
|
@@ -43,6 +43,8 @@ import { setTimeout } from "node:timers/promises";
|
|
|
43
43
|
import { frontendData } from "./frontend_path";
|
|
44
44
|
import type { NavigationState } from "@milaboratories/pl-model-common";
|
|
45
45
|
import { getBlockParameters, blockOutputs } from "./block";
|
|
46
|
+
import type { BlockActionContextMode, BlockActionContextNode } from "./block_action_context";
|
|
47
|
+
import { readBlockActionContextTree } from "./block_action_context";
|
|
46
48
|
import { getBlockPackInfo } from "./util";
|
|
47
49
|
import type { FrontendData } from "../model/frontend";
|
|
48
50
|
import type { ProjectId, ProjectStructure } from "../model/project_model";
|
|
@@ -818,6 +820,17 @@ export class Project {
|
|
|
818
820
|
* Returns a computable, that can be used to retrieve and watch full block state,
|
|
819
821
|
* including outputs, arguments, ui state.
|
|
820
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
|
+
|
|
821
834
|
public getBlockState(blockId: string): Computable<BlockStateInternalV3> {
|
|
822
835
|
return this.getBlockComputables(blockId).fullState;
|
|
823
836
|
}
|
|
@@ -123,7 +123,10 @@ export interface ProjectField {
|
|
|
123
123
|
| "stagingOutput"
|
|
124
124
|
| "stagingCtxPrevious"
|
|
125
125
|
| "stagingUiCtxPrevious"
|
|
126
|
-
| "stagingOutputPrevious"
|
|
126
|
+
| "stagingOutputPrevious"
|
|
127
|
+
// Root action context of the current render. The project holds it so it outlives the render.
|
|
128
|
+
| "prodActionContext"
|
|
129
|
+
| "stagingActionContext";
|
|
127
130
|
}
|
|
128
131
|
|
|
129
132
|
export const FieldsToDuplicate: Set<ProjectField["fieldName"]> = new Set([
|
|
@@ -144,7 +147,7 @@ export function projectFieldName(blockId: string, fieldName: ProjectField["field
|
|
|
144
147
|
}
|
|
145
148
|
|
|
146
149
|
const projectFieldPattern =
|
|
147
|
-
/^(?<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)$/;
|
|
148
151
|
|
|
149
152
|
export function parseProjectField(name: string): ProjectField | undefined {
|
|
150
153
|
const match = name.match(projectFieldPattern);
|
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";
|
|
@@ -732,6 +733,15 @@ export class ProjectMutator {
|
|
|
732
733
|
return { ref, value: jsonBytes, status: "Ready" };
|
|
733
734
|
}
|
|
734
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
|
+
|
|
735
745
|
private getBlock(blockId: string): Block {
|
|
736
746
|
for (const block of allBlocks(this.struct)) if (block.id === blockId) return block;
|
|
737
747
|
throw new Error("block not found");
|
|
@@ -834,6 +844,7 @@ export class ProjectMutator {
|
|
|
834
844
|
this.setBlockFieldObj(blockId, "stagingCtxPrevious", fields.stagingCtx!);
|
|
835
845
|
this.setBlockFieldObj(blockId, "stagingUiCtxPrevious", fields.stagingUiCtx!);
|
|
836
846
|
}
|
|
847
|
+
this.deleteBlockFields(blockId, "stagingActionContext");
|
|
837
848
|
if (this.deleteBlockFields(blockId, "stagingOutput", "stagingCtx", "stagingUiCtx"))
|
|
838
849
|
this.resetStagingRefreshTimestamp();
|
|
839
850
|
}
|
|
@@ -849,7 +860,14 @@ export class ProjectMutator {
|
|
|
849
860
|
this.setBlockFieldObj(blockId, "prodCtxPrevious", fields.prodCtx!);
|
|
850
861
|
this.setBlockFieldObj(blockId, "prodUiCtxPrevious", fields.prodUiCtx!);
|
|
851
862
|
}
|
|
852
|
-
this.deleteBlockFields(
|
|
863
|
+
this.deleteBlockFields(
|
|
864
|
+
blockId,
|
|
865
|
+
"prodOutput",
|
|
866
|
+
"prodCtx",
|
|
867
|
+
"prodUiCtx",
|
|
868
|
+
"prodArgs",
|
|
869
|
+
"prodActionContext",
|
|
870
|
+
);
|
|
853
871
|
}
|
|
854
872
|
|
|
855
873
|
/** Running blocks are reset, settled ones (Ready or finished Error) moved to limbo. Returns if
|
|
@@ -878,6 +896,7 @@ export class ProjectMutator {
|
|
|
878
896
|
return true;
|
|
879
897
|
} else {
|
|
880
898
|
// reset - clean up any partial/inconsistent production stat
|
|
899
|
+
this.deleteBlockFields(blockId, "prodActionContext");
|
|
881
900
|
return this.deleteBlockFields(
|
|
882
901
|
blockId,
|
|
883
902
|
"prodOutput",
|
|
@@ -1214,12 +1233,17 @@ export class ProjectMutator {
|
|
|
1214
1233
|
const tpl = info.getTemplate(this.tx);
|
|
1215
1234
|
|
|
1216
1235
|
// Use currentPrerunArgs for staging rendering
|
|
1217
|
-
const results = createRenderHeavyBlock(
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
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
|
+
);
|
|
1223
1247
|
|
|
1224
1248
|
// Here we set the staging ctx to the input context of the staging workflow, not the output because exports
|
|
1225
1249
|
// of one staging context should stay within the same block, and not travel downstream.
|
|
@@ -1230,6 +1254,8 @@ export class ProjectMutator {
|
|
|
1230
1254
|
// thus creating a certain discrepancy between staging workflow context behavior and desktop's result pool.
|
|
1231
1255
|
this.setBlockField(blockId, "stagingUiCtx", this.exportCtx(results.context), "NotReady");
|
|
1232
1256
|
this.setBlockField(blockId, "stagingOutput", results.result, "NotReady");
|
|
1257
|
+
if (results.actionContext !== undefined)
|
|
1258
|
+
this.setBlockField(blockId, "stagingActionContext", results.actionContext, "NotReady");
|
|
1233
1259
|
}
|
|
1234
1260
|
|
|
1235
1261
|
private renderProductionFor(blockId: string) {
|
|
@@ -1256,12 +1282,17 @@ export class ProjectMutator {
|
|
|
1256
1282
|
|
|
1257
1283
|
const tpl = info.getTemplate(this.tx);
|
|
1258
1284
|
|
|
1259
|
-
const results = createRenderHeavyBlock(
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
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
|
+
);
|
|
1265
1296
|
this.setBlockField(
|
|
1266
1297
|
blockId,
|
|
1267
1298
|
"prodCtx",
|
|
@@ -1270,6 +1301,8 @@ export class ProjectMutator {
|
|
|
1270
1301
|
);
|
|
1271
1302
|
this.setBlockField(blockId, "prodUiCtx", this.exportCtx(results.context), "NotReady");
|
|
1272
1303
|
this.setBlockField(blockId, "prodOutput", results.result, "NotReady");
|
|
1304
|
+
if (results.actionContext !== undefined)
|
|
1305
|
+
this.setBlockField(blockId, "prodActionContext", results.actionContext, "NotReady");
|
|
1273
1306
|
|
|
1274
1307
|
// saving inputs for which we rendered the production
|
|
1275
1308
|
this.setBlockFieldObj(blockId, "prodArgs", info.fields.currentArgs);
|
|
@@ -1794,6 +1827,7 @@ export class ProjectMutator {
|
|
|
1794
1827
|
// skipping finished blocks
|
|
1795
1828
|
continue;
|
|
1796
1829
|
|
|
1830
|
+
this.deleteBlockFields(blockId, "prodActionContext");
|
|
1797
1831
|
if (this.deleteBlockFields(blockId, "prodOutput", "prodCtx", "prodUiCtx", "prodArgs")) {
|
|
1798
1832
|
// was actually stopped
|
|
1799
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
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AnyRef, PlTransaction, ResourceRef, ResourceType } from "@milaboratories/pl-client";
|
|
2
2
|
import { field, Pl } from "@milaboratories/pl-client";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
|
-
import {
|
|
4
|
+
import { createRenderTemplateWithResource } from "./render_template";
|
|
5
5
|
|
|
6
6
|
export const BContextEnd: ResourceType = { name: "BContextEnd", version: "1" };
|
|
7
7
|
export const BContext: ResourceType = { name: "BContext", version: "1" };
|
|
@@ -30,12 +30,63 @@ export type HeavyBlockOutputs = {
|
|
|
30
30
|
|
|
31
31
|
export const HeavyBlockOutputNames: (keyof HeavyBlockOutputs)[] = ["context", "result"];
|
|
32
32
|
|
|
33
|
+
/** Baggage of the root action context of a block render. See the block action context spec. */
|
|
34
|
+
export type BlockActionContextBaggage = {
|
|
35
|
+
/** Display name of the block. */
|
|
36
|
+
name: string;
|
|
37
|
+
/** JSON of the block pack spec. */
|
|
38
|
+
blockPack: string;
|
|
39
|
+
blockId: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** Resource type of a block action context. See ActionContextV1Type in the backend. */
|
|
43
|
+
export const ActionContextType: ResourceType = { name: "ActionContext", version: "1" };
|
|
44
|
+
|
|
45
|
+
/** The capability under which the backend accepts the "actionContext" link on a render. */
|
|
46
|
+
export const ActionContextLinkCapability = "optionalFieldsInSchemas:v1";
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Creates the root action context of a block render: an ActionContext resource carrying the
|
|
50
|
+
* baggage, with a new random `run-id`, since each render is one run of the block and the contexts
|
|
51
|
+
* the templates create copy it. Undefined without baggage, or on a backend that cannot take the link.
|
|
52
|
+
*/
|
|
53
|
+
function createBlockActionContext(
|
|
54
|
+
tx: PlTransaction,
|
|
55
|
+
baggage: BlockActionContextBaggage | undefined,
|
|
56
|
+
): ResourceRef | undefined {
|
|
57
|
+
if (baggage === undefined || !tx.hasCapability(ActionContextLinkCapability)) return undefined;
|
|
58
|
+
return tx.createEphemeral(
|
|
59
|
+
ActionContextType,
|
|
60
|
+
JSON.stringify({
|
|
61
|
+
name: baggage.name,
|
|
62
|
+
"block-pack": baggage.blockPack,
|
|
63
|
+
"block-id": baggage.blockId,
|
|
64
|
+
"run-id": randomUUID(),
|
|
65
|
+
}),
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Creates the heavy block render, and with `baggage` its root action context in the same
|
|
71
|
+
* transaction. Returns `actionContext`: the render is garbage collected after its outputs resolve,
|
|
72
|
+
* so the caller must hold the context to keep it.
|
|
73
|
+
*/
|
|
33
74
|
export function createRenderHeavyBlock(
|
|
34
75
|
tx: PlTransaction,
|
|
35
76
|
tpl: AnyRef,
|
|
36
77
|
inputs: HeavyBlockInputs,
|
|
37
|
-
|
|
38
|
-
|
|
78
|
+
baggage?: BlockActionContextBaggage,
|
|
79
|
+
): HeavyBlockOutputs & { actionContext?: ResourceRef } {
|
|
80
|
+
const actionContext = createBlockActionContext(tx, baggage);
|
|
81
|
+
const { outputs } = createRenderTemplateWithResource(
|
|
82
|
+
tx,
|
|
83
|
+
tpl,
|
|
84
|
+
true,
|
|
85
|
+
inputs,
|
|
86
|
+
HeavyBlockOutputNames,
|
|
87
|
+
actionContext,
|
|
88
|
+
);
|
|
89
|
+
return actionContext === undefined ? outputs : { ...outputs, actionContext };
|
|
39
90
|
}
|
|
40
91
|
|
|
41
92
|
export type LightBlockInputs = {
|
|
@@ -51,12 +102,26 @@ export type LightBlockOutput = {
|
|
|
51
102
|
|
|
52
103
|
export const LightBlockOutputNames: (keyof LightBlockOutput)[] = ["result"];
|
|
53
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Creates the light block render, with the same action context as a heavy one, so template code
|
|
107
|
+
* of either kind knows its block and run.
|
|
108
|
+
*/
|
|
54
109
|
export function createRenderLightBlock(
|
|
55
110
|
tx: PlTransaction,
|
|
56
111
|
tpl: ResourceRef,
|
|
57
112
|
inputs: LightBlockInputs,
|
|
58
|
-
|
|
59
|
-
|
|
113
|
+
baggage?: BlockActionContextBaggage,
|
|
114
|
+
): LightBlockOutput & { actionContext?: ResourceRef } {
|
|
115
|
+
const actionContext = createBlockActionContext(tx, baggage);
|
|
116
|
+
const { outputs } = createRenderTemplateWithResource(
|
|
117
|
+
tx,
|
|
118
|
+
tpl,
|
|
119
|
+
true,
|
|
120
|
+
inputs,
|
|
121
|
+
LightBlockOutputNames,
|
|
122
|
+
actionContext,
|
|
123
|
+
);
|
|
124
|
+
return actionContext === undefined ? outputs : { ...outputs, actionContext };
|
|
60
125
|
}
|
|
61
126
|
|
|
62
127
|
export function createBContextEnd(tx: PlTransaction): ResourceRef {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AnyRef, FieldRef, PlTransaction } from "@milaboratories/pl-client";
|
|
1
|
+
import type { AnyRef, FieldRef, PlTransaction, ResourceRef } from "@milaboratories/pl-client";
|
|
2
2
|
import { field, Pl } from "@milaboratories/pl-client";
|
|
3
3
|
import type { ResourceType } from "@platforma-sdk/model";
|
|
4
4
|
|
|
@@ -27,6 +27,24 @@ export function createRenderTemplate<O extends string>(
|
|
|
27
27
|
inputs: Pl.PlRecord,
|
|
28
28
|
outputNames: O[],
|
|
29
29
|
): Record<O, FieldRef> {
|
|
30
|
+
return createRenderTemplateWithResource(tx, tpl, ephemeral, inputs, outputNames).outputs;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Same as {@link createRenderTemplate}, and also returns the render resource itself.
|
|
35
|
+
*
|
|
36
|
+
* With `actionContext`, the render's "actionContext" service field points to it. The link is created
|
|
37
|
+
* before the inputs are locked: the backend accepts it only in the creating transaction, ahead of
|
|
38
|
+
* any explicit input lock.
|
|
39
|
+
*/
|
|
40
|
+
export function createRenderTemplateWithResource<O extends string>(
|
|
41
|
+
tx: PlTransaction,
|
|
42
|
+
tpl: AnyRef,
|
|
43
|
+
ephemeral: boolean,
|
|
44
|
+
inputs: Pl.PlRecord,
|
|
45
|
+
outputNames: O[],
|
|
46
|
+
actionContext?: AnyRef,
|
|
47
|
+
): { render: ResourceRef; outputs: Record<O, FieldRef> } {
|
|
30
48
|
if (outputNames.length === 0) throw new Error("Zero output names provided");
|
|
31
49
|
const rId = ephemeral ? tx.createEphemeral(EphRenderTemplate) : tx.createStruct(RenderTemplate);
|
|
32
50
|
|
|
@@ -35,7 +53,9 @@ export function createRenderTemplate<O extends string>(
|
|
|
35
53
|
|
|
36
54
|
tx.createField(tplField, "Input", tpl);
|
|
37
55
|
tx.createField(inputsField, "Input", Pl.createPlMap(tx, inputs, ephemeral));
|
|
56
|
+
if (actionContext !== undefined)
|
|
57
|
+
tx.createField(field(rId, "actionContext"), "Service", actionContext);
|
|
38
58
|
tx.lockInputs(rId);
|
|
39
59
|
|
|
40
|
-
return Pl.futureRecord(tx, rId, outputNames, "Output", "outputs/");
|
|
60
|
+
return { render: rId, outputs: Pl.futureRecord(tx, rId, outputNames, "Output", "outputs/") };
|
|
41
61
|
}
|