@field/cascade 0.11.0 → 0.12.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/AGENTS.md +7 -4
- package/ARCHITECTURE.md +5 -4
- package/CHANGELOG.md +195 -0
- package/README.md +1 -1
- package/dist/assets/AgentPanel-1JugFjE4.css +1 -0
- package/dist/assets/AgentPanel-BDL5MddA.js +5 -0
- package/dist/assets/{CodeEditor-4O7FQ__R.js → CodeEditor-C-xYG-Wn.js} +120 -120
- package/dist/assets/CodePanel-CtXd50m6.js +2 -0
- package/dist/assets/CookInfoPanel-B3NMhKJF.css +1 -0
- package/dist/assets/CookInfoPanel-DG4_TRsX.js +2 -0
- package/dist/assets/CustomNodeDialog-CHfZUs-r.js +1 -0
- package/dist/assets/DefinitionNode-CaRTXQ_b.js +1 -0
- package/dist/assets/DefinitionPanel-C3CxZY9r.js +1 -0
- package/dist/assets/DefinitionPanel-VNLnAiQD.css +1 -0
- package/dist/assets/OpenGraphDialog-BLdr6k_q.js +1 -0
- package/dist/assets/ProjectSettings-ZnvmIAir.js +2 -0
- package/dist/assets/{SettingsDialog-Bz8JxISH.js → SettingsDialog-CBX3Wa87.js} +1 -1
- package/dist/assets/TimelinePanel-CDeR0arY.css +1 -0
- package/dist/assets/TimelinePanel-CWgnFH1e.js +1 -0
- package/dist/assets/{VersionHistoryDialog-D5Z0nJ5-.js → VersionHistoryDialog-BOSoW1R-.js} +1 -1
- package/dist/assets/assert-BO2_erPD.js +1 -0
- package/dist/assets/assert-hvykGCnP.js +1 -0
- package/dist/assets/builder-BmBKcDix.js +1 -0
- package/dist/assets/camera-DFF7YA3F.js +1 -0
- package/dist/assets/checks-DAvyO9Gf.js +1 -0
- package/dist/assets/contactSheet-N1NcaWxw.js +2 -0
- package/dist/assets/{core-Yqi0Ni61.js → core-DB-MkGjj.js} +3 -4
- package/dist/assets/coreTypes-DtnDATZ7.js +1 -0
- package/dist/assets/{cssMode-CxUzc4vp.js → cssMode-DPVckSOc.js} +1 -1
- package/dist/assets/definitions-B227e7f_.js +1 -0
- package/dist/assets/definitions-D0WOUJd7.js +1 -0
- package/dist/assets/{editor.api-BNFb25CX.js → editor.api-Dkutg_Z1.js} +1 -1
- package/dist/assets/expressions-X-Z-GrgW.js +5 -0
- package/dist/assets/file-DAL6IfjV.js +1 -0
- package/dist/assets/filters-D3k1LRMa.js +1 -0
- package/dist/assets/{freemarker2-0ZDnWxeM.js → freemarker2-CLiLKk3B.js} +1 -1
- package/dist/assets/generators-BhDxRAFC.js +1 -0
- package/dist/assets/geo-BsMImJIJ.js +1 -0
- package/dist/assets/geo-C4YngDob.js +2 -0
- package/dist/assets/{handlebars-Cy18MoaD.js → handlebars-CcD3aMeL.js} +1 -1
- package/dist/assets/{html-B-2Dx7nq.js → html-DvZWGfqb.js} +1 -1
- package/dist/assets/{htmlMode-DUAc9B_d.js → htmlMode-DUD3VaoN.js} +1 -1
- package/dist/assets/image-0SfH-snh.js +1 -0
- package/dist/assets/image-Ck-RdGU4.js +2 -0
- package/dist/assets/index-BWnrmIB9.js +137 -0
- package/dist/assets/index-DIaxEONI.css +1 -0
- package/dist/assets/io-BGmt2g0D.js +1 -0
- package/dist/assets/{javascript-DeQsDqP5.js → javascript-Cap2ahgh.js} +1 -1
- package/dist/assets/{jsonMode-BiFwJVSk.js → jsonMode-B0uLjp9C.js} +1 -1
- package/dist/assets/{liquid-BLoHH68a.js → liquid-CnxwhfLn.js} +1 -1
- package/dist/assets/{lspLanguageFeatures-CNFgm1Kv.js → lspLanguageFeatures-Dn9hlKX6.js} +1 -1
- package/dist/assets/math-GeULS-sk.js +1 -0
- package/dist/assets/{mdx-CFXQVNfs.js → mdx-BV3jbrVa.js} +1 -1
- package/dist/assets/pop-KMF_qloi.js +1 -0
- package/dist/assets/preload-helper-Czpn1I53.js +1 -0
- package/dist/assets/{python-Cf7S7p2E.js → python-CGyFk4XK.js} +1 -1
- package/dist/assets/{razor-CMHBcG-g.js → razor-CQfzX3-N.js} +1 -1
- package/dist/assets/remap-5SjXQU-q.js +1 -0
- package/dist/assets/renderMarkdown-ork0Oir7.js +67 -0
- package/dist/assets/studioImages-CAyH-0Pl.js +1 -0
- package/dist/assets/text-ZpX7qNkx.js +2 -0
- package/dist/assets/time-P_OiBhcf.js +1 -0
- package/dist/assets/{tsMode-BEih0GDj.js → tsMode-BOeXzO9H.js} +1 -1
- package/dist/assets/{typescript-COEKYD_a.js → typescript-J1SnE_Tu.js} +1 -1
- package/dist/assets/{workers-BNhAhMNn.js → workers-CYwwTiVF.js} +1 -1
- package/dist/assets/{xml-CPiMpnPl.js → xml-DnPRNzK9.js} +1 -1
- package/dist/assets/{yaml-Bgp1ITLR.js → yaml-BmmYBNa9.js} +1 -1
- package/dist/cli/DefinitionNode-P7AA4TAU.js +15 -0
- package/dist/cli/{Graph-ZJJTRQTM.js → Graph-OB6EWSGL.js} +12 -12
- package/dist/cli/assert-GROGQNCQ.js +25 -0
- package/dist/cli/checks-3WB44XBL.js +189 -0
- package/dist/cli/{chunk-RQZ4SX5R.js → chunk-2JEU3MGB.js} +2 -2
- package/dist/cli/{chunk-WKURDVYP.js → chunk-3RXL5GHB.js} +28 -331
- package/dist/cli/chunk-53LGFPWX.js +138 -0
- package/dist/cli/chunk-5ORUQ6ES.js +267 -0
- package/dist/cli/chunk-73S4JZXK.js +336 -0
- package/dist/cli/{chunk-YAT5Y25U.js → chunk-7QE2ZNSN.js} +3 -3
- package/dist/cli/{chunk-SGJAET2L.js → chunk-BPODRH52.js} +8 -4
- package/dist/cli/{chunk-XWDSKG7N.js → chunk-BZ63XQ3I.js} +2 -2
- package/dist/cli/{chunk-FCDXJ6EK.js → chunk-CTPLRSWF.js} +2 -13
- package/dist/cli/chunk-EU4ZFZ7Y.js +122 -0
- package/dist/cli/{chunk-WIH4TEIS.js → chunk-EZRWJ4G2.js} +1 -1
- package/dist/cli/{chunk-BIJO5OFS.js → chunk-FWMSGKEH.js} +1 -1
- package/dist/cli/{chunk-POMMNKZZ.js → chunk-G23A2BZZ.js} +38 -5
- package/dist/cli/{chunk-35D343DC.js → chunk-GRIN2DDP.js} +19 -1
- package/dist/cli/chunk-JVOCK5W4.js +83 -0
- package/dist/cli/chunk-KBGUO27Q.js +15 -0
- package/dist/cli/{chunk-GFS6W3C5.js → chunk-L6B6FUQP.js} +54 -32
- package/dist/cli/chunk-LTXWBXOM.js +438 -0
- package/dist/cli/chunk-MI5JLQYA.js +220 -0
- package/dist/cli/{chunk-EUIAC3U3.js → chunk-MP653A5O.js} +884 -124
- package/dist/cli/{chunk-A4OAMOZH.js → chunk-NJFMDXHR.js} +13 -3
- package/dist/cli/{chunk-3JTJHF7V.js → chunk-OM6BEME6.js} +4 -4
- package/dist/cli/{chunk-DUIUFVU2.js → chunk-ST6LROUG.js} +49 -23
- package/dist/cli/{chunk-RLCAJBVR.js → chunk-TJW67QSI.js} +8 -7
- package/dist/cli/chunk-WGKPJZDY.js +364 -0
- package/dist/cli/{chunk-D6BUOHUG.js → chunk-X4WKAOQP.js} +4 -4
- package/dist/cli/{chunk-VVGSLVSF.js → chunk-X566FWBV.js} +6 -1
- package/dist/cli/chunk-YH637PDL.js +317 -0
- package/dist/cli/chunk-ZSCUZE6I.js +112 -0
- package/dist/cli/{cliCommands-UQSFGEHF.js → cliCommands-JJN5Z3ZC.js} +23 -17
- package/dist/cli/{commands-Q23M26QM.js → commands-PHYFYA3F.js} +129 -47
- package/dist/cli/contactSheet-2PQVRUFW.js +11 -0
- package/dist/cli/{definitions-HF2FTHYU.js → definitions-BPOPW52L.js} +7 -7
- package/dist/cli/{doctorCommand-B7BPQMN6.js → doctorCommand-ORWQRNYP.js} +36 -6
- package/dist/cli/{edit-JHCPTQFT.js → edit-S5KEQ5RA.js} +3 -3
- package/dist/cli/editorCommands-NFR32JOY.js +41 -0
- package/dist/cli/{encode-HAUZTYPT.js → encode-ICPEPMH5.js} +2 -2
- package/dist/cli/file-3HTQHPYQ.js +41 -0
- package/dist/cli/filters-6ODFWYK4.js +379 -0
- package/dist/cli/generators-YU7GB4MD.js +210 -0
- package/dist/cli/{geo-4RNK3QVN.js → geo-6ZPGKZRU.js} +7 -7
- package/dist/cli/{graphCommands-6BMAKGQE.js → graphCommands-EOUOBOIJ.js} +4 -4
- package/dist/cli/{graphFile-VPSQ6OF7.js → graphFile-YTGJB5F5.js} +25 -19
- package/dist/cli/{headlessCanvas-5XLDZT7M.js → headlessCanvas-ENRHC7GS.js} +3 -2
- package/dist/cli/{helpCommand-3VRE3KVO.js → helpCommand-PULH6ZYB.js} +9 -7
- package/dist/cli/image-TXELZZK5.js +55 -0
- package/dist/cli/index.js +14 -14
- package/dist/cli/{math-ZRMLKU2D.js → math-RSYRQ3C6.js} +5 -5
- package/dist/cli/{mcp-ZLEEMYGF.js → mcp-JVZFQOEX.js} +4 -4
- package/dist/cli/{parity-KINARCP2.js → parity-X6DEH2DV.js} +42 -36
- package/dist/cli/{pop-USDFXB7H.js → pop-B7SIIYKN.js} +5 -5
- package/dist/cli/project-VYGIA7JX.js +12 -0
- package/dist/cli/{projectDefinition-DMTZN2NE.js → projectDefinition-F3D7UIHJ.js} +8 -8
- package/dist/cli/{projectRuntime-Z5PJOH47.js → projectRuntime-2Q4JKEH3.js} +19 -12
- package/dist/cli/registerStandardNodes-BXJCB6E4.js +744 -0
- package/dist/cli/{render-YURAPPUI.js → render-PT4MWI6U.js} +24 -22
- package/dist/cli/{runCommand-PI764LEG.js → runCommand-HNPQVZMS.js} +3 -3
- package/dist/cli/runner-5XPXVW7J.js +43 -0
- package/dist/cli/runtime/image.ts +27 -2
- package/dist/cli/runtime/io.ts +78 -0
- package/dist/cli/share-MHF6TNRK.js +166 -0
- package/dist/cli/stageCache-TY2BFZJR.js +15 -0
- package/dist/cli/{stageRunner-KUNIMQKJ.js → stageRunner-KY2EJP7X.js} +2 -2
- package/dist/cli/text-MB3EKR4H.js +126 -0
- package/dist/cli/{verbs-RCE3K4VQ.js → verbs-PI2JH5FI.js} +2 -2
- package/dist/cli/webBuild-WPPMGGW7.js +44 -0
- package/dist/contracts/capabilities.d.ts +18 -1
- package/dist/contracts/color.d.ts +8 -0
- package/dist/contracts/color.js +23 -0
- package/dist/contracts/definition.d.ts +10 -0
- package/dist/contracts/schema.d.ts +1 -1
- package/dist/contracts/schema.js +1 -0
- package/dist/contracts/validate.js +11 -1
- package/dist/contracts/values.d.ts +24 -0
- package/dist/image.d.ts +15 -0
- package/dist/image.js +36 -2
- package/dist/index.html +8 -2
- package/dist/io.d.ts +29 -0
- package/dist/io.js +40 -0
- package/dist/player/app.js +3 -2
- package/dist/player/editor.js +3 -2
- package/dist/player/editorEngine.js +3 -2
- package/dist/runtime/builtins/assert/checks.d.ts +18 -0
- package/dist/runtime/builtins/assert/checks.js +207 -0
- package/dist/runtime/builtins/assert/definitions.d.ts +130 -0
- package/dist/runtime/builtins/assert/definitions.js +54 -0
- package/dist/runtime/builtins/assert/index.d.ts +152 -0
- package/dist/runtime/builtins/assert/index.js +36 -0
- package/dist/runtime/builtins/image/contactSheet.d.ts +3 -165
- package/dist/runtime/builtins/image/contactSheet.js +0 -49
- package/dist/runtime/builtins/image/definitions.d.ts +950 -0
- package/dist/runtime/builtins/image/definitions.js +343 -0
- package/dist/runtime/builtins/image/file.d.ts +8 -0
- package/dist/runtime/builtins/image/file.js +37 -0
- package/dist/runtime/builtins/image/filters.d.ts +8 -0
- package/dist/runtime/builtins/image/filters.js +394 -0
- package/dist/runtime/builtins/image/generators.d.ts +7 -0
- package/dist/runtime/builtins/image/generators.js +144 -0
- package/dist/runtime/builtins/image/index.d.ts +947 -4
- package/dist/runtime/builtins/image/index.js +41 -6
- package/dist/runtime/builtins/image/io.d.ts +100 -0
- package/dist/runtime/builtins/image/io.js +142 -0
- package/dist/runtime/builtins/image/planar.d.ts +55 -0
- package/dist/runtime/builtins/image/planar.js +274 -0
- package/dist/runtime/builtins/image/simplex.d.ts +22 -0
- package/dist/runtime/builtins/image/simplex.js +92 -0
- package/dist/runtime/builtins/image/text.d.ts +4 -0
- package/dist/runtime/builtins/image/text.js +141 -0
- package/dist/runtime/builtins/index.d.ts +3 -0
- package/dist/runtime/builtins/index.js +7 -0
- package/dist/runtime/gpu.d.ts +25 -0
- package/dist/runtime/gpu.js +40 -0
- package/dist/runtime/runtime.d.ts +2 -0
- package/dist/runtime/runtime.js +268 -44
- package/dist/runtime/types.d.ts +30 -0
- package/doc/CLI_REFERENCE.md +21 -3
- package/doc/EDITOR_EXPORT.md +26 -0
- package/doc/MCP.md +2 -1
- package/doc/NODE_REFERENCE.md +286 -14
- package/doc/PROJECT_AUTHORING.md +98 -3
- package/package.json +4 -1
- package/dist/assets/CodePanel-BU-autmq.js +0 -2
- package/dist/assets/CustomNodeDialog-BRpDfOV5.js +0 -1
- package/dist/assets/OpenGraphDialog-BwbRlC0X.js +0 -1
- package/dist/assets/ProjectSettings-DLOXLxcA.js +0 -2
- package/dist/assets/definitions-GreEMXLV.js +0 -1
- package/dist/assets/geo-u0n_jOsD.js +0 -1
- package/dist/assets/image-cAa-Nu9O.js +0 -2062
- package/dist/assets/index-5ZtDAuy2.js +0 -220
- package/dist/assets/index-7vae6YFC.css +0 -1
- package/dist/assets/math-DioIQOPJ.js +0 -1
- package/dist/assets/pop-DC_TQG9t.js +0 -1
- package/dist/cli/chunk-36P5K3SD.js +0 -631
- package/dist/cli/chunk-4UDFYF5G.js +0 -132
- package/dist/cli/chunk-53NMBJAO.js +0 -35
- package/dist/cli/chunk-MIIEFUGG.js +0 -623
- package/dist/cli/chunk-QBBKRNGY.js +0 -90
- package/dist/cli/chunk-WPUDGEFY.js +0 -20
- package/dist/cli/editorCommands-UIF5YLUZ.js +0 -33
- package/dist/cli/project-K3QIPGBB.js +0 -12
- package/dist/cli/registerStandardNodes-S2VLMF6J.js +0 -3081
- package/dist/cli/runner-RA6UKEPD.js +0 -37
- package/dist/cli/webBuild-GT4PZT5D.js +0 -37
- package/dist/runtime/builtins/headless.d.ts +0 -19
- package/dist/runtime/builtins/headless.js +0 -25
package/dist/runtime/runtime.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { bundleFieldType, bundleSchemasCompatible, bundleValueProblem, canConnectTypes, isBundleValue, selectBundleFields, suggestName, unknownFieldMessage, validateNodeDefinition, } from "@field/cascade/contracts";
|
|
1
|
+
import { bundleFieldType, bundleSchemasCompatible, bundleValueProblem, canConnectTypes, Color, isBundleValue, selectBundleFields, suggestName, unknownFieldMessage, validateNodeDefinition, } from "@field/cascade/contracts";
|
|
2
2
|
import { CascadeRuntimeError } from "./error.js";
|
|
3
3
|
import { builtinNodeRegistrations } from "./builtins/index.js";
|
|
4
4
|
import { deserializeChannel, isEmptyChannel } from "./animation/channel.js";
|
|
5
5
|
import { PropAnimator, isBoundProp } from "./params/index.js";
|
|
6
6
|
import { bundleFieldReferences, parseBundleFieldPath, remoteBundlePath, } from "./expressions/bundlePaths.js";
|
|
7
7
|
import { DEFAULT_FPS, MIN_FRAME } from "./expressions/time.js";
|
|
8
|
-
import { isGpuHost } from "./gpu.js";
|
|
8
|
+
import { gpuLimitShortfall, isGpuHost } from "./gpu.js";
|
|
9
9
|
import { DEFAULT_GRAPH_SEED, graphSeed, nodeSeed, seededRandom } from "./seed.js";
|
|
10
10
|
/**
|
|
11
11
|
* Preflight codes that describe something other than "this graph cannot run
|
|
@@ -113,16 +113,23 @@ class MutableAbortSignal {
|
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
export function createRuntime(options) {
|
|
116
|
-
return new Runtime(options.host, [...builtinNodeRegistrations, ...(options.nodes ?? [])]
|
|
116
|
+
return new Runtime(options.host, [...builtinNodeRegistrations, ...(options.nodes ?? [])], {
|
|
117
|
+
values: options.values === "trust" ? "trust" : "freeze",
|
|
118
|
+
concurrency: Math.max(1, Math.floor(Number(options.concurrency ?? 1)) || 1),
|
|
119
|
+
...(options.nodeScope ? { nodeScope: options.nodeScope } : {}),
|
|
120
|
+
});
|
|
117
121
|
}
|
|
122
|
+
const SERIAL_EXECUTION = { values: "freeze", concurrency: 1 };
|
|
118
123
|
class Runtime {
|
|
119
124
|
host;
|
|
125
|
+
execution;
|
|
120
126
|
registrations = new Map();
|
|
121
127
|
graphs = new Set();
|
|
122
128
|
sealed = false;
|
|
123
129
|
disposed = false;
|
|
124
|
-
constructor(host, registrations) {
|
|
130
|
+
constructor(host, registrations, execution = SERIAL_EXECUTION) {
|
|
125
131
|
this.host = host;
|
|
132
|
+
this.execution = execution;
|
|
126
133
|
for (const registration of registrations)
|
|
127
134
|
this.registerNode(registration);
|
|
128
135
|
}
|
|
@@ -166,7 +173,7 @@ class Runtime {
|
|
|
166
173
|
const connections = (document.connections ?? []).map((connection, index) => materializeConnection(connection, index, byId));
|
|
167
174
|
validateCardinality(byId, connections);
|
|
168
175
|
validateTopology(nodes, connections);
|
|
169
|
-
const graph = new Graph(this.host, nodes, connections, () => this.graphs.delete(graph), document.annotations, graphSeed(document));
|
|
176
|
+
const graph = new Graph(this.host, nodes, connections, () => this.graphs.delete(graph), document.annotations, graphSeed(document), this.execution);
|
|
170
177
|
// The graph's own frame rate, saved with it; a run's `fps` still overrides.
|
|
171
178
|
const fps = document.timeline?.fps;
|
|
172
179
|
if (typeof fps === "number" && Number.isFinite(fps) && fps > 0)
|
|
@@ -188,6 +195,7 @@ class Graph {
|
|
|
188
195
|
connections;
|
|
189
196
|
onDispose;
|
|
190
197
|
seed;
|
|
198
|
+
execution;
|
|
191
199
|
graphState = "ready";
|
|
192
200
|
byId;
|
|
193
201
|
dataConnectionsByInput;
|
|
@@ -213,14 +221,20 @@ class Graph {
|
|
|
213
221
|
animator;
|
|
214
222
|
frame = MIN_FRAME;
|
|
215
223
|
fps = DEFAULT_FPS;
|
|
224
|
+
/** What happens to a value a node hands over; see `RuntimeValuePolicy`. */
|
|
225
|
+
values;
|
|
216
226
|
constructor(host, nodes, connections, onDispose, annotations = [],
|
|
217
227
|
/** The document's seed; each node's `context.seed` derives from it. */
|
|
218
|
-
seed = DEFAULT_GRAPH_SEED
|
|
228
|
+
seed = DEFAULT_GRAPH_SEED,
|
|
229
|
+
/** Value policy, concurrency, node scope; see `CreateRuntimeOptions`. */
|
|
230
|
+
execution = SERIAL_EXECUTION) {
|
|
219
231
|
this.host = host;
|
|
220
232
|
this.nodes = nodes;
|
|
221
233
|
this.connections = connections;
|
|
222
234
|
this.onDispose = onDispose;
|
|
223
235
|
this.seed = seed;
|
|
236
|
+
this.execution = execution;
|
|
237
|
+
this.values = execution.values;
|
|
224
238
|
this.byId = new Map(nodes.map((node) => [node.id, node]));
|
|
225
239
|
const byInput = new Map();
|
|
226
240
|
const byNode = new Map();
|
|
@@ -316,7 +330,7 @@ class Graph {
|
|
|
316
330
|
type: definition.kind === "trigger" ? "trigger" : definition.type,
|
|
317
331
|
connected: this.connectedOutputs.has(`${node.id}\0${name}`),
|
|
318
332
|
...(definition.kind === "data" && node.outputs.has(name)
|
|
319
|
-
? { value:
|
|
333
|
+
? { value: this.expose(node.outputs.get(name)) }
|
|
320
334
|
: {}),
|
|
321
335
|
},
|
|
322
336
|
])),
|
|
@@ -479,7 +493,15 @@ class Graph {
|
|
|
479
493
|
return () => this.listeners.delete(listener);
|
|
480
494
|
}
|
|
481
495
|
getOutput(nodeId, outputName) {
|
|
482
|
-
return
|
|
496
|
+
return this.expose(this.requireNode(nodeId).outputs.get(outputName));
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* An output value as callers see it. Under `"freeze"` every output was
|
|
500
|
+
* deep-frozen when it was set, so this is the value itself (the snapshot is
|
|
501
|
+
* a lookup); under `"trust"` it is handed out as the producer made it.
|
|
502
|
+
*/
|
|
503
|
+
expose(value) {
|
|
504
|
+
return this.values === "trust" ? value : snapshot(value);
|
|
483
505
|
}
|
|
484
506
|
getGraphOutput(name) {
|
|
485
507
|
const node = this.graphOutputs.get(name);
|
|
@@ -490,7 +512,7 @@ class Graph {
|
|
|
490
512
|
getOutputs(nodeId) {
|
|
491
513
|
return immutableMap([...this.requireNode(nodeId).outputs].map(([name, value]) => [
|
|
492
514
|
name,
|
|
493
|
-
|
|
515
|
+
this.expose(value),
|
|
494
516
|
]));
|
|
495
517
|
}
|
|
496
518
|
dispose() {
|
|
@@ -578,19 +600,23 @@ class Graph {
|
|
|
578
600
|
const ordered = topological(this.nodes, this.connections).filter((node) => initial.includes(node) && !insideFeedback.has(node.id));
|
|
579
601
|
/** Nodes that cooked in this run, for `reuse: "strict"`. */
|
|
580
602
|
const cooked = new Set();
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
603
|
+
const skip = (node) => !!reuse && this.reusable(node) && !(reuse === "strict" && this.feedsFrom(node, cooked));
|
|
604
|
+
if (this.execution.concurrency > 1)
|
|
605
|
+
await this.cookConcurrently(ordered, runId, signal, enqueue, skip, cooked);
|
|
606
|
+
else
|
|
607
|
+
for (const node of ordered) {
|
|
608
|
+
if (signal.aborted)
|
|
609
|
+
break;
|
|
610
|
+
if (skip(node)) {
|
|
611
|
+
this.emit({ type: "node:start", runId, nodeId: node.id });
|
|
612
|
+
this.emit({ type: "node:complete", runId, nodeId: node.id });
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
await this.executeNode(node, runId, signal, {}, enqueue);
|
|
616
|
+
cooked.add(node.id);
|
|
617
|
+
if (!signal.aborted)
|
|
618
|
+
node.lastCook = this.cookKey(node);
|
|
588
619
|
}
|
|
589
|
-
await this.executeNode(node, runId, signal, {}, enqueue);
|
|
590
|
-
cooked.add(node.id);
|
|
591
|
-
if (!signal.aborted)
|
|
592
|
-
node.lastCook = this.cookKey(node);
|
|
593
|
-
}
|
|
594
620
|
if (injection && !signal.aborted)
|
|
595
621
|
enqueue(injection.nodeId, injection.inputName, injection.payload, {
|
|
596
622
|
kind: "external",
|
|
@@ -628,7 +654,90 @@ class Graph {
|
|
|
628
654
|
this.active = undefined;
|
|
629
655
|
}
|
|
630
656
|
}
|
|
657
|
+
/**
|
|
658
|
+
* The run's main order with up to `concurrency` nodes cooking at once.
|
|
659
|
+
*
|
|
660
|
+
* Nodes START strictly in topological order: the next node starts once
|
|
661
|
+
* everything it reads has completed and a slot is free, and nothing later
|
|
662
|
+
* in the order overtakes it. So the `node:start` events — and a run trace
|
|
663
|
+
* built from them — come in the same order as a serial run on every run;
|
|
664
|
+
* only completions interleave. A Kahn order puts siblings side by side (the
|
|
665
|
+
* morph's four renders are adjacent), which is where the overlap is.
|
|
666
|
+
*
|
|
667
|
+
* A feedback container, a node with trigger ports and a GPU node cook
|
|
668
|
+
* alone: their state (the carried step, the trigger queue's sequence, one
|
|
669
|
+
* device) is ordered by the serial loop and not worth sharing.
|
|
670
|
+
*
|
|
671
|
+
* On a failure nothing new starts, the nodes already cooking finish, and
|
|
672
|
+
* the failure earliest in the order is the run's, so the message does not
|
|
673
|
+
* depend on which stage happened to answer first.
|
|
674
|
+
*/
|
|
675
|
+
async cookConcurrently(ordered, runId, signal, enqueue, skip, cooked) {
|
|
676
|
+
const position = new Map(ordered.map((node, index) => [node.id, index]));
|
|
677
|
+
const needs = new Map();
|
|
678
|
+
for (const dependency of graphDependencies(this.nodes, this.connections)) {
|
|
679
|
+
if (!position.has(dependency.sourceId) || !position.has(dependency.targetId))
|
|
680
|
+
continue;
|
|
681
|
+
const list = needs.get(dependency.targetId) ?? [];
|
|
682
|
+
list.push(dependency.sourceId);
|
|
683
|
+
needs.set(dependency.targetId, list);
|
|
684
|
+
}
|
|
685
|
+
const done = new Set();
|
|
686
|
+
const running = new Map();
|
|
687
|
+
let exclusiveRunning = false;
|
|
688
|
+
let failure;
|
|
689
|
+
let next = 0;
|
|
690
|
+
for (;;) {
|
|
691
|
+
while (!failure &&
|
|
692
|
+
!signal.aborted &&
|
|
693
|
+
!exclusiveRunning &&
|
|
694
|
+
next < ordered.length &&
|
|
695
|
+
running.size < this.execution.concurrency) {
|
|
696
|
+
const node = ordered[next];
|
|
697
|
+
if (!(needs.get(node.id) ?? []).every((id) => done.has(id)))
|
|
698
|
+
break;
|
|
699
|
+
const alone = cooksAlone(node);
|
|
700
|
+
if (alone && running.size)
|
|
701
|
+
break;
|
|
702
|
+
next++;
|
|
703
|
+
if (skip(node)) {
|
|
704
|
+
this.emit({ type: "node:start", runId, nodeId: node.id });
|
|
705
|
+
this.emit({ type: "node:complete", runId, nodeId: node.id });
|
|
706
|
+
done.add(node.id);
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
if (alone)
|
|
710
|
+
exclusiveRunning = true;
|
|
711
|
+
const index = position.get(node.id);
|
|
712
|
+
const task = this.executeNode(node, runId, signal, {}, enqueue).then(() => {
|
|
713
|
+
cooked.add(node.id);
|
|
714
|
+
if (!signal.aborted)
|
|
715
|
+
node.lastCook = this.cookKey(node);
|
|
716
|
+
done.add(node.id);
|
|
717
|
+
}, (error) => {
|
|
718
|
+
if (!failure || index < failure.index)
|
|
719
|
+
failure = { index, error };
|
|
720
|
+
}).finally(() => {
|
|
721
|
+
running.delete(node.id);
|
|
722
|
+
if (alone)
|
|
723
|
+
exclusiveRunning = false;
|
|
724
|
+
});
|
|
725
|
+
running.set(node.id, task);
|
|
726
|
+
}
|
|
727
|
+
if (!running.size)
|
|
728
|
+
break;
|
|
729
|
+
await Promise.race(running.values());
|
|
730
|
+
}
|
|
731
|
+
if (failure)
|
|
732
|
+
throw failure.error;
|
|
733
|
+
}
|
|
631
734
|
async executeNode(node, runId, signal, triggers, enqueue) {
|
|
735
|
+
const scope = this.execution.nodeScope;
|
|
736
|
+
if (!scope)
|
|
737
|
+
return this.cookNode(node, runId, signal, triggers, enqueue);
|
|
738
|
+
return scope(node.id, () => this.cookNode(node, runId, signal, triggers, enqueue));
|
|
739
|
+
}
|
|
740
|
+
async cookNode(node, runId, signal, triggers, enqueue) {
|
|
632
741
|
this.emit({ type: "node:start", runId, nodeId: node.id });
|
|
633
742
|
/**
|
|
634
743
|
* `cascade.core.Previous` reads the carried state off its container, the
|
|
@@ -710,7 +819,7 @@ class Graph {
|
|
|
710
819
|
const definition = node.definition.outputs?.[name];
|
|
711
820
|
if (!definition || definition.kind !== "data")
|
|
712
821
|
continue;
|
|
713
|
-
const value = outputValue(response.outputs[name], definition, `${node.id}.${name}
|
|
822
|
+
const value = outputValue(response.outputs[name], definition, `${node.id}.${name}`, this.values);
|
|
714
823
|
node.outputs.set(name, value);
|
|
715
824
|
this.emit({
|
|
716
825
|
type: "node:output",
|
|
@@ -739,7 +848,7 @@ class Graph {
|
|
|
739
848
|
}),
|
|
740
849
|
}
|
|
741
850
|
: {
|
|
742
|
-
set: (value) => pendingData.set(name, outputValue(value, definition, `${node.id}.${name}
|
|
851
|
+
set: (value) => pendingData.set(name, outputValue(value, definition, `${node.id}.${name}`, this.values)),
|
|
743
852
|
},
|
|
744
853
|
]));
|
|
745
854
|
const capabilityEntries = [];
|
|
@@ -747,7 +856,11 @@ class Graph {
|
|
|
747
856
|
const installed = this.host.capabilities[name];
|
|
748
857
|
if (name === "gpu" && isGpuHost(installed)) {
|
|
749
858
|
await installed.ensure();
|
|
750
|
-
|
|
859
|
+
const gpu = installed.forNode(node.id);
|
|
860
|
+
const shortfall = gpuLimitShortfall(node.id, node.definition.gpuLimits, gpu);
|
|
861
|
+
if (shortfall)
|
|
862
|
+
throw new Error(shortfall);
|
|
863
|
+
capabilityEntries.push([name, gpu]);
|
|
751
864
|
}
|
|
752
865
|
else {
|
|
753
866
|
capabilityEntries.push([
|
|
@@ -801,12 +914,14 @@ class Graph {
|
|
|
801
914
|
if (pendingData.has(name)) {
|
|
802
915
|
const value = pendingData.get(name);
|
|
803
916
|
node.outputs.set(name, value);
|
|
917
|
+
// The stored value is already frozen (or trusted), so the event
|
|
918
|
+
// carries it as is rather than a fresh copy nobody may read.
|
|
804
919
|
this.emit({
|
|
805
920
|
type: "node:output",
|
|
806
921
|
runId,
|
|
807
922
|
nodeId: node.id,
|
|
808
923
|
outputName: name,
|
|
809
|
-
value:
|
|
924
|
+
value: this.expose(value),
|
|
810
925
|
});
|
|
811
926
|
}
|
|
812
927
|
for (const emitted of pendingTriggers)
|
|
@@ -1262,7 +1377,8 @@ class Graph {
|
|
|
1262
1377
|
}
|
|
1263
1378
|
return Object.freeze({
|
|
1264
1379
|
result: carried,
|
|
1265
|
-
history
|
|
1380
|
+
// Recorded as settled, so reading `history` back is not a copy.
|
|
1381
|
+
history: settle(history),
|
|
1266
1382
|
});
|
|
1267
1383
|
}
|
|
1268
1384
|
finally {
|
|
@@ -1406,7 +1522,10 @@ function materialize(authored, registration) {
|
|
|
1406
1522
|
const object = saved && typeof saved === "object" && "value" in saved
|
|
1407
1523
|
? saved
|
|
1408
1524
|
: null;
|
|
1409
|
-
const
|
|
1525
|
+
const stored = object ? object.value : saved;
|
|
1526
|
+
const value = runtimeValue(
|
|
1527
|
+
// A colour saved as `{ r, g, b }` by the class image nodes reads as one.
|
|
1528
|
+
definition.type === "color" ? Color.upgrade(stored) : stored, definition.type, false, `${authored.id}.${name}`);
|
|
1410
1529
|
props[name] = value;
|
|
1411
1530
|
// A stored plain value is an author's decision and beats the declared
|
|
1412
1531
|
// default expression, so any binding seeded above has to go.
|
|
@@ -1702,6 +1821,18 @@ function snapshotRegistration(registration) {
|
|
|
1702
1821
|
const definition = snapshot(registration.definition);
|
|
1703
1822
|
return Object.freeze({ ...registration, definition });
|
|
1704
1823
|
}
|
|
1824
|
+
/** A node `cookConcurrently` never overlaps with another; see there. */
|
|
1825
|
+
function cooksAlone(node) {
|
|
1826
|
+
if (node.definition.container === "feedback")
|
|
1827
|
+
return true;
|
|
1828
|
+
if (node.definition.capabilities?.includes("gpu"))
|
|
1829
|
+
return true;
|
|
1830
|
+
const ports = [
|
|
1831
|
+
...Object.values(node.definition.inputs ?? {}),
|
|
1832
|
+
...Object.values(node.definition.outputs ?? {}),
|
|
1833
|
+
];
|
|
1834
|
+
return ports.some((port) => port.kind === "trigger");
|
|
1835
|
+
}
|
|
1705
1836
|
function isGated(node) {
|
|
1706
1837
|
return Object.values(node.definition.inputs ?? {}).some((input) => input.kind === "trigger");
|
|
1707
1838
|
}
|
|
@@ -1882,7 +2013,103 @@ function misuse(code, message) {
|
|
|
1882
2013
|
throw runtimeError(code, message);
|
|
1883
2014
|
}
|
|
1884
2015
|
function snapshot(value) {
|
|
1885
|
-
|
|
2016
|
+
const clone = cloneValue(value, new WeakSet());
|
|
2017
|
+
if (clone !== null && typeof clone === "object")
|
|
2018
|
+
settled.add(clone);
|
|
2019
|
+
return clone;
|
|
2020
|
+
}
|
|
2021
|
+
/**
|
|
2022
|
+
* Plain arrays and objects already checked and deep-frozen — by `cloneValue`
|
|
2023
|
+
* or `settle` — so a second snapshot of the same value is a lookup, not a
|
|
2024
|
+
* copy. A runtime value is read far more often than it is made: the producer
|
|
2025
|
+
* sets it once, then the event, `getOutputs`, `finish` and `inspect` each
|
|
2026
|
+
* used to clone it again (a 1M-point array four times a frame).
|
|
2027
|
+
*
|
|
2028
|
+
* Only roots and containers of objects are recorded; a leaf of primitives
|
|
2029
|
+
* (a `[x, y]` point) is cheaper to re-walk than to record a million times.
|
|
2030
|
+
*/
|
|
2031
|
+
const settled = new WeakSet();
|
|
2032
|
+
/** Plain containers copied by `cloneValue`, for the copy-count tests. */
|
|
2033
|
+
let copiedContainers = 0;
|
|
2034
|
+
/** How many plain arrays/objects the runtime has copied in this process. */
|
|
2035
|
+
export function runtimeValueCopies() {
|
|
2036
|
+
return copiedContainers;
|
|
2037
|
+
}
|
|
2038
|
+
function isPlainContainer(value) {
|
|
2039
|
+
if (Array.isArray(value))
|
|
2040
|
+
return true;
|
|
2041
|
+
const prototype = Object.getPrototypeOf(value);
|
|
2042
|
+
return prototype === Object.prototype || prototype === null;
|
|
2043
|
+
}
|
|
2044
|
+
function checkScalar(value) {
|
|
2045
|
+
if (typeof value === "number" && !Number.isFinite(value))
|
|
2046
|
+
misuse("runtime/non-serializable-value", "Runtime numbers must be finite");
|
|
2047
|
+
if (typeof value === "function" ||
|
|
2048
|
+
typeof value === "symbol" ||
|
|
2049
|
+
typeof value === "bigint")
|
|
2050
|
+
misuse("runtime/non-serializable-value", `Runtime values cannot contain ${typeof value}`);
|
|
2051
|
+
}
|
|
2052
|
+
/**
|
|
2053
|
+
* Check a value a node handed over and deep-freeze it where it stands: the
|
|
2054
|
+
* same rules as `cloneValue` (finite numbers, no functions, no cycles, class
|
|
2055
|
+
* instances and typed arrays passed through untouched), without allocating.
|
|
2056
|
+
* The producer gave it away with `set()`; freezing is what keeps the
|
|
2057
|
+
* consumers' view of it from changing under them.
|
|
2058
|
+
*/
|
|
2059
|
+
function settle(value) {
|
|
2060
|
+
if (value === null || typeof value !== "object") {
|
|
2061
|
+
checkScalar(value);
|
|
2062
|
+
return value;
|
|
2063
|
+
}
|
|
2064
|
+
if (!settled.has(value) && isPlainContainer(value)) {
|
|
2065
|
+
settleContainer(value, new WeakSet());
|
|
2066
|
+
settled.add(value);
|
|
2067
|
+
}
|
|
2068
|
+
return value;
|
|
2069
|
+
}
|
|
2070
|
+
/** Returns whether `value` held any plain container (and so was recorded). */
|
|
2071
|
+
function settleContainer(value, ancestors) {
|
|
2072
|
+
let nested = false;
|
|
2073
|
+
const visit = (item) => {
|
|
2074
|
+
if (item === null || typeof item !== "object") {
|
|
2075
|
+
checkScalar(item);
|
|
2076
|
+
return;
|
|
2077
|
+
}
|
|
2078
|
+
if (settled.has(item) || !isPlainContainer(item))
|
|
2079
|
+
return;
|
|
2080
|
+
if (ancestors.has(item))
|
|
2081
|
+
misuse("runtime/non-serializable-value", "Runtime values cannot contain cycles");
|
|
2082
|
+
if (!nested) {
|
|
2083
|
+
nested = true;
|
|
2084
|
+
ancestors.add(value);
|
|
2085
|
+
}
|
|
2086
|
+
if (settleContainer(item, ancestors))
|
|
2087
|
+
settled.add(item);
|
|
2088
|
+
};
|
|
2089
|
+
if (Array.isArray(value))
|
|
2090
|
+
for (let index = 0; index < value.length; index++)
|
|
2091
|
+
visit(value[index]);
|
|
2092
|
+
else
|
|
2093
|
+
for (const key of Object.keys(value))
|
|
2094
|
+
visit(value[key]);
|
|
2095
|
+
if (nested)
|
|
2096
|
+
ancestors.delete(value);
|
|
2097
|
+
Object.freeze(value);
|
|
2098
|
+
return nested;
|
|
2099
|
+
}
|
|
2100
|
+
/**
|
|
2101
|
+
* `"trust"`: the top level is checked by `runtimeValue`'s type test and
|
|
2102
|
+
* frozen shallowly, so `push` or `sort` on a handed-over array still throws;
|
|
2103
|
+
* nested contents are not walked.
|
|
2104
|
+
*/
|
|
2105
|
+
function trust(value) {
|
|
2106
|
+
if (value === null || typeof value !== "object") {
|
|
2107
|
+
checkScalar(value);
|
|
2108
|
+
return value;
|
|
2109
|
+
}
|
|
2110
|
+
if (isPlainContainer(value))
|
|
2111
|
+
Object.freeze(value);
|
|
2112
|
+
return value;
|
|
1886
2113
|
}
|
|
1887
2114
|
function immutableMap(entries) {
|
|
1888
2115
|
const source = new Map(entries);
|
|
@@ -1902,22 +2129,15 @@ function immutableMap(entries) {
|
|
|
1902
2129
|
return view;
|
|
1903
2130
|
}
|
|
1904
2131
|
function cloneValue(value, ancestors) {
|
|
1905
|
-
|
|
1906
|
-
misuse("runtime/non-serializable-value", "Runtime numbers must be finite");
|
|
1907
|
-
if (typeof value === "function" ||
|
|
1908
|
-
typeof value === "symbol" ||
|
|
1909
|
-
typeof value === "bigint")
|
|
1910
|
-
misuse("runtime/non-serializable-value", `Runtime values cannot contain ${typeof value}`);
|
|
2132
|
+
checkScalar(value);
|
|
1911
2133
|
if (value === undefined || value === null || typeof value !== "object")
|
|
1912
2134
|
return value;
|
|
1913
|
-
|
|
1914
|
-
if (!Array.isArray(value) &&
|
|
1915
|
-
prototype !== Object.prototype &&
|
|
1916
|
-
prototype !== null)
|
|
2135
|
+
if (settled.has(value) || !isPlainContainer(value))
|
|
1917
2136
|
return value;
|
|
1918
2137
|
if (ancestors.has(value))
|
|
1919
2138
|
misuse("runtime/non-serializable-value", "Runtime values cannot contain cycles");
|
|
1920
2139
|
ancestors.add(value);
|
|
2140
|
+
copiedContainers++;
|
|
1921
2141
|
const clone = Array.isArray(value)
|
|
1922
2142
|
? value.map((item) => cloneValue(item, ancestors))
|
|
1923
2143
|
: Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
@@ -1943,17 +2163,19 @@ const TUPLE_LENGTHS = {
|
|
|
1943
2163
|
* An output value, checked against its declaration. A bundle with declared
|
|
1944
2164
|
* fields may only set those, and each one must be a valid value of its type.
|
|
1945
2165
|
*/
|
|
1946
|
-
function outputValue(value, definition, path) {
|
|
2166
|
+
function outputValue(value, definition, path, policy) {
|
|
2167
|
+
// Handed over by the producer, so adopted (frozen where it stands), not copied.
|
|
2168
|
+
const adopt = policy === "trust" ? trust : settle;
|
|
1947
2169
|
if (definition.type !== "bundle" || !definition.fields)
|
|
1948
|
-
return runtimeValue(value, definition.type, false, path);
|
|
2170
|
+
return runtimeValue(value, definition.type, false, path, adopt);
|
|
1949
2171
|
const fields = definition.fields;
|
|
1950
2172
|
const problem = bundleValueProblem(value, fields);
|
|
1951
2173
|
if (problem)
|
|
1952
2174
|
misuse("runtime/value-type", `${path} ${problem}`);
|
|
1953
2175
|
if (isBundleValue(value))
|
|
1954
2176
|
for (const [field, item] of Object.entries(value))
|
|
1955
|
-
runtimeValue(item, bundleFieldType(fields[field]), true, `${path}.${field}
|
|
1956
|
-
return runtimeValue(value, "bundle", false, path);
|
|
2177
|
+
runtimeValue(item, bundleFieldType(fields[field]), true, `${path}.${field}`, (checked) => checked);
|
|
2178
|
+
return runtimeValue(value, "bundle", false, path, adopt);
|
|
1957
2179
|
}
|
|
1958
2180
|
/** Every path an expression names through ch/chs/chv. */
|
|
1959
2181
|
function expressionPaths(expression) {
|
|
@@ -1975,7 +2197,9 @@ function readsBundleFields(node) {
|
|
|
1975
2197
|
}
|
|
1976
2198
|
return false;
|
|
1977
2199
|
}
|
|
1978
|
-
function runtimeValue(value, type, allowUndefined, path
|
|
2200
|
+
function runtimeValue(value, type, allowUndefined, path,
|
|
2201
|
+
/** How a checked value is kept: copied (a caller's value) by default. */
|
|
2202
|
+
keep = snapshot) {
|
|
1979
2203
|
if (value === undefined) {
|
|
1980
2204
|
if (allowUndefined)
|
|
1981
2205
|
return undefined;
|
|
@@ -2001,7 +2225,7 @@ function runtimeValue(value, type, allowUndefined, path) {
|
|
|
2001
2225
|
type !== "string");
|
|
2002
2226
|
if (!valid)
|
|
2003
2227
|
misuse("runtime/value-type", `${path} is not a valid ${type}`);
|
|
2004
|
-
return
|
|
2228
|
+
return keep(value);
|
|
2005
2229
|
}
|
|
2006
2230
|
function sameValues(a, b, equal) {
|
|
2007
2231
|
return a.length === b.length && a.every((value, index) => equal(value, b[index]));
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -216,9 +216,39 @@ export type RuntimeEvent = {
|
|
|
216
216
|
readonly type: "run:complete";
|
|
217
217
|
readonly result: RunResult;
|
|
218
218
|
};
|
|
219
|
+
/**
|
|
220
|
+
* What the runtime does with a value a node hands to `outputs.x.set()`.
|
|
221
|
+
*
|
|
222
|
+
* - `"freeze"` (default): checked and deep-frozen in place, never copied. A
|
|
223
|
+
* node that mutates its output after handing it over, or mutates an input,
|
|
224
|
+
* throws instead of corrupting another node's view.
|
|
225
|
+
* - `"trust"`: checked at the top level only and shallow-frozen; nested
|
|
226
|
+
* contents are the producer's word. For headless runs (`cascade run`), where
|
|
227
|
+
* a 1M-point array walked every frame was most of the frame.
|
|
228
|
+
*
|
|
229
|
+
* Either way nothing is cloned on the way through: reading an output back
|
|
230
|
+
* (`getOutputs`, `inspect`, `node:output`) hands out the same frozen value.
|
|
231
|
+
*/
|
|
232
|
+
export type RuntimeValuePolicy = "freeze" | "trust";
|
|
219
233
|
export interface CreateRuntimeOptions {
|
|
220
234
|
readonly host: RuntimeHost;
|
|
221
235
|
readonly nodes?: readonly NodeRegistration[];
|
|
236
|
+
/** See `RuntimeValuePolicy`. Default `"freeze"`. */
|
|
237
|
+
readonly values?: RuntimeValuePolicy;
|
|
238
|
+
/**
|
|
239
|
+
* How many nodes one run may cook at once (default 1: one after another).
|
|
240
|
+
* Nodes still START in topological order, a node only once everything it
|
|
241
|
+
* reads has completed, so the `node:start` sequence and the outputs are the
|
|
242
|
+
* same as a serial run; only completions interleave. Feedback containers,
|
|
243
|
+
* trigger-wired and GPU nodes always cook alone.
|
|
244
|
+
*/
|
|
245
|
+
readonly concurrency?: number;
|
|
246
|
+
/**
|
|
247
|
+
* Wraps each node's execute. A Node host uses it to carry the node id
|
|
248
|
+
* through the cook's async work (AsyncLocalStorage), so a stage call or a
|
|
249
|
+
* log line is attributed to its node when several cook at once.
|
|
250
|
+
*/
|
|
251
|
+
readonly nodeScope?: <T>(nodeId: string, run: () => T) => T;
|
|
222
252
|
}
|
|
223
253
|
export interface CascadeRuntime {
|
|
224
254
|
registerNode(registration: NodeRegistration): void;
|
package/doc/CLI_REFERENCE.md
CHANGED
|
@@ -50,6 +50,7 @@ Running, sharing, locks and Studio. `cascade --help` has their options; `cascade
|
|
|
50
50
|
| `cascade parity <graph-file>` | Cook headless twice and through Studio's path; report per-node differences. |
|
|
51
51
|
| `cascade build <graph-file> --out <new-directory>` | A standalone browser player ([WEB_PLAYER.md](WEB_PLAYER.md)). |
|
|
52
52
|
| `cascade export <graph-file> (--plan \| --out <dir>)` | The graph's published editor as a static site ([EDITOR_EXPORT.md](EDITOR_EXPORT.md)). |
|
|
53
|
+
| `cascade share <graph-file \| site-directory> [--name <slug>] [--scope <team>] [--protected]` | One link for the team: export the editor (or take a static directory with an `index.html`), deploy it to the Vercel project `cascade-<slug>` (team `field-io`), print the production URL (`data.url` with `--json`) and record it in the project's `shares.json`. Public unless `--protected` ([EDITOR_EXPORT.md](EDITOR_EXPORT.md#share-as-a-link)). |
|
|
53
54
|
| `cascade publish <graph-file> <node>.<prop>` | Publish a node prop as an editor parameter. |
|
|
54
55
|
| `cascade freeze <graph-file> <node> --fields <a,b>` | Snapshot chosen fields of a live data source for export. |
|
|
55
56
|
| `cascade scene <graph-file> <scene.json> --out <new-graph-file>` | Apply a scene file to a copy of the graph. |
|
|
@@ -58,7 +59,7 @@ Running, sharing, locks and Studio. `cascade --help` has their options; `cascade
|
|
|
58
59
|
| `cascade lock verify [<lock-id>...]` | Replay locks and report which still match. |
|
|
59
60
|
| `cascade replay <lock-id>` | Re-render a lock from its stored code and compare fingerprints. |
|
|
60
61
|
| `cascade locks` | List the project's locks. |
|
|
61
|
-
| `cascade ladder <graph-file> <node> --name <name> --vary <node.prop>=<a,b,c> \| --seeds <a,b,c
|
|
62
|
+
| `cascade ladder <graph-file> <node> --name <name> --vary <node.prop>=<a,b,c> \| --seeds <a,b,c> [--cairn <vault dir> [--vault <dir>]]` | Cook 3 to 5 variants (values zipped across params, or graph seeds) from the stored code; write `ladders/<id>/<n>.png`, a labelled contact sheet `ladders/<id>/sheet.png` and the record `ladders/<id>.json` (per variant: graph, params, seed, inputs, fingerprint; the code and git commit once). `--cairn` copies each variant to `<dir>/<id>-<nn>.png` and prints a `cairn:choice` card (`--json`: `data.cairnChoice`) to post in the Cairn chat; see [Picking on the phone](#picking-a-ladder-variant-in-cairn). |
|
|
62
63
|
| `cascade pick <ladder-id> <n> [--name <lock name>]` | At approval, write `locks/<id>.json` for variant `n` from the ladder's record, without cooking again; `cascade replay` reproduces that image pixel for pixel. |
|
|
63
64
|
| `cascade help node [<type>] [--project <dir>]` | A node type's inputs, props and outputs (type, default, range, description), where it runs and its capabilities, read from the installed package or the project's `nodes/`; without a type, every node type. No repository checkout needed. |
|
|
64
65
|
| `cascade doctor [project-directory] [--fix]` | What a headless run here will find: the declared Python environment, the stage interpreter, ffmpeg, the canvas. `--fix` builds the environment. See [Python for stages](#python-for-stages-cascadejson-python). |
|
|
@@ -137,6 +138,23 @@ Opened in Studio, the top level reads `motif → transform → render`, with the
|
|
|
137
138
|
|
|
138
139
|
`cascade run <graph> --out <dir>` copies each image output to `<dir>/<node>.<ext>`, or `<dir>/<node>.<port>.<ext>` for a node with more than one image output. With `--frames` the same names carry the frame: `<node>[.<port>].<frame>.<ext>`. Which outputs: every image output nothing downstream consumes (a `cascade.core.Null` passing an image counts), or those of `--node`; `--out-node <id>` (repeatable) adds a mid-graph node's. `--out` may be project-relative or absolute; `files` in the result is spelled the same way. `run --json` lists every image output as `data.images: [{ node, port, source, file? }]`, with or without `--out`. The MCP `cook` and `render` tools take `out` and `outNodes`.
|
|
139
140
|
|
|
141
|
+
## Picking a ladder variant in Cairn
|
|
142
|
+
|
|
143
|
+
Marcus picks on his phone, in the Cairn web chat, by tapping a picture; the tap locks it. The loop for an agent:
|
|
144
|
+
|
|
145
|
+
1. `cascade ladder <graph> <node> --name <name> --seeds 1,2,3 --cairn <vault>/<space>/_attachments/ladders/<id> --json` (`<id>` is the name lower-cased with dashes). Each variant is copied to `<dir>/<id>-<nn>.png`, and `data.cairnChoice` is the card. At most 8 variants.
|
|
146
|
+
2. Post `data.cairnChoice` in the reply exactly as returned:
|
|
147
|
+
|
|
148
|
+
````text
|
|
149
|
+
```cairn:choice
|
|
150
|
+
{"prompt":"Dial: which one? (ladder dial, index.cascade render)","options":[{"label":"seed 1","value":"pick:dial:1","image":"Work/Projects/Cascade/_attachments/ladders/dial/dial-01.png"}, …]}
|
|
151
|
+
```
|
|
152
|
+
````
|
|
153
|
+
|
|
154
|
+
`label` says what the variant changed (`seed 1`, `hue=0.3`); `image` is relative to the vault, the nearest `.obsidian` (then `.git`) above `--cairn`, or `--vault`.
|
|
155
|
+
3. A tap comes back as `Picked: seed 1 (pick:dial:1)`. Run `cascade pick dial 1 --project <project>` (MCP: `pick { "ladder": "dial", "variant": 1 }`).
|
|
156
|
+
4. Reply with the locked image, `![[<that option's image>]]`, and the lock id.
|
|
157
|
+
|
|
140
158
|
## Checks that fail a run (`cascade.assert.*`)
|
|
141
159
|
|
|
142
160
|
Put the check in the graph instead of a script beside it:
|
|
@@ -153,7 +171,7 @@ cascade run index.cascade --json # exit 1: { "ok": false, "error": { "messa
|
|
|
153
171
|
|
|
154
172
|
## Debug output and environment (`.cascade/runs/`, `--env`)
|
|
155
173
|
|
|
156
|
-
Every run's trace in `.cascade/runs/<time>-<graph>.json` holds, per node, what it printed while it cooked: `console.log/info/warn/error/debug` from the node, and its Python stages' stderr, as `logs: [{ level, text }]` (500 lines per node, the rest counted in `logsDropped`). `run --json` gives the same lines as `data.logs: [{ node, level, text }]`. A failed run writes its trace too, and the error ends with `Run trace: <path>`.
|
|
174
|
+
Every run's trace in `.cascade/runs/<time>-<graph>.json` holds, per node, what it printed while it cooked: `console.log/info/warn/error/debug` from the node, and its Python stages' stderr, as `logs: [{ level, text }]` (500 lines per node, the rest counted in `logsDropped`). `run --json` gives the same lines as `data.logs: [{ node, level, text }]`. A failed run writes its trace too, and the error ends with `Run trace: <path>`. Each run (one per frame with `--frames`) carries `ms`, the cook; `frameMs`, the whole frame to its outputs on disk; and `saveMs`, the difference (collecting and writing the outputs), beside each node's own `ms`.
|
|
157
175
|
|
|
158
176
|
`--env NAME=value` (repeatable) sets a variable for the run: Python stages, the warm stage worker and `cascade render`'s worker processes see it, so `CLOUD_ANALYST_ONLINE=1` no longer needs a shell prefix. The trace records the names under `env`, never the values. The MCP `cook` and `render` tools take `env: { NAME: "value" }`.
|
|
159
177
|
|
|
@@ -168,4 +186,4 @@ A project whose stages import numpy or Pillow declares them, and Cascade keeps a
|
|
|
168
186
|
}
|
|
169
187
|
```
|
|
170
188
|
|
|
171
|
-
`requirements` is a list of pip specifiers or a project-relative requirements file; `interpreter` (default `python3`) is what creates the venv. `cascade doctor` reports it as `current`, `missing` or `stale` (cascade.json changed since it was built); `cascade doctor --fix` creates or rebuilds it, moving an old one to `.cascade-cache/python/_archive/`. While one is declared, stages run under its interpreter instead of a bare `python`/`python3` in `exec.stages.python` (a project-relative executable there still wins), and a stage call with the environment missing or stale fails with the command that fixes it, rather than later on an `ImportError`. MCP: `doctor { "fix": true }`.
|
|
189
|
+
`requirements` is a list of pip specifiers or a project-relative requirements file; `interpreter` (default `python3`) is what creates the venv. `cascade doctor` reports it as `current`, `missing` or `stale` (cascade.json changed since it was built); `cascade doctor --fix` creates or rebuilds it, moving an old one to `.cascade-cache/python/_archive/`. With none declared, doctor fails when a stage source imports numpy and the stage interpreter cannot (`stage imports`), which is how a project on the system Python renders nothing. While one is declared, stages run under its interpreter instead of a bare `python`/`python3` in `exec.stages.python` (a project-relative executable there still wins), and a stage call with the environment missing or stale fails with the command that fixes it, rather than later on an `ImportError`. MCP: `doctor { "fix": true }`.
|
package/doc/EDITOR_EXPORT.md
CHANGED
|
@@ -95,6 +95,32 @@ prints the same manifest without building anything.
|
|
|
95
95
|
Vercel Authentication. Add `--public` when the editor is meant to be open to
|
|
96
96
|
anyone: a readable site is then reported as intended instead of failing.
|
|
97
97
|
|
|
98
|
+
## Share as a link
|
|
99
|
+
|
|
100
|
+
`cascade share` is the one step from a graph to a URL for the team:
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
cascade share plotter-social.cascade # https://cascade-plotter-social.vercel.app
|
|
104
|
+
cascade share renders/shelf --name commons-shelf # any static directory with an index.html
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
It exports the editor into a scratch directory with the same allowlist and
|
|
108
|
+
leak scan as `cascade export` (a directory is copied without dotfiles or
|
|
109
|
+
`licensed/` and scanned the same way), deploys it to production in the Vercel
|
|
110
|
+
project `cascade-<slug>` (team `field-io`, `--scope` for another), and prints
|
|
111
|
+
the project's production URL. The slug is `--name`, else the editor id or the
|
|
112
|
+
directory name. `--json` gives `data.url`, `data.deployment` and `data.shares`.
|
|
113
|
+
|
|
114
|
+
Each share is recorded in the project's `shares.json` (the directory holding
|
|
115
|
+
the graph, or the nearest `cascade.json` above a shared directory), one entry
|
|
116
|
+
per source with its `name`, `project`, `url` and `sharedAt`. Sharing again
|
|
117
|
+
replaces the entry and redeploys to the same URL.
|
|
118
|
+
|
|
119
|
+
A share is public: that is what a link someone can open is. The export
|
|
120
|
+
boundary above is the gate, so review `cascade export --plan` first.
|
|
121
|
+
`--protected` instead requires Vercel Authentication on the project, as
|
|
122
|
+
`cascade export --deploy vercel` does without `--public`.
|
|
123
|
+
|
|
98
124
|
## The `editor` section
|
|
99
125
|
|
|
100
126
|
It sits beside `nodes` in the `.cascade` file. The runtime ignores it, and
|
package/doc/MCP.md
CHANGED
|
@@ -36,8 +36,9 @@ With a global install (`npm i -g @field/cascade`), use `"command": "cascade", "a
|
|
|
36
36
|
| `cook` | `cascade run <graph> [--node <id>]` |
|
|
37
37
|
| `render` | `cascade run <graph> --frames … [--fps] [--out] [--node]… [--scene]` |
|
|
38
38
|
| `export` | `cascade export <graph> --plan \| --out <dir>` |
|
|
39
|
+
| `share` | `cascade share <graph \| directory> [--name] [--scope] [--protected]` (deploys; `data.url` is the link) |
|
|
39
40
|
| `lock`, `replay`, `locks` | `cascade lock <graph> <node> --name`, `cascade replay <id> [--live]`, `cascade locks` |
|
|
40
|
-
| `ladder`, `pick` | `cascade ladder <graph> <node> --name --vary node.prop=a,b,c \| --seeds a,b,c` (`vary` is `{ "node.prop": [a, b, c] }`), `cascade pick <ladder> <n>` |
|
|
41
|
+
| `ladder`, `pick` | `cascade ladder <graph> <node> --name --vary node.prop=a,b,c \| --seeds a,b,c` (`vary` is `{ "node.prop": [a, b, c] }`; `cairn`, an absolute vault directory, returns `data.cairnChoice`, a tap-to-pick card for the Cairn chat: see [Picking in Cairn](CLI_REFERENCE.md#picking-a-ladder-variant-in-cairn)), `cascade pick <ladder> <n>` (on "Picked: … (pick:<id>:<n>)", `ladder` <id>, `variant` <n>) |
|
|
41
42
|
| `node_help` | `cascade help node [<type>]` (a node type's declaration; every type without one) |
|
|
42
43
|
| `doctor` | `cascade doctor [--fix]` (Python environment, stage interpreter, ffmpeg, canvas; `fix` builds the environment) |
|
|
43
44
|
|