@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
|
@@ -3,31 +3,39 @@ import {
|
|
|
3
3
|
builtinNodeRegistrations,
|
|
4
4
|
headlessBuiltin,
|
|
5
5
|
headlessBuiltinRegistrations
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-MI5JLQYA.js";
|
|
7
7
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
sequenceWidth
|
|
11
|
-
} from "./chunk-ICH7N5PN.js";
|
|
8
|
+
Color
|
|
9
|
+
} from "./chunk-GRIN2DDP.js";
|
|
12
10
|
import {
|
|
13
11
|
DEFAULT_GRAPH_SEED,
|
|
14
12
|
bundleFieldType,
|
|
15
13
|
bundleSchemasCompatible,
|
|
16
14
|
bundleValueProblem,
|
|
17
15
|
createGpuHost,
|
|
16
|
+
gpuLimitShortfall,
|
|
18
17
|
graphSeed,
|
|
19
18
|
isBundleValue,
|
|
20
19
|
isGpuHost,
|
|
21
20
|
nodeSeed,
|
|
21
|
+
requestDeviceWithAdapterLimits,
|
|
22
22
|
seededRandom,
|
|
23
23
|
selectBundleFields,
|
|
24
24
|
suggestName,
|
|
25
25
|
unknownFieldMessage
|
|
26
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-L6B6FUQP.js";
|
|
27
27
|
import {
|
|
28
28
|
PropAnimator,
|
|
29
29
|
isBoundProp
|
|
30
30
|
} from "./chunk-HFHWYL55.js";
|
|
31
|
+
import {
|
|
32
|
+
installIoBridge
|
|
33
|
+
} from "./chunk-KBGUO27Q.js";
|
|
34
|
+
import {
|
|
35
|
+
imagePath,
|
|
36
|
+
sequenceFileName,
|
|
37
|
+
sequenceWidth
|
|
38
|
+
} from "./chunk-ICH7N5PN.js";
|
|
31
39
|
import {
|
|
32
40
|
DEFAULT_FPS,
|
|
33
41
|
MIN_FRAME,
|
|
@@ -38,23 +46,36 @@ import {
|
|
|
38
46
|
parseBundleFieldPath,
|
|
39
47
|
remoteBundlePath
|
|
40
48
|
} from "./chunk-CO22XHNG.js";
|
|
49
|
+
import {
|
|
50
|
+
PythonWorker,
|
|
51
|
+
PythonWorkerPool,
|
|
52
|
+
defaultStageWorkerCount
|
|
53
|
+
} from "./chunk-YH637PDL.js";
|
|
54
|
+
import {
|
|
55
|
+
readStageConfig,
|
|
56
|
+
runProjectStage
|
|
57
|
+
} from "./chunk-X566FWBV.js";
|
|
58
|
+
import {
|
|
59
|
+
callStage,
|
|
60
|
+
stageSources
|
|
61
|
+
} from "./chunk-5ORUQ6ES.js";
|
|
41
62
|
import {
|
|
42
63
|
ProjectRoot,
|
|
43
64
|
extractNodeDefinition
|
|
44
|
-
} from "./chunk-
|
|
65
|
+
} from "./chunk-2JEU3MGB.js";
|
|
45
66
|
import {
|
|
46
67
|
CAPABILITIES_BY_ENVIRONMENT,
|
|
47
68
|
validateNodeDefinition
|
|
48
|
-
} from "./chunk-
|
|
69
|
+
} from "./chunk-NJFMDXHR.js";
|
|
49
70
|
import {
|
|
50
71
|
canConnectTypes
|
|
51
|
-
} from "./chunk-
|
|
72
|
+
} from "./chunk-FWMSGKEH.js";
|
|
52
73
|
|
|
53
74
|
// src/cli/projectRuntime.ts
|
|
54
|
-
import
|
|
55
|
-
import
|
|
75
|
+
import fs5 from "node:fs/promises";
|
|
76
|
+
import path6 from "node:path";
|
|
56
77
|
import { fileURLToPath, pathToFileURL as pathToFileURL2 } from "node:url";
|
|
57
|
-
import { existsSync } from "node:fs";
|
|
78
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
58
79
|
import ts from "typescript";
|
|
59
80
|
import { build } from "esbuild";
|
|
60
81
|
|
|
@@ -117,11 +138,21 @@ var MutableAbortSignal = class {
|
|
|
117
138
|
}
|
|
118
139
|
};
|
|
119
140
|
function createRuntime(options) {
|
|
120
|
-
return new Runtime(
|
|
141
|
+
return new Runtime(
|
|
142
|
+
options.host,
|
|
143
|
+
[...builtinNodeRegistrations, ...options.nodes ?? []],
|
|
144
|
+
{
|
|
145
|
+
values: options.values === "trust" ? "trust" : "freeze",
|
|
146
|
+
concurrency: Math.max(1, Math.floor(Number(options.concurrency ?? 1)) || 1),
|
|
147
|
+
...options.nodeScope ? { nodeScope: options.nodeScope } : {}
|
|
148
|
+
}
|
|
149
|
+
);
|
|
121
150
|
}
|
|
151
|
+
var SERIAL_EXECUTION = { values: "freeze", concurrency: 1 };
|
|
122
152
|
var Runtime = class {
|
|
123
|
-
constructor(host, registrations) {
|
|
153
|
+
constructor(host, registrations, execution = SERIAL_EXECUTION) {
|
|
124
154
|
this.host = host;
|
|
155
|
+
this.execution = execution;
|
|
125
156
|
this.registrations = /* @__PURE__ */ new Map();
|
|
126
157
|
this.graphs = /* @__PURE__ */ new Set();
|
|
127
158
|
this.sealed = false;
|
|
@@ -202,7 +233,8 @@ var Runtime = class {
|
|
|
202
233
|
connections,
|
|
203
234
|
() => this.graphs.delete(graph),
|
|
204
235
|
document2.annotations,
|
|
205
|
-
graphSeed(document2)
|
|
236
|
+
graphSeed(document2),
|
|
237
|
+
this.execution
|
|
206
238
|
);
|
|
207
239
|
const fps = document2.timeline?.fps;
|
|
208
240
|
if (typeof fps === "number" && Number.isFinite(fps) && fps > 0) graph.setFps(fps);
|
|
@@ -217,12 +249,13 @@ var Runtime = class {
|
|
|
217
249
|
}
|
|
218
250
|
};
|
|
219
251
|
var Graph = class {
|
|
220
|
-
constructor(host, nodes, connections, onDispose, annotations = [], seed = DEFAULT_GRAPH_SEED) {
|
|
252
|
+
constructor(host, nodes, connections, onDispose, annotations = [], seed = DEFAULT_GRAPH_SEED, execution = SERIAL_EXECUTION) {
|
|
221
253
|
this.host = host;
|
|
222
254
|
this.nodes = nodes;
|
|
223
255
|
this.connections = connections;
|
|
224
256
|
this.onDispose = onDispose;
|
|
225
257
|
this.seed = seed;
|
|
258
|
+
this.execution = execution;
|
|
226
259
|
this.graphState = "ready";
|
|
227
260
|
this.listeners = /* @__PURE__ */ new Set();
|
|
228
261
|
this.diagnostics = [];
|
|
@@ -241,6 +274,7 @@ var Graph = class {
|
|
|
241
274
|
* state survives an inner loop running to completion inside one of its steps.
|
|
242
275
|
*/
|
|
243
276
|
this.feedbackState = /* @__PURE__ */ new Map();
|
|
277
|
+
this.values = execution.values;
|
|
244
278
|
this.byId = new Map(nodes.map((node) => [node.id, node]));
|
|
245
279
|
const byInput = /* @__PURE__ */ new Map();
|
|
246
280
|
const byNode = /* @__PURE__ */ new Map();
|
|
@@ -350,7 +384,7 @@ var Graph = class {
|
|
|
350
384
|
kind: definition.kind,
|
|
351
385
|
type: definition.kind === "trigger" ? "trigger" : definition.type,
|
|
352
386
|
connected: this.connectedOutputs.has(`${node.id}\0${name}`),
|
|
353
|
-
...definition.kind === "data" && node.outputs.has(name) ? { value:
|
|
387
|
+
...definition.kind === "data" && node.outputs.has(name) ? { value: this.expose(node.outputs.get(name)) } : {}
|
|
354
388
|
}
|
|
355
389
|
]
|
|
356
390
|
)
|
|
@@ -552,7 +586,15 @@ var Graph = class {
|
|
|
552
586
|
return () => this.listeners.delete(listener);
|
|
553
587
|
}
|
|
554
588
|
getOutput(nodeId, outputName2) {
|
|
555
|
-
return
|
|
589
|
+
return this.expose(this.requireNode(nodeId).outputs.get(outputName2));
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* An output value as callers see it. Under `"freeze"` every output was
|
|
593
|
+
* deep-frozen when it was set, so this is the value itself (the snapshot is
|
|
594
|
+
* a lookup); under `"trust"` it is handed out as the producer made it.
|
|
595
|
+
*/
|
|
596
|
+
expose(value) {
|
|
597
|
+
return this.values === "trust" ? value : snapshot(value);
|
|
556
598
|
}
|
|
557
599
|
getGraphOutput(name) {
|
|
558
600
|
const node = this.graphOutputs.get(name);
|
|
@@ -564,7 +606,7 @@ var Graph = class {
|
|
|
564
606
|
return immutableMap(
|
|
565
607
|
[...this.requireNode(nodeId).outputs].map(([name, value]) => [
|
|
566
608
|
name,
|
|
567
|
-
|
|
609
|
+
this.expose(value)
|
|
568
610
|
])
|
|
569
611
|
);
|
|
570
612
|
}
|
|
@@ -639,17 +681,21 @@ var Graph = class {
|
|
|
639
681
|
(node) => initial.includes(node) && !insideFeedback.has(node.id)
|
|
640
682
|
);
|
|
641
683
|
const cooked = /* @__PURE__ */ new Set();
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
684
|
+
const skip = (node) => !!reuse && this.reusable(node) && !(reuse === "strict" && this.feedsFrom(node, cooked));
|
|
685
|
+
if (this.execution.concurrency > 1)
|
|
686
|
+
await this.cookConcurrently(ordered, runId, signal, enqueue, skip, cooked);
|
|
687
|
+
else
|
|
688
|
+
for (const node of ordered) {
|
|
689
|
+
if (signal.aborted) break;
|
|
690
|
+
if (skip(node)) {
|
|
691
|
+
this.emit({ type: "node:start", runId, nodeId: node.id });
|
|
692
|
+
this.emit({ type: "node:complete", runId, nodeId: node.id });
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
await this.executeNode(node, runId, signal, {}, enqueue);
|
|
696
|
+
cooked.add(node.id);
|
|
697
|
+
if (!signal.aborted) node.lastCook = this.cookKey(node);
|
|
648
698
|
}
|
|
649
|
-
await this.executeNode(node, runId, signal, {}, enqueue);
|
|
650
|
-
cooked.add(node.id);
|
|
651
|
-
if (!signal.aborted) node.lastCook = this.cookKey(node);
|
|
652
|
-
}
|
|
653
699
|
if (injection && !signal.aborted)
|
|
654
700
|
enqueue(injection.nodeId, injection.inputName, injection.payload, {
|
|
655
701
|
kind: "external"
|
|
@@ -700,7 +746,79 @@ var Graph = class {
|
|
|
700
746
|
this.active = void 0;
|
|
701
747
|
}
|
|
702
748
|
}
|
|
749
|
+
/**
|
|
750
|
+
* The run's main order with up to `concurrency` nodes cooking at once.
|
|
751
|
+
*
|
|
752
|
+
* Nodes START strictly in topological order: the next node starts once
|
|
753
|
+
* everything it reads has completed and a slot is free, and nothing later
|
|
754
|
+
* in the order overtakes it. So the `node:start` events — and a run trace
|
|
755
|
+
* built from them — come in the same order as a serial run on every run;
|
|
756
|
+
* only completions interleave. A Kahn order puts siblings side by side (the
|
|
757
|
+
* morph's four renders are adjacent), which is where the overlap is.
|
|
758
|
+
*
|
|
759
|
+
* A feedback container, a node with trigger ports and a GPU node cook
|
|
760
|
+
* alone: their state (the carried step, the trigger queue's sequence, one
|
|
761
|
+
* device) is ordered by the serial loop and not worth sharing.
|
|
762
|
+
*
|
|
763
|
+
* On a failure nothing new starts, the nodes already cooking finish, and
|
|
764
|
+
* the failure earliest in the order is the run's, so the message does not
|
|
765
|
+
* depend on which stage happened to answer first.
|
|
766
|
+
*/
|
|
767
|
+
async cookConcurrently(ordered, runId, signal, enqueue, skip, cooked) {
|
|
768
|
+
const position = new Map(ordered.map((node, index) => [node.id, index]));
|
|
769
|
+
const needs = /* @__PURE__ */ new Map();
|
|
770
|
+
for (const dependency of graphDependencies(this.nodes, this.connections)) {
|
|
771
|
+
if (!position.has(dependency.sourceId) || !position.has(dependency.targetId)) continue;
|
|
772
|
+
const list = needs.get(dependency.targetId) ?? [];
|
|
773
|
+
list.push(dependency.sourceId);
|
|
774
|
+
needs.set(dependency.targetId, list);
|
|
775
|
+
}
|
|
776
|
+
const done = /* @__PURE__ */ new Set();
|
|
777
|
+
const running = /* @__PURE__ */ new Map();
|
|
778
|
+
let exclusiveRunning = false;
|
|
779
|
+
let failure;
|
|
780
|
+
let next = 0;
|
|
781
|
+
for (; ; ) {
|
|
782
|
+
while (!failure && !signal.aborted && !exclusiveRunning && next < ordered.length && running.size < this.execution.concurrency) {
|
|
783
|
+
const node = ordered[next];
|
|
784
|
+
if (!(needs.get(node.id) ?? []).every((id) => done.has(id))) break;
|
|
785
|
+
const alone = cooksAlone(node);
|
|
786
|
+
if (alone && running.size) break;
|
|
787
|
+
next++;
|
|
788
|
+
if (skip(node)) {
|
|
789
|
+
this.emit({ type: "node:start", runId, nodeId: node.id });
|
|
790
|
+
this.emit({ type: "node:complete", runId, nodeId: node.id });
|
|
791
|
+
done.add(node.id);
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
if (alone) exclusiveRunning = true;
|
|
795
|
+
const index = position.get(node.id);
|
|
796
|
+
const task = this.executeNode(node, runId, signal, {}, enqueue).then(
|
|
797
|
+
() => {
|
|
798
|
+
cooked.add(node.id);
|
|
799
|
+
if (!signal.aborted) node.lastCook = this.cookKey(node);
|
|
800
|
+
done.add(node.id);
|
|
801
|
+
},
|
|
802
|
+
(error) => {
|
|
803
|
+
if (!failure || index < failure.index) failure = { index, error };
|
|
804
|
+
}
|
|
805
|
+
).finally(() => {
|
|
806
|
+
running.delete(node.id);
|
|
807
|
+
if (alone) exclusiveRunning = false;
|
|
808
|
+
});
|
|
809
|
+
running.set(node.id, task);
|
|
810
|
+
}
|
|
811
|
+
if (!running.size) break;
|
|
812
|
+
await Promise.race(running.values());
|
|
813
|
+
}
|
|
814
|
+
if (failure) throw failure.error;
|
|
815
|
+
}
|
|
703
816
|
async executeNode(node, runId, signal, triggers, enqueue) {
|
|
817
|
+
const scope = this.execution.nodeScope;
|
|
818
|
+
if (!scope) return this.cookNode(node, runId, signal, triggers, enqueue);
|
|
819
|
+
return scope(node.id, () => this.cookNode(node, runId, signal, triggers, enqueue));
|
|
820
|
+
}
|
|
821
|
+
async cookNode(node, runId, signal, triggers, enqueue) {
|
|
704
822
|
this.emit({ type: "node:start", runId, nodeId: node.id });
|
|
705
823
|
if (node.moduleId === "cascade.core.Previous") {
|
|
706
824
|
const container = this.enclosingFeedback(node);
|
|
@@ -778,7 +896,8 @@ var Graph = class {
|
|
|
778
896
|
const value = outputValue(
|
|
779
897
|
response.outputs[name],
|
|
780
898
|
definition,
|
|
781
|
-
`${node.id}.${name}
|
|
899
|
+
`${node.id}.${name}`,
|
|
900
|
+
this.values
|
|
782
901
|
);
|
|
783
902
|
node.outputs.set(name, value);
|
|
784
903
|
this.emit({
|
|
@@ -811,7 +930,7 @@ var Graph = class {
|
|
|
811
930
|
} : {
|
|
812
931
|
set: (value) => pendingData.set(
|
|
813
932
|
name,
|
|
814
|
-
outputValue(value, definition, `${node.id}.${name}
|
|
933
|
+
outputValue(value, definition, `${node.id}.${name}`, this.values)
|
|
815
934
|
)
|
|
816
935
|
}
|
|
817
936
|
]
|
|
@@ -822,7 +941,10 @@ var Graph = class {
|
|
|
822
941
|
const installed = this.host.capabilities[name];
|
|
823
942
|
if (name === "gpu" && isGpuHost(installed)) {
|
|
824
943
|
await installed.ensure();
|
|
825
|
-
|
|
944
|
+
const gpu = installed.forNode(node.id);
|
|
945
|
+
const shortfall = gpuLimitShortfall(node.id, node.definition.gpuLimits, gpu);
|
|
946
|
+
if (shortfall) throw new Error(shortfall);
|
|
947
|
+
capabilityEntries.push([name, gpu]);
|
|
826
948
|
} else {
|
|
827
949
|
capabilityEntries.push([
|
|
828
950
|
name,
|
|
@@ -872,7 +994,7 @@ var Graph = class {
|
|
|
872
994
|
runId,
|
|
873
995
|
nodeId: node.id,
|
|
874
996
|
outputName: name,
|
|
875
|
-
value:
|
|
997
|
+
value: this.expose(value)
|
|
876
998
|
});
|
|
877
999
|
}
|
|
878
1000
|
for (const emitted of pendingTriggers)
|
|
@@ -1002,8 +1124,8 @@ var Graph = class {
|
|
|
1002
1124
|
}
|
|
1003
1125
|
for (const [prop, binding] of Object.entries(node.bindings)) {
|
|
1004
1126
|
if (!binding.expression) continue;
|
|
1005
|
-
for (const
|
|
1006
|
-
const remote = remoteBundlePath(
|
|
1127
|
+
for (const path7 of expressionPaths(binding.expression)) {
|
|
1128
|
+
const remote = remoteBundlePath(path7);
|
|
1007
1129
|
if (remote) {
|
|
1008
1130
|
const owner = this.byId.get(remote.node);
|
|
1009
1131
|
const output = owner?.definition.outputs?.[remote.port];
|
|
@@ -1011,12 +1133,12 @@ var Graph = class {
|
|
|
1011
1133
|
push(
|
|
1012
1134
|
node.id,
|
|
1013
1135
|
"runtime/bundle-remote-read",
|
|
1014
|
-
`${node.id}.${prop} reads "${
|
|
1136
|
+
`${node.id}.${prop} reads "${path7}", another node's bundle, without a wire. Wire ${remote.node}.${remote.port} into a bundle input on ${node.id} and read "${remote.port}.${remote.field}" instead`,
|
|
1015
1137
|
`${node.id}.${prop}`
|
|
1016
1138
|
);
|
|
1017
1139
|
continue;
|
|
1018
1140
|
}
|
|
1019
|
-
const reference = parseBundleFieldPath(
|
|
1141
|
+
const reference = parseBundleFieldPath(path7);
|
|
1020
1142
|
if (!reference) continue;
|
|
1021
1143
|
const input = node.definition.inputs?.[reference.port];
|
|
1022
1144
|
if (!input) {
|
|
@@ -1025,7 +1147,7 @@ var Graph = class {
|
|
|
1025
1147
|
push(
|
|
1026
1148
|
node.id,
|
|
1027
1149
|
"runtime/bundle-unknown-port",
|
|
1028
|
-
`${node.id}.${prop} reads "${
|
|
1150
|
+
`${node.id}.${prop} reads "${path7}", but ${node.moduleId} has no input "${reference.port}"` + (suggestion ? ` \u2014 did you mean "${suggestion}.${reference.field}"?` : ""),
|
|
1029
1151
|
`${node.id}.${prop}`
|
|
1030
1152
|
);
|
|
1031
1153
|
continue;
|
|
@@ -1334,7 +1456,8 @@ var Graph = class {
|
|
|
1334
1456
|
}
|
|
1335
1457
|
return Object.freeze({
|
|
1336
1458
|
result: carried,
|
|
1337
|
-
history
|
|
1459
|
+
// Recorded as settled, so reading `history` back is not a copy.
|
|
1460
|
+
history: settle(history)
|
|
1338
1461
|
});
|
|
1339
1462
|
} finally {
|
|
1340
1463
|
this.feedbackState.delete(node.id);
|
|
@@ -1476,8 +1599,10 @@ function materialize(authored, registration) {
|
|
|
1476
1599
|
const definition = registration.definition.props?.[name];
|
|
1477
1600
|
if (!definition) continue;
|
|
1478
1601
|
const object = saved && typeof saved === "object" && "value" in saved ? saved : null;
|
|
1602
|
+
const stored = object ? object.value : saved;
|
|
1479
1603
|
const value = runtimeValue(
|
|
1480
|
-
|
|
1604
|
+
// A colour saved as `{ r, g, b }` by the class image nodes reads as one.
|
|
1605
|
+
definition.type === "color" ? Color.upgrade(stored) : stored,
|
|
1481
1606
|
definition.type,
|
|
1482
1607
|
false,
|
|
1483
1608
|
`${authored.id}.${name}`
|
|
@@ -1734,6 +1859,15 @@ function snapshotRegistration(registration) {
|
|
|
1734
1859
|
const definition = snapshot(registration.definition);
|
|
1735
1860
|
return Object.freeze({ ...registration, definition });
|
|
1736
1861
|
}
|
|
1862
|
+
function cooksAlone(node) {
|
|
1863
|
+
if (node.definition.container === "feedback") return true;
|
|
1864
|
+
if (node.definition.capabilities?.includes("gpu")) return true;
|
|
1865
|
+
const ports = [
|
|
1866
|
+
...Object.values(node.definition.inputs ?? {}),
|
|
1867
|
+
...Object.values(node.definition.outputs ?? {})
|
|
1868
|
+
];
|
|
1869
|
+
return ports.some((port) => port.kind === "trigger");
|
|
1870
|
+
}
|
|
1737
1871
|
function isGated(node) {
|
|
1738
1872
|
return Object.values(node.definition.inputs ?? {}).some(
|
|
1739
1873
|
(input) => input.kind === "trigger"
|
|
@@ -1877,7 +2011,69 @@ function misuse(code, message) {
|
|
|
1877
2011
|
throw runtimeError(code, message);
|
|
1878
2012
|
}
|
|
1879
2013
|
function snapshot(value) {
|
|
1880
|
-
|
|
2014
|
+
const clone = cloneValue(value, /* @__PURE__ */ new WeakSet());
|
|
2015
|
+
if (clone !== null && typeof clone === "object") settled.add(clone);
|
|
2016
|
+
return clone;
|
|
2017
|
+
}
|
|
2018
|
+
var settled = /* @__PURE__ */ new WeakSet();
|
|
2019
|
+
var copiedContainers = 0;
|
|
2020
|
+
function isPlainContainer(value) {
|
|
2021
|
+
if (Array.isArray(value)) return true;
|
|
2022
|
+
const prototype = Object.getPrototypeOf(value);
|
|
2023
|
+
return prototype === Object.prototype || prototype === null;
|
|
2024
|
+
}
|
|
2025
|
+
function checkScalar(value) {
|
|
2026
|
+
if (typeof value === "number" && !Number.isFinite(value))
|
|
2027
|
+
misuse("runtime/non-serializable-value", "Runtime numbers must be finite");
|
|
2028
|
+
if (typeof value === "function" || typeof value === "symbol" || typeof value === "bigint")
|
|
2029
|
+
misuse(
|
|
2030
|
+
"runtime/non-serializable-value",
|
|
2031
|
+
`Runtime values cannot contain ${typeof value}`
|
|
2032
|
+
);
|
|
2033
|
+
}
|
|
2034
|
+
function settle(value) {
|
|
2035
|
+
if (value === null || typeof value !== "object") {
|
|
2036
|
+
checkScalar(value);
|
|
2037
|
+
return value;
|
|
2038
|
+
}
|
|
2039
|
+
if (!settled.has(value) && isPlainContainer(value)) {
|
|
2040
|
+
settleContainer(value, /* @__PURE__ */ new WeakSet());
|
|
2041
|
+
settled.add(value);
|
|
2042
|
+
}
|
|
2043
|
+
return value;
|
|
2044
|
+
}
|
|
2045
|
+
function settleContainer(value, ancestors) {
|
|
2046
|
+
let nested = false;
|
|
2047
|
+
const visit = (item) => {
|
|
2048
|
+
if (item === null || typeof item !== "object") {
|
|
2049
|
+
checkScalar(item);
|
|
2050
|
+
return;
|
|
2051
|
+
}
|
|
2052
|
+
if (settled.has(item) || !isPlainContainer(item)) return;
|
|
2053
|
+
if (ancestors.has(item))
|
|
2054
|
+
misuse(
|
|
2055
|
+
"runtime/non-serializable-value",
|
|
2056
|
+
"Runtime values cannot contain cycles"
|
|
2057
|
+
);
|
|
2058
|
+
if (!nested) {
|
|
2059
|
+
nested = true;
|
|
2060
|
+
ancestors.add(value);
|
|
2061
|
+
}
|
|
2062
|
+
if (settleContainer(item, ancestors)) settled.add(item);
|
|
2063
|
+
};
|
|
2064
|
+
if (Array.isArray(value)) for (let index = 0; index < value.length; index++) visit(value[index]);
|
|
2065
|
+
else for (const key of Object.keys(value)) visit(value[key]);
|
|
2066
|
+
if (nested) ancestors.delete(value);
|
|
2067
|
+
Object.freeze(value);
|
|
2068
|
+
return nested;
|
|
2069
|
+
}
|
|
2070
|
+
function trust(value) {
|
|
2071
|
+
if (value === null || typeof value !== "object") {
|
|
2072
|
+
checkScalar(value);
|
|
2073
|
+
return value;
|
|
2074
|
+
}
|
|
2075
|
+
if (isPlainContainer(value)) Object.freeze(value);
|
|
2076
|
+
return value;
|
|
1881
2077
|
}
|
|
1882
2078
|
function immutableMap(entries) {
|
|
1883
2079
|
const source = new Map(entries);
|
|
@@ -1911,24 +2107,17 @@ function immutableMap(entries) {
|
|
|
1911
2107
|
return view;
|
|
1912
2108
|
}
|
|
1913
2109
|
function cloneValue(value, ancestors) {
|
|
1914
|
-
|
|
1915
|
-
misuse("runtime/non-serializable-value", "Runtime numbers must be finite");
|
|
1916
|
-
if (typeof value === "function" || typeof value === "symbol" || typeof value === "bigint")
|
|
1917
|
-
misuse(
|
|
1918
|
-
"runtime/non-serializable-value",
|
|
1919
|
-
`Runtime values cannot contain ${typeof value}`
|
|
1920
|
-
);
|
|
2110
|
+
checkScalar(value);
|
|
1921
2111
|
if (value === void 0 || value === null || typeof value !== "object")
|
|
1922
2112
|
return value;
|
|
1923
|
-
|
|
1924
|
-
if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null)
|
|
1925
|
-
return value;
|
|
2113
|
+
if (settled.has(value) || !isPlainContainer(value)) return value;
|
|
1926
2114
|
if (ancestors.has(value))
|
|
1927
2115
|
misuse(
|
|
1928
2116
|
"runtime/non-serializable-value",
|
|
1929
2117
|
"Runtime values cannot contain cycles"
|
|
1930
2118
|
);
|
|
1931
2119
|
ancestors.add(value);
|
|
2120
|
+
copiedContainers++;
|
|
1932
2121
|
const clone = Array.isArray(value) ? value.map((item) => cloneValue(item, ancestors)) : Object.fromEntries(
|
|
1933
2122
|
Object.entries(value).map(([key, item]) => [
|
|
1934
2123
|
key,
|
|
@@ -1950,16 +2139,17 @@ var TUPLE_LENGTHS = {
|
|
|
1950
2139
|
mat4: 16,
|
|
1951
2140
|
color: 4
|
|
1952
2141
|
};
|
|
1953
|
-
function outputValue(value, definition,
|
|
2142
|
+
function outputValue(value, definition, path7, policy) {
|
|
2143
|
+
const adopt = policy === "trust" ? trust : settle;
|
|
1954
2144
|
if (definition.type !== "bundle" || !definition.fields)
|
|
1955
|
-
return runtimeValue(value, definition.type, false,
|
|
2145
|
+
return runtimeValue(value, definition.type, false, path7, adopt);
|
|
1956
2146
|
const fields = definition.fields;
|
|
1957
2147
|
const problem = bundleValueProblem(value, fields);
|
|
1958
|
-
if (problem) misuse("runtime/value-type", `${
|
|
2148
|
+
if (problem) misuse("runtime/value-type", `${path7} ${problem}`);
|
|
1959
2149
|
if (isBundleValue(value))
|
|
1960
2150
|
for (const [field, item] of Object.entries(value))
|
|
1961
|
-
runtimeValue(item, bundleFieldType(fields[field]), true, `${
|
|
1962
|
-
return runtimeValue(value, "bundle", false,
|
|
2151
|
+
runtimeValue(item, bundleFieldType(fields[field]), true, `${path7}.${field}`, (checked) => checked);
|
|
2152
|
+
return runtimeValue(value, "bundle", false, path7, adopt);
|
|
1963
2153
|
}
|
|
1964
2154
|
function expressionPaths(expression) {
|
|
1965
2155
|
const paths = [];
|
|
@@ -1977,17 +2167,17 @@ function readsBundleFields(node) {
|
|
|
1977
2167
|
}
|
|
1978
2168
|
return false;
|
|
1979
2169
|
}
|
|
1980
|
-
function runtimeValue(value, type, allowUndefined,
|
|
2170
|
+
function runtimeValue(value, type, allowUndefined, path7, keep = snapshot) {
|
|
1981
2171
|
if (value === void 0) {
|
|
1982
2172
|
if (allowUndefined) return void 0;
|
|
1983
|
-
misuse("runtime/value-type", `${
|
|
2173
|
+
misuse("runtime/value-type", `${path7} requires a ${type} value`);
|
|
1984
2174
|
}
|
|
1985
2175
|
const tupleLength = TUPLE_LENGTHS[type];
|
|
1986
2176
|
const valid = type === "float" && typeof value === "number" && Number.isFinite(value) || type === "int" && typeof value === "number" && Number.isInteger(value) || type === "bool" && typeof value === "boolean" || type === "string" && typeof value === "string" || tupleLength !== void 0 && Array.isArray(value) && value.length === tupleLength && value.every(
|
|
1987
2177
|
(item) => type.endsWith("i") ? typeof item === "number" && Number.isInteger(item) : typeof item === "number" && Number.isFinite(item)
|
|
1988
2178
|
) || type === "bundle" && isBundleValue(value) || !tupleLength && type !== "bundle" && type !== "float" && type !== "int" && type !== "bool" && type !== "string";
|
|
1989
|
-
if (!valid) misuse("runtime/value-type", `${
|
|
1990
|
-
return
|
|
2179
|
+
if (!valid) misuse("runtime/value-type", `${path7} is not a valid ${type}`);
|
|
2180
|
+
return keep(value);
|
|
1991
2181
|
}
|
|
1992
2182
|
function sameValues(a, b, equal) {
|
|
1993
2183
|
return a.length === b.length && a.every((value, index) => equal(value, b[index]));
|
|
@@ -2211,56 +2401,198 @@ import { pathToFileURL } from "node:url";
|
|
|
2211
2401
|
|
|
2212
2402
|
// src/cli/headlessIo.ts
|
|
2213
2403
|
import crypto from "node:crypto";
|
|
2404
|
+
import { existsSync } from "node:fs";
|
|
2214
2405
|
import fs2 from "node:fs/promises";
|
|
2215
2406
|
import path2 from "node:path";
|
|
2216
|
-
|
|
2217
|
-
// server/src/runtime/io.ts
|
|
2218
|
-
var IO_BRIDGE_KEY = "__cascadeIoBridge";
|
|
2219
|
-
function installIoBridge(bridge) {
|
|
2220
|
-
const globals = globalThis;
|
|
2221
|
-
const previous = globals[IO_BRIDGE_KEY];
|
|
2222
|
-
globals[IO_BRIDGE_KEY] = bridge ?? void 0;
|
|
2223
|
-
return () => {
|
|
2224
|
-
globals[IO_BRIDGE_KEY] = previous;
|
|
2225
|
-
};
|
|
2226
|
-
}
|
|
2227
|
-
|
|
2228
|
-
// src/cli/headlessIo.ts
|
|
2229
2407
|
var CACHE_DIR = ".cascade-cache";
|
|
2230
2408
|
function contentAddressed(relative, body) {
|
|
2231
|
-
|
|
2409
|
+
return addressed(relative, crypto.createHash("sha1").update(body).digest("hex").slice(0, 12));
|
|
2410
|
+
}
|
|
2411
|
+
function pixelAddressed(relative, pixels) {
|
|
2412
|
+
const hash = crypto.createHash("sha1").update(`${pixels.width}x${pixels.height}\0`).update(pixels.data).digest("hex").slice(0, 12);
|
|
2413
|
+
return addressed(relative, hash);
|
|
2414
|
+
}
|
|
2415
|
+
function addressed(relative, hash) {
|
|
2232
2416
|
const directory = path2.posix.dirname(relative);
|
|
2233
2417
|
const name = path2.posix.basename(relative);
|
|
2234
2418
|
const [stem, ...rest] = name.split(".");
|
|
2235
2419
|
const tail = rest.length ? `.${rest.join(".")}` : "";
|
|
2236
2420
|
return path2.posix.join(directory, `${stem}.${hash}${tail}`);
|
|
2237
2421
|
}
|
|
2422
|
+
var INTERMEDIATE_PNG = { level: 1, adaptiveFiltering: false };
|
|
2423
|
+
var FINAL_PNG = { level: 6, adaptiveFiltering: true };
|
|
2424
|
+
var sharpLoad;
|
|
2425
|
+
function loadSharp() {
|
|
2426
|
+
sharpLoad ??= import("sharp").then((module) => module.default, () => null);
|
|
2427
|
+
return sharpLoad;
|
|
2428
|
+
}
|
|
2429
|
+
async function encodePixelsPng(pixels, options = INTERMEDIATE_PNG) {
|
|
2430
|
+
const sharp = await loadSharp();
|
|
2431
|
+
if (sharp) {
|
|
2432
|
+
const raw = Buffer.from(pixels.data.buffer, pixels.data.byteOffset, pixels.data.byteLength);
|
|
2433
|
+
return sharp(raw, { raw: { width: pixels.width, height: pixels.height, channels: 4 } }).png({ compressionLevel: options.level, adaptiveFiltering: options.adaptiveFiltering }).toBuffer();
|
|
2434
|
+
}
|
|
2435
|
+
const Canvas = globalThis.OffscreenCanvas;
|
|
2436
|
+
const Data = globalThis.ImageData;
|
|
2437
|
+
if (!Canvas || !Data) throw new Error("cascade/io: cannot encode a PNG here (neither sharp nor a canvas is installed)");
|
|
2438
|
+
const canvas = new Canvas(pixels.width, pixels.height);
|
|
2439
|
+
canvas.getContext("2d").putImageData(new Data(pixels.data, pixels.width, pixels.height), 0, 0);
|
|
2440
|
+
return new Uint8Array(await (await canvas.convertToBlob({ type: "image/png" })).arrayBuffer());
|
|
2441
|
+
}
|
|
2442
|
+
async function writeWhole(full, data) {
|
|
2443
|
+
await fs2.mkdir(path2.dirname(full), { recursive: true });
|
|
2444
|
+
const staging = `${full}.${process.pid}.${crypto.randomBytes(4).toString("hex")}.tmp`;
|
|
2445
|
+
await fs2.writeFile(staging, data);
|
|
2446
|
+
await fs2.rename(staging, full);
|
|
2447
|
+
}
|
|
2448
|
+
var RESIDENT_BUDGET_BYTES = 512 * 1024 * 1024;
|
|
2449
|
+
var ResidentImages = class {
|
|
2450
|
+
constructor(project, budget = RESIDENT_BUDGET_BYTES) {
|
|
2451
|
+
this.project = project;
|
|
2452
|
+
this.budget = budget;
|
|
2453
|
+
this.held = /* @__PURE__ */ new Map();
|
|
2454
|
+
this.bytes = 0;
|
|
2455
|
+
/** Files written behind, for a test or a trace. */
|
|
2456
|
+
this.encodes = 0;
|
|
2457
|
+
}
|
|
2458
|
+
/** Hold the pixels, start the write behind, return the path at once. */
|
|
2459
|
+
keep(relative, pixels) {
|
|
2460
|
+
const target = pixelAddressed(relative, pixels);
|
|
2461
|
+
const known = this.held.get(target);
|
|
2462
|
+
if (known) {
|
|
2463
|
+
this.touch(target, known);
|
|
2464
|
+
return target;
|
|
2465
|
+
}
|
|
2466
|
+
const full = this.project.resolve(target);
|
|
2467
|
+
const entry = {
|
|
2468
|
+
pixels,
|
|
2469
|
+
done: false,
|
|
2470
|
+
written: (async () => {
|
|
2471
|
+
if (existsSync(full)) return;
|
|
2472
|
+
this.encodes += 1;
|
|
2473
|
+
await writeWhole(full, await encodePixelsPng(pixels, INTERMEDIATE_PNG));
|
|
2474
|
+
})()
|
|
2475
|
+
};
|
|
2476
|
+
entry.written.then(() => {
|
|
2477
|
+
entry.done = true;
|
|
2478
|
+
this.trim();
|
|
2479
|
+
}, () => {
|
|
2480
|
+
entry.done = true;
|
|
2481
|
+
});
|
|
2482
|
+
this.held.set(target, entry);
|
|
2483
|
+
this.bytes += pixels.data.byteLength;
|
|
2484
|
+
this.trim();
|
|
2485
|
+
return target;
|
|
2486
|
+
}
|
|
2487
|
+
/** The pixels behind a path, while held. */
|
|
2488
|
+
pixels(relative) {
|
|
2489
|
+
const entry = this.held.get(relative);
|
|
2490
|
+
if (!entry) return void 0;
|
|
2491
|
+
this.touch(relative, entry);
|
|
2492
|
+
return entry.pixels;
|
|
2493
|
+
}
|
|
2494
|
+
/** Resolve when `relative` (or every held image) is a whole file. */
|
|
2495
|
+
async settle(relative) {
|
|
2496
|
+
if (relative !== void 0) {
|
|
2497
|
+
await this.held.get(relative)?.written;
|
|
2498
|
+
return;
|
|
2499
|
+
}
|
|
2500
|
+
await Promise.all([...this.held.values()].filter((entry) => !entry.done).map((entry) => entry.written));
|
|
2501
|
+
}
|
|
2502
|
+
touch(key, entry) {
|
|
2503
|
+
this.held.delete(key);
|
|
2504
|
+
this.held.set(key, entry);
|
|
2505
|
+
}
|
|
2506
|
+
/** Let the oldest written images go until under budget; a pending one stays. */
|
|
2507
|
+
trim() {
|
|
2508
|
+
if (this.bytes <= this.budget) return;
|
|
2509
|
+
for (const [key, entry] of this.held) {
|
|
2510
|
+
if (this.bytes <= this.budget) break;
|
|
2511
|
+
if (!entry.done) continue;
|
|
2512
|
+
this.held.delete(key);
|
|
2513
|
+
this.bytes -= entry.pixels.data.byteLength;
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
};
|
|
2517
|
+
var residentStores = /* @__PURE__ */ new Map();
|
|
2518
|
+
function residentImages(project) {
|
|
2519
|
+
return residentStores.get(project.root);
|
|
2520
|
+
}
|
|
2521
|
+
async function settleProjectImages(project) {
|
|
2522
|
+
await residentImages(project)?.settle();
|
|
2523
|
+
}
|
|
2524
|
+
async function copyImageFile(project, relative, destination) {
|
|
2525
|
+
const clean = relative.replace(/^\.?\//, "");
|
|
2526
|
+
const store = residentImages(project);
|
|
2527
|
+
const pixels = /\.png$/i.test(clean) ? store?.pixels(clean) : void 0;
|
|
2528
|
+
if (pixels) {
|
|
2529
|
+
await writeWhole(destination, await encodePixelsPng(pixels, FINAL_PNG));
|
|
2530
|
+
return;
|
|
2531
|
+
}
|
|
2532
|
+
await store?.settle(clean);
|
|
2533
|
+
await fs2.copyFile(project.resolveMedia(clean), destination);
|
|
2534
|
+
}
|
|
2238
2535
|
function createIoBridge(project) {
|
|
2239
|
-
|
|
2536
|
+
const confined = (relative) => {
|
|
2537
|
+
const clean = relative.replace(/^\.?\//, "");
|
|
2538
|
+
if (!clean.startsWith(`${CACHE_DIR}/`)) {
|
|
2539
|
+
throw new Error(`cascade/io: writes are confined to ${CACHE_DIR}/ \u2014 got ${clean}`);
|
|
2540
|
+
}
|
|
2541
|
+
return clean;
|
|
2542
|
+
};
|
|
2543
|
+
const bridge = {
|
|
2544
|
+
/** An intermediate image: held in memory when the run keeps pixels, else a fast PNG now. */
|
|
2545
|
+
async keep(relative, pixels) {
|
|
2546
|
+
const clean = confined(relative);
|
|
2547
|
+
if (!pixels.width || !pixels.height) throw new Error(`cascade/io: refusing to store an empty image to ${clean}`);
|
|
2548
|
+
const store = residentImages(project);
|
|
2549
|
+
if (store) return store.keep(clean, pixels);
|
|
2550
|
+
const target = pixelAddressed(clean, pixels);
|
|
2551
|
+
const full = project.resolve(target);
|
|
2552
|
+
if (!existsSync(full)) await writeWhole(full, await encodePixelsPng(pixels, INTERMEDIATE_PNG));
|
|
2553
|
+
return target;
|
|
2554
|
+
},
|
|
2555
|
+
pixels(relative) {
|
|
2556
|
+
return residentImages(project)?.pixels(relative.replace(/^\.?\//, ""));
|
|
2557
|
+
},
|
|
2240
2558
|
async read(relative) {
|
|
2241
|
-
const
|
|
2559
|
+
const clean = relative.replace(/^\.?\//, "");
|
|
2560
|
+
const full = project.resolveMedia(clean);
|
|
2242
2561
|
try {
|
|
2562
|
+
await residentImages(project)?.settle(clean);
|
|
2243
2563
|
return await fs2.readFile(full);
|
|
2244
2564
|
} catch (error) {
|
|
2245
2565
|
throw new Error(`cascade/io: cannot load ${relative} \u2014 ${error instanceof Error ? error.message : error}`);
|
|
2246
2566
|
}
|
|
2247
2567
|
},
|
|
2248
2568
|
async write(relative, data) {
|
|
2249
|
-
const clean = relative
|
|
2250
|
-
if (!clean.startsWith(`${CACHE_DIR}/`)) {
|
|
2251
|
-
throw new Error(`cascade/io: writes are confined to ${CACHE_DIR}/ \u2014 got ${clean}`);
|
|
2252
|
-
}
|
|
2569
|
+
const clean = confined(relative);
|
|
2253
2570
|
if (!data.byteLength) throw new Error(`cascade/io: refusing to write an empty file to ${clean}`);
|
|
2254
2571
|
const target = contentAddressed(clean, data);
|
|
2255
|
-
|
|
2256
|
-
await fs2.mkdir(path2.dirname(full), { recursive: true });
|
|
2257
|
-
await fs2.writeFile(full, data);
|
|
2572
|
+
await writeWhole(project.resolve(target), data);
|
|
2258
2573
|
return target;
|
|
2259
2574
|
}
|
|
2260
2575
|
};
|
|
2576
|
+
return bridge;
|
|
2261
2577
|
}
|
|
2262
|
-
function installProjectIo(project) {
|
|
2263
|
-
|
|
2578
|
+
function installProjectIo(project, options = {}) {
|
|
2579
|
+
let removeStore = () => {
|
|
2580
|
+
};
|
|
2581
|
+
if (options.resident) {
|
|
2582
|
+
const store = new ResidentImages(project);
|
|
2583
|
+
const previous = residentStores.get(project.root);
|
|
2584
|
+
residentStores.set(project.root, store);
|
|
2585
|
+
removeStore = () => {
|
|
2586
|
+
if (residentStores.get(project.root) !== store) return;
|
|
2587
|
+
if (previous) residentStores.set(project.root, previous);
|
|
2588
|
+
else residentStores.delete(project.root);
|
|
2589
|
+
};
|
|
2590
|
+
}
|
|
2591
|
+
const removeBridge = installIoBridge(createIoBridge(project));
|
|
2592
|
+
return () => {
|
|
2593
|
+
removeBridge();
|
|
2594
|
+
removeStore();
|
|
2595
|
+
};
|
|
2264
2596
|
}
|
|
2265
2597
|
|
|
2266
2598
|
// src/cli/nodeAssets.ts
|
|
@@ -2280,7 +2612,18 @@ function createNodeAssetCapability(project) {
|
|
|
2280
2612
|
const written = await io.write(`${CACHE_DIR2}/${assetFileName(metadata.suggestedName)}`, data);
|
|
2281
2613
|
return { path: written, ...metadata.mediaType ? { mediaType: metadata.mediaType } : {} };
|
|
2282
2614
|
},
|
|
2615
|
+
// `cascade/image`'s `writeImage` hands an intermediate over as pixels:
|
|
2616
|
+
// the same file policy as `write`, through the bridge's fast PNG.
|
|
2617
|
+
async keepImage(pixels, metadata) {
|
|
2618
|
+
const written = await io.keep(`${CACHE_DIR2}/${assetFileName(metadata.suggestedName)}`, pixels);
|
|
2619
|
+
return { path: written, mediaType: "image/png" };
|
|
2620
|
+
},
|
|
2621
|
+
// And reads them back from memory while the run holds them.
|
|
2622
|
+
imagePixels(ref) {
|
|
2623
|
+
return io.pixels(String(ref.path));
|
|
2624
|
+
},
|
|
2283
2625
|
async resolveUrl(ref) {
|
|
2626
|
+
await settleProjectImages(project);
|
|
2284
2627
|
const full = project.resolveMedia(String(ref.path).replace(/^\.?\//, ""));
|
|
2285
2628
|
return { value: pathToFileURL(full).href, release: () => {
|
|
2286
2629
|
} };
|
|
@@ -2300,24 +2643,43 @@ function looksLikeImage(value) {
|
|
|
2300
2643
|
return IMAGE_EXTENSIONS.has(path4.extname(file).toLowerCase());
|
|
2301
2644
|
}
|
|
2302
2645
|
function collectImageOutputs(graph, options = {}) {
|
|
2646
|
+
return planImageOutputs(graph, options).collect();
|
|
2647
|
+
}
|
|
2648
|
+
function planImageOutputs(graph, options = {}) {
|
|
2303
2649
|
const wanted = new Set(options.nodes ?? []);
|
|
2304
2650
|
const extra = new Set(options.extra ?? []);
|
|
2305
|
-
const found = [];
|
|
2306
2651
|
const inspection = graph.inspect();
|
|
2307
2652
|
const isAssertion = (moduleId2) => moduleId2.startsWith("cascade.assert.");
|
|
2308
2653
|
const assertions = new Set(inspection.nodes.filter((node) => isAssertion(node.moduleId)).map((node) => node.id));
|
|
2309
2654
|
const consumed = new Set(inspection.connections.filter((connection) => !assertions.has(connection.target.nodeId)).map((connection) => `${connection.source.nodeId}\0${connection.source.outputName}`));
|
|
2655
|
+
const candidates = [];
|
|
2310
2656
|
for (const node of inspection.nodes) {
|
|
2311
2657
|
const named = wanted.has(node.id) || extra.has(node.id);
|
|
2312
2658
|
if (wanted.size && !named) continue;
|
|
2313
2659
|
if (!named && assertions.has(node.id)) continue;
|
|
2314
|
-
const ports = Object.entries(node.outputs).filter(([, port]) => port.
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2660
|
+
const ports = Object.entries(node.outputs).filter(([, port]) => port.type === "image" || port.type === "any").map(([name, port]) => ({
|
|
2661
|
+
name,
|
|
2662
|
+
type: port.type,
|
|
2663
|
+
// Consumed downstream: not a final image, though it still counts
|
|
2664
|
+
// towards `shared` when it carries one.
|
|
2665
|
+
skip: !named && port.connected && consumed.has(`${node.id}\0${name}`)
|
|
2666
|
+
}));
|
|
2667
|
+
if (ports.length) candidates.push({ nodeId: node.id, ports });
|
|
2319
2668
|
}
|
|
2320
|
-
return
|
|
2669
|
+
return {
|
|
2670
|
+
collect() {
|
|
2671
|
+
const found = [];
|
|
2672
|
+
for (const candidate of candidates) {
|
|
2673
|
+
const values = graph.getOutputs(candidate.nodeId);
|
|
2674
|
+
const ports = candidate.ports.map((port) => ({ ...port, value: values.get(port.name) })).filter((port) => port.value !== void 0 && (port.type === "image" || looksLikeImage(port.value)));
|
|
2675
|
+
for (const port of ports) {
|
|
2676
|
+
if (port.skip) continue;
|
|
2677
|
+
found.push({ nodeId: candidate.nodeId, port: port.name, value: port.value, shared: ports.length > 1 });
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
return found;
|
|
2681
|
+
}
|
|
2682
|
+
};
|
|
2321
2683
|
}
|
|
2322
2684
|
function unknownNodes(graph, ids) {
|
|
2323
2685
|
const known = new Set(graph.inspect().nodes.map((node) => node.id));
|
|
@@ -2346,7 +2708,8 @@ async function saveStillImages(graph, project, options) {
|
|
|
2346
2708
|
continue;
|
|
2347
2709
|
}
|
|
2348
2710
|
const name = stillFileName(output, relative);
|
|
2349
|
-
await fs3.copyFile(source, path4.join(out.directory, name));
|
|
2711
|
+
if (path4.isAbsolute(relative)) await fs3.copyFile(source, path4.join(out.directory, name));
|
|
2712
|
+
else await copyImageFile(project, relative, path4.join(out.directory, name));
|
|
2350
2713
|
saved.push({ node: output.nodeId, port: output.port, source, file: out.spell(name) });
|
|
2351
2714
|
}
|
|
2352
2715
|
if (out && !saved.length) {
|
|
@@ -2378,7 +2741,7 @@ function createDawnGpuHost() {
|
|
|
2378
2741
|
try {
|
|
2379
2742
|
const adapter = await gpu.requestAdapter();
|
|
2380
2743
|
if (!adapter) throw new Error("Dawn found no WebGPU adapter. Check GPU drivers and backend availability; headless rendering still needs a hardware or explicitly configured software adapter.");
|
|
2381
|
-
device = await adapter
|
|
2744
|
+
device = await requestDeviceWithAdapterLimits(adapter);
|
|
2382
2745
|
const info = adapter.info;
|
|
2383
2746
|
return { device, info: {
|
|
2384
2747
|
vendor: info.vendor,
|
|
@@ -2397,9 +2760,376 @@ function createDawnGpuHost() {
|
|
|
2397
2760
|
});
|
|
2398
2761
|
}
|
|
2399
2762
|
|
|
2763
|
+
// src/cli/nodeShape.ts
|
|
2764
|
+
var LONG_EXECUTE_LINES = 150;
|
|
2765
|
+
var MIXED_JOB_COUNT = 4;
|
|
2766
|
+
var JOB_ORDER = ["load", "generate", "colour", "draw", "export"];
|
|
2767
|
+
var BY_NAME = {
|
|
2768
|
+
load: /^(readFile(Sync)?|loadImage|loadFont|loadJson|readJson|fetch|decodeImage)$/,
|
|
2769
|
+
generate: /^(random|rng|hash\d*|noise\w*|\w*[nN]oise\d*|fbm|simplex\w*|perlin\w*|worley\w*|poisson\w*|scatter\w*)$/,
|
|
2770
|
+
colour: /^(oklch|oklab|\w*[oO]klab\w*|\w*[oO]klch\w*|okMix|hsl\w*|palette\w*|mixColou?r|lerpColou?r|\w*Palette)$/,
|
|
2771
|
+
draw: /^(getContext|beginPath|fillRect|strokeRect|drawImage|putImageData|createImageData|arc|lineTo|createRenderPipeline|createComputePipeline|draw|drawIndexed|dispatchWorkgroups)$/,
|
|
2772
|
+
export: /^(writeFile(Sync)?|savePng|saveSvg|saveJpe?g|toBuffer|toBlob|toDataURL|encodePng|encodeJpe?g|encodeImage)$/
|
|
2773
|
+
};
|
|
2774
|
+
var CAPABILITY_RECEIVER = /(^|\.)(files|assets)$/;
|
|
2775
|
+
function measureNodeShape(source, fileName, ts2) {
|
|
2776
|
+
const file = ts2.createSourceFile(fileName, source, ts2.ScriptTarget.Latest, true, ts2.ScriptKind.TS);
|
|
2777
|
+
let executeLines = null;
|
|
2778
|
+
let executeLine = null;
|
|
2779
|
+
const jobs = /* @__PURE__ */ new Set();
|
|
2780
|
+
for (const statement of file.statements) {
|
|
2781
|
+
const body = executeBody(statement);
|
|
2782
|
+
if (!body) continue;
|
|
2783
|
+
const start = file.getLineAndCharacterOfPosition(body.getStart(file)).line;
|
|
2784
|
+
const end = file.getLineAndCharacterOfPosition(body.getEnd()).line;
|
|
2785
|
+
executeLines = end - start + 1;
|
|
2786
|
+
executeLine = file.getLineAndCharacterOfPosition(statement.getStart(file)).line + 1;
|
|
2787
|
+
}
|
|
2788
|
+
visit(file);
|
|
2789
|
+
return { executeLines, executeLine, jobs: JOB_ORDER.filter((job) => jobs.has(job)) };
|
|
2790
|
+
function executeBody(statement) {
|
|
2791
|
+
if (ts2.isFunctionDeclaration(statement) && statement.name?.text === "execute") return statement.body;
|
|
2792
|
+
if (ts2.isVariableStatement(statement)) {
|
|
2793
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
2794
|
+
if (!ts2.isIdentifier(declaration.name) || declaration.name.text !== "execute") continue;
|
|
2795
|
+
const init = declaration.initializer;
|
|
2796
|
+
if (init && (ts2.isArrowFunction(init) || ts2.isFunctionExpression(init))) return init.body;
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
return void 0;
|
|
2800
|
+
}
|
|
2801
|
+
function visit(node) {
|
|
2802
|
+
if (ts2.isCallExpression(node)) classify(node.expression);
|
|
2803
|
+
ts2.forEachChild(node, visit);
|
|
2804
|
+
}
|
|
2805
|
+
function classify(callee) {
|
|
2806
|
+
let name;
|
|
2807
|
+
if (ts2.isIdentifier(callee)) name = callee.text;
|
|
2808
|
+
else if (ts2.isPropertyAccessExpression(callee)) {
|
|
2809
|
+
name = callee.name.text;
|
|
2810
|
+
const receiver = callee.expression.getText(file);
|
|
2811
|
+
if (CAPABILITY_RECEIVER.test(receiver)) {
|
|
2812
|
+
if (/^(read|list|stat|resolveUrl)$/.test(name)) jobs.add("load");
|
|
2813
|
+
if (name === "write") jobs.add("export");
|
|
2814
|
+
}
|
|
2815
|
+
if (receiver === "Math" && name === "random") {
|
|
2816
|
+
jobs.add("generate");
|
|
2817
|
+
return;
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
if (!name) return;
|
|
2821
|
+
for (const job of JOB_ORDER) if (BY_NAME[job].test(name)) jobs.add(job);
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
function nodeShapeWarnings(source, fileName, ts2, moduleId2) {
|
|
2825
|
+
const shape = measureNodeShape(source, fileName, ts2);
|
|
2826
|
+
const where = moduleId2 ?? fileName;
|
|
2827
|
+
const out = [];
|
|
2828
|
+
if (shape.executeLines !== null && shape.executeLines > LONG_EXECUTE_LINES) {
|
|
2829
|
+
out.push({
|
|
2830
|
+
phase: "shape",
|
|
2831
|
+
code: "shape/long-execute",
|
|
2832
|
+
message: `${where}: execute is ${shape.executeLines} lines (over ${LONG_EXECUTE_LINES}). Split it into single-job nodes wired in the graph.`,
|
|
2833
|
+
file: fileName,
|
|
2834
|
+
line: shape.executeLine ?? void 0
|
|
2835
|
+
});
|
|
2836
|
+
}
|
|
2837
|
+
const loadsAndExports = shape.jobs.includes("load") && shape.jobs.includes("export") && shape.jobs.length > 2;
|
|
2838
|
+
if (shape.jobs.length >= MIXED_JOB_COUNT || loadsAndExports) {
|
|
2839
|
+
out.push({
|
|
2840
|
+
phase: "shape",
|
|
2841
|
+
code: "shape/mixed-jobs",
|
|
2842
|
+
message: `${where}: one node does ${shape.jobs.join(", ")} (judged from the calls it makes). Give each job its own node.`,
|
|
2843
|
+
file: fileName
|
|
2844
|
+
});
|
|
2845
|
+
}
|
|
2846
|
+
return out;
|
|
2847
|
+
}
|
|
2848
|
+
function reportNodeShape(warnings) {
|
|
2849
|
+
if (!warnings.length) return;
|
|
2850
|
+
console.warn([
|
|
2851
|
+
`Node shape \u2014 ${warnings.length === 1 ? "one warning" : `${warnings.length} warnings`} (advice only, the check still passes):`,
|
|
2852
|
+
...warnings.map((item) => ` ${item.code}: ${item.message}`),
|
|
2853
|
+
' See "Design the graph" in doc/PROJECT_AUTHORING.md.'
|
|
2854
|
+
].join("\n"));
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2857
|
+
// server/src/runtime/stage.ts
|
|
2858
|
+
var BRIDGE_KEY = "__cascadeStageBridge";
|
|
2859
|
+
function installStageBridge(bridge) {
|
|
2860
|
+
const globals = globalThis;
|
|
2861
|
+
const previous = globals[BRIDGE_KEY];
|
|
2862
|
+
globals[BRIDGE_KEY] = bridge ?? void 0;
|
|
2863
|
+
return () => {
|
|
2864
|
+
globals[BRIDGE_KEY] = previous;
|
|
2865
|
+
};
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
// src/cli/projectStages.ts
|
|
2869
|
+
function stageWorkerCount(config, poolSize) {
|
|
2870
|
+
if (poolSize !== void 0) return Math.max(1, Math.floor(poolSize));
|
|
2871
|
+
const fromEnv = Number(process.env.CASCADE_STAGE_WORKERS);
|
|
2872
|
+
if (Number.isInteger(fromEnv) && fromEnv >= 1) return fromEnv;
|
|
2873
|
+
return config.workers ?? defaultStageWorkerCount();
|
|
2874
|
+
}
|
|
2875
|
+
var LANES_KEY = "__cascadeStageLanes";
|
|
2876
|
+
function installedStageLanes() {
|
|
2877
|
+
const lanes = globalThis[LANES_KEY];
|
|
2878
|
+
return typeof lanes === "number" && lanes > 1 ? lanes : 1;
|
|
2879
|
+
}
|
|
2880
|
+
function installProjectStages(project, options = {}) {
|
|
2881
|
+
let worker = null;
|
|
2882
|
+
const warmWorker = (config) => {
|
|
2883
|
+
if (options.worker === false || !config.worker) return null;
|
|
2884
|
+
const entrypoint = config.worker;
|
|
2885
|
+
const sources = stageSources(project, config);
|
|
2886
|
+
const size = stageWorkerCount(config, options.poolSize);
|
|
2887
|
+
const key = `${config.python}\0${entrypoint}\0${sources.roots.join("\0")}\0${size}`;
|
|
2888
|
+
if (worker?.key !== key) {
|
|
2889
|
+
worker?.process.shutdown();
|
|
2890
|
+
worker = {
|
|
2891
|
+
key,
|
|
2892
|
+
process: new PythonWorkerPool(size, () => new PythonWorker(project, entrypoint, config.python, process.env, {
|
|
2893
|
+
sources,
|
|
2894
|
+
// The one-shot path's bound, not Studio's two minutes: an offline
|
|
2895
|
+
// render may run a stage for several.
|
|
2896
|
+
requestTimeoutMs: 10 * 60 * 1e3,
|
|
2897
|
+
// What a stage prints is shown with --verbose only, as before.
|
|
2898
|
+
onStderr: options.onOutput ? (text) => {
|
|
2899
|
+
for (const line2 of text.split("\n")) options.onOutput(null, "stderr", line2);
|
|
2900
|
+
} : options.verbose ? (text) => {
|
|
2901
|
+
for (const line2 of text.split("\n")) console.warn(`[stage-worker] ${line2}`);
|
|
2902
|
+
} : () => {
|
|
2903
|
+
}
|
|
2904
|
+
}))
|
|
2905
|
+
};
|
|
2906
|
+
}
|
|
2907
|
+
return worker.process;
|
|
2908
|
+
};
|
|
2909
|
+
const globals = globalThis;
|
|
2910
|
+
const previousLanes = globals[LANES_KEY];
|
|
2911
|
+
try {
|
|
2912
|
+
const config = readStageConfig(project);
|
|
2913
|
+
globals[LANES_KEY] = options.worker === false || !config.worker ? 1 : stageWorkerCount(config, options.poolSize);
|
|
2914
|
+
} catch {
|
|
2915
|
+
globals[LANES_KEY] = 1;
|
|
2916
|
+
}
|
|
2917
|
+
if (options.prewarm) {
|
|
2918
|
+
try {
|
|
2919
|
+
warmWorker(readStageConfig(project))?.warm();
|
|
2920
|
+
} catch {
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
const dispose = installStageBridge(async (stage, args, callOptions) => {
|
|
2924
|
+
await settleProjectImages(project);
|
|
2925
|
+
const config = readStageConfig(project);
|
|
2926
|
+
const warm = warmWorker(config);
|
|
2927
|
+
if (warm) {
|
|
2928
|
+
const { result: result2 } = await callStage(project, config, stage, args, (envelope) => warm.callWithMeta(stage, args, { signal: callOptions?.signal, cascade: envelope }), { onTrace: options.onTrace });
|
|
2929
|
+
return result2;
|
|
2930
|
+
}
|
|
2931
|
+
const { result } = await callStage(project, config, stage, args, async (envelope) => ({
|
|
2932
|
+
result: await runProjectStage(project, stage, args, {
|
|
2933
|
+
// A superseded cook kills its subprocess rather than waiting for it.
|
|
2934
|
+
signal: callOptions?.signal,
|
|
2935
|
+
cascade: envelope,
|
|
2936
|
+
codeHash: envelope.code,
|
|
2937
|
+
/**
|
|
2938
|
+
* A stage can log, and until 0.8 nobody could read it: `stderr` was
|
|
2939
|
+
* consulted only on failure and `stdout` only for its last line. Behind
|
|
2940
|
+
* `--verbose`, because a stage that prints per mark would bury the
|
|
2941
|
+
* render it is part of; prefixed with the stage name, because a graph
|
|
2942
|
+
* runs several.
|
|
2943
|
+
*/
|
|
2944
|
+
onOutput: options.onOutput ? (stream, text) => {
|
|
2945
|
+
for (const line2 of text.split("\n")) options.onOutput(stage, stream, line2);
|
|
2946
|
+
} : options.verbose ? (stream, text) => {
|
|
2947
|
+
const write = stream === "stderr" ? console.warn : console.log;
|
|
2948
|
+
for (const line2 of text.split("\n")) write(`[${stage}] ${line2}`);
|
|
2949
|
+
} : void 0
|
|
2950
|
+
})
|
|
2951
|
+
}), { onTrace: options.onTrace });
|
|
2952
|
+
return result;
|
|
2953
|
+
});
|
|
2954
|
+
return () => {
|
|
2955
|
+
dispose();
|
|
2956
|
+
globals[LANES_KEY] = previousLanes;
|
|
2957
|
+
worker?.process.shutdown();
|
|
2958
|
+
worker = null;
|
|
2959
|
+
};
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2962
|
+
// src/cli/runTrace.ts
|
|
2963
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2964
|
+
import fs4 from "node:fs";
|
|
2965
|
+
import path5 from "node:path";
|
|
2966
|
+
var MAX_NODE_LOGS = 500;
|
|
2967
|
+
var MAX_LOG_LINE = 2e3;
|
|
2968
|
+
var cookingNode = new AsyncLocalStorage();
|
|
2969
|
+
function traceNodeScope(nodeId, run) {
|
|
2970
|
+
return cookingNode.run(nodeId, run);
|
|
2971
|
+
}
|
|
2972
|
+
var RunTrace = class {
|
|
2973
|
+
constructor(graphFile, cascade, seed) {
|
|
2974
|
+
this.graphFile = graphFile;
|
|
2975
|
+
this.cascade = cascade;
|
|
2976
|
+
this.seed = seed;
|
|
2977
|
+
this.runs = [];
|
|
2978
|
+
this.current = null;
|
|
2979
|
+
/** Nodes of the latest run still cooking, by id. */
|
|
2980
|
+
this.cooking = /* @__PURE__ */ new Map();
|
|
2981
|
+
/** Stage calls made outside any node (a run with no graph events). */
|
|
2982
|
+
this.loose = [];
|
|
2983
|
+
this.startedAt = /* @__PURE__ */ new Date();
|
|
2984
|
+
this.runStarted = null;
|
|
2985
|
+
this.envNames = [];
|
|
2986
|
+
}
|
|
2987
|
+
/** The node a stage call or log line belongs to. */
|
|
2988
|
+
owner() {
|
|
2989
|
+
const id = cookingNode.getStore();
|
|
2990
|
+
if (id !== void 0) {
|
|
2991
|
+
const scoped = this.cooking.get(id)?.node;
|
|
2992
|
+
if (scoped) return scoped;
|
|
2993
|
+
}
|
|
2994
|
+
return this.current?.node;
|
|
2995
|
+
}
|
|
2996
|
+
/** Follow one loaded graph's events. Returns the unsubscribe. */
|
|
2997
|
+
attach(graph) {
|
|
2998
|
+
const modules = new Map(graph.inspect().nodes.map((node) => [node.id, node.moduleId]));
|
|
2999
|
+
return graph.subscribe((event) => {
|
|
3000
|
+
if (event.type === "run:start") {
|
|
3001
|
+
this.runs.push({ runId: event.runId, frame: graph.getFrame(), nodes: [] });
|
|
3002
|
+
this.runStarted = performance.now();
|
|
3003
|
+
} else if (event.type === "run:complete") {
|
|
3004
|
+
const run = this.runs[this.runs.length - 1];
|
|
3005
|
+
if (run && this.runStarted !== null) run.ms = Math.round(performance.now() - this.runStarted);
|
|
3006
|
+
this.runStarted = null;
|
|
3007
|
+
} else if (event.type === "node:start") {
|
|
3008
|
+
const run = this.runs[this.runs.length - 1];
|
|
3009
|
+
if (!run) return;
|
|
3010
|
+
const node = { id: event.nodeId, module: modules.get(event.nodeId) ?? "", ms: 0, stages: [] };
|
|
3011
|
+
run.nodes.push(node);
|
|
3012
|
+
this.current = { node, started: performance.now() };
|
|
3013
|
+
this.cooking.set(event.nodeId, this.current);
|
|
3014
|
+
} else if (event.type === "node:complete") {
|
|
3015
|
+
const cooking = this.cooking.get(event.nodeId);
|
|
3016
|
+
if (!cooking) return;
|
|
3017
|
+
cooking.node.ms = Math.round(performance.now() - cooking.started);
|
|
3018
|
+
this.cooking.delete(event.nodeId);
|
|
3019
|
+
if (this.current === cooking) this.current = null;
|
|
3020
|
+
}
|
|
3021
|
+
});
|
|
3022
|
+
}
|
|
3023
|
+
/**
|
|
3024
|
+
* The latest run's whole frame took `frameMs`, its outputs saved: what is
|
|
3025
|
+
* left after the cook is the save. Called by the frame loop per frame and
|
|
3026
|
+
* once by a still run.
|
|
3027
|
+
*/
|
|
3028
|
+
recordSaved(frameMs) {
|
|
3029
|
+
const run = this.runs[this.runs.length - 1];
|
|
3030
|
+
if (!run) return;
|
|
3031
|
+
run.frameMs = Math.round(frameMs);
|
|
3032
|
+
if (run.ms !== void 0) run.saveMs = Math.max(0, run.frameMs - run.ms);
|
|
3033
|
+
}
|
|
3034
|
+
/** Record a stage call against the node cooking now. */
|
|
3035
|
+
recordStage(trace) {
|
|
3036
|
+
const node = this.owner();
|
|
3037
|
+
if (node) node.stages.push(trace);
|
|
3038
|
+
else this.loose.push(trace);
|
|
3039
|
+
}
|
|
3040
|
+
/**
|
|
3041
|
+
* Record a line printed while a node cooks, against that node. Returns
|
|
3042
|
+
* false when no node is cooking, so the caller leaves the line alone (the
|
|
3043
|
+
* CLI's own messages are not node output).
|
|
3044
|
+
*/
|
|
3045
|
+
recordLog(level, text) {
|
|
3046
|
+
const node = this.owner();
|
|
3047
|
+
if (!node) return false;
|
|
3048
|
+
for (const line2 of text.split("\n")) {
|
|
3049
|
+
if (!line2.trim()) continue;
|
|
3050
|
+
node.logs ??= [];
|
|
3051
|
+
if (node.logs.length >= MAX_NODE_LOGS) {
|
|
3052
|
+
node.logsDropped = (node.logsDropped ?? 0) + 1;
|
|
3053
|
+
continue;
|
|
3054
|
+
}
|
|
3055
|
+
node.logs.push({ level, text: line2.length > MAX_LOG_LINE ? `${line2.slice(0, MAX_LOG_LINE)}\u2026` : line2 });
|
|
3056
|
+
}
|
|
3057
|
+
return true;
|
|
3058
|
+
}
|
|
3059
|
+
/** Every logged line in order, with its node. */
|
|
3060
|
+
logs() {
|
|
3061
|
+
return this.runs.flatMap((run) => run.nodes.flatMap((node) => (node.logs ?? []).map((line2) => ({ node: node.id, ...line2 }))));
|
|
3062
|
+
}
|
|
3063
|
+
/** The names (never the values) of variables `--env` set, for the trace. */
|
|
3064
|
+
setEnvNames(names) {
|
|
3065
|
+
this.envNames = [...names];
|
|
3066
|
+
}
|
|
3067
|
+
/** Every stage call in order, with its node. */
|
|
3068
|
+
stages() {
|
|
3069
|
+
const list = [];
|
|
3070
|
+
for (const run of this.runs) {
|
|
3071
|
+
for (const node of run.nodes) for (const stage of node.stages) list.push({ node: node.id, ...stage });
|
|
3072
|
+
}
|
|
3073
|
+
for (const stage of this.loose) list.push({ node: null, ...stage });
|
|
3074
|
+
return list;
|
|
3075
|
+
}
|
|
3076
|
+
toJSON() {
|
|
3077
|
+
return {
|
|
3078
|
+
format: 1,
|
|
3079
|
+
graph: this.graphFile,
|
|
3080
|
+
startedAt: this.startedAt.toISOString(),
|
|
3081
|
+
cascade: this.cascade,
|
|
3082
|
+
seed: this.seed,
|
|
3083
|
+
...this.envNames.length ? { env: this.envNames } : {},
|
|
3084
|
+
runs: this.runs
|
|
3085
|
+
};
|
|
3086
|
+
}
|
|
3087
|
+
/** Write the trace into the project and return its project-relative path. */
|
|
3088
|
+
write(projectRoot) {
|
|
3089
|
+
const stamp = this.startedAt.toISOString().replace(/[:.]/g, "-");
|
|
3090
|
+
const name = `${stamp}-${path5.basename(this.graphFile, ".cascade")}.json`;
|
|
3091
|
+
const relative = path5.posix.join(".cascade", "runs", name);
|
|
3092
|
+
const full = path5.join(projectRoot, relative);
|
|
3093
|
+
fs4.mkdirSync(path5.dirname(full), { recursive: true });
|
|
3094
|
+
fs4.writeFileSync(full, JSON.stringify(this.toJSON(), null, 2) + "\n");
|
|
3095
|
+
pruneTraces(path5.dirname(full));
|
|
3096
|
+
return relative;
|
|
3097
|
+
}
|
|
3098
|
+
};
|
|
3099
|
+
var CONSOLE_LEVELS = ["log", "info", "warn", "error", "debug"];
|
|
3100
|
+
function captureNodeConsole(trace) {
|
|
3101
|
+
const original = Object.fromEntries(CONSOLE_LEVELS.map((level) => [level, console[level]]));
|
|
3102
|
+
for (const level of CONSOLE_LEVELS) {
|
|
3103
|
+
console[level] = (...args) => {
|
|
3104
|
+
trace.recordLog(level, args.map((arg) => typeof arg === "string" ? arg : arg instanceof Error ? arg.stack ?? arg.message : safeJson(arg)).join(" "));
|
|
3105
|
+
original[level](...args);
|
|
3106
|
+
};
|
|
3107
|
+
}
|
|
3108
|
+
return () => {
|
|
3109
|
+
for (const level of CONSOLE_LEVELS) console[level] = original[level];
|
|
3110
|
+
};
|
|
3111
|
+
}
|
|
3112
|
+
function safeJson(value) {
|
|
3113
|
+
try {
|
|
3114
|
+
return JSON.stringify(value) ?? String(value);
|
|
3115
|
+
} catch {
|
|
3116
|
+
return String(value);
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
var KEPT_TRACES = 200;
|
|
3120
|
+
function pruneTraces(directory) {
|
|
3121
|
+
const traces = fs4.readdirSync(directory).filter((name) => name.endsWith(".json")).sort();
|
|
3122
|
+
for (const name of traces.slice(0, Math.max(0, traces.length - KEPT_TRACES))) {
|
|
3123
|
+
fs4.rmSync(path5.join(directory, name), { force: true });
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3126
|
+
|
|
2400
3127
|
// src/cli/projectRuntime.ts
|
|
3128
|
+
function headlessExecution() {
|
|
3129
|
+
return { values: "trust", concurrency: installedStageLanes(), nodeScope: traceNodeScope };
|
|
3130
|
+
}
|
|
2401
3131
|
function createProjectHost(file, registrations) {
|
|
2402
|
-
const project = new ProjectRoot(
|
|
3132
|
+
const project = new ProjectRoot(path6.dirname(path6.resolve(file)));
|
|
2403
3133
|
return createNodeRuntimeHost({
|
|
2404
3134
|
modules: resolver(registrations),
|
|
2405
3135
|
shell: project.shell,
|
|
@@ -2524,6 +3254,7 @@ async function checkProjectGraph(file, document2, strict = false) {
|
|
|
2524
3254
|
throw new Error(`Static check requires definition-v1 modules; dynamic modules: ${dynamic.join(", ")}`);
|
|
2525
3255
|
}
|
|
2526
3256
|
await preflightAgainstRunHost(file, prepared.registrations, document2);
|
|
3257
|
+
reportNodeShape(prepared.shape);
|
|
2527
3258
|
return unresolved;
|
|
2528
3259
|
}
|
|
2529
3260
|
async function runDeterministicProjectGraph(file, document2, entryNode, observe, afterRun) {
|
|
@@ -2533,7 +3264,10 @@ async function runDeterministicProjectGraph(file, document2, entryNode, observe,
|
|
|
2533
3264
|
const host = createProjectHost(file, prepared.registrations);
|
|
2534
3265
|
const runtime = createRuntime({
|
|
2535
3266
|
host,
|
|
2536
|
-
nodes: prepared.registrations
|
|
3267
|
+
nodes: prepared.registrations,
|
|
3268
|
+
// A run trusts what its nodes hand over instead of walking it to freeze
|
|
3269
|
+
// (Studio and `cook` keep `"freeze"`), and overlaps independent stages.
|
|
3270
|
+
...headlessExecution()
|
|
2537
3271
|
});
|
|
2538
3272
|
let graph;
|
|
2539
3273
|
try {
|
|
@@ -2547,6 +3281,7 @@ async function runDeterministicProjectGraph(file, document2, entryNode, observe,
|
|
|
2547
3281
|
if (result.status !== "completed") {
|
|
2548
3282
|
throw new Error(result.diagnostics.map((item) => item.message).join("\n") || `Graph ${result.status}`);
|
|
2549
3283
|
}
|
|
3284
|
+
await settleProjectImages({ root: path6.dirname(path6.resolve(file)) });
|
|
2550
3285
|
await afterRun?.(graph);
|
|
2551
3286
|
return true;
|
|
2552
3287
|
} finally {
|
|
@@ -2570,6 +3305,7 @@ async function cookProjectGraph(file, document2, target, observe) {
|
|
|
2570
3305
|
const result = await graph.run(target ? { target: { kind: "node", nodeId: target } } : {});
|
|
2571
3306
|
if (result.status !== "completed")
|
|
2572
3307
|
throw new Error(result.diagnostics.map((item) => item.message).join("\n") || `Graph ${result.status}`);
|
|
3308
|
+
await settleProjectImages({ root: path6.dirname(path6.resolve(file)) });
|
|
2573
3309
|
const outputs = /* @__PURE__ */ new Map();
|
|
2574
3310
|
for (const node of graph.inspect().nodes) outputs.set(node.id, new Map(graph.getOutputs(node.id)));
|
|
2575
3311
|
return { outputs, assets: host.capabilities?.assets };
|
|
@@ -2585,11 +3321,15 @@ async function renderDeterministicProjectFrames(file, document2, options) {
|
|
|
2585
3321
|
const prepared = await prepare(file, document2, true);
|
|
2586
3322
|
reportUnresolved(prepared);
|
|
2587
3323
|
if (!usesDeterministicHost(document2, prepared)) return null;
|
|
2588
|
-
const project = new ProjectRoot(
|
|
3324
|
+
const project = new ProjectRoot(path6.dirname(path6.resolve(file)));
|
|
2589
3325
|
const host = createProjectHost(file, prepared.registrations);
|
|
2590
3326
|
const runtime = createRuntime({
|
|
2591
3327
|
host,
|
|
2592
|
-
nodes: prepared.registrations
|
|
3328
|
+
nodes: prepared.registrations,
|
|
3329
|
+
// Every frame hands over fresh values; walking them to freeze was most
|
|
3330
|
+
// of a big-array frame. Independent stages (the morph's four renders)
|
|
3331
|
+
// cook side by side on the stage pool.
|
|
3332
|
+
...headlessExecution()
|
|
2593
3333
|
});
|
|
2594
3334
|
let graph;
|
|
2595
3335
|
const width = sequenceWidth(options.end);
|
|
@@ -2605,9 +3345,10 @@ async function renderDeterministicProjectFrames(file, document2, options) {
|
|
|
2605
3345
|
const targets = options.nodes?.length ? options.nodes : options.entryNode ? [options.entryNode] : [];
|
|
2606
3346
|
const extra = options.outNodes ?? [];
|
|
2607
3347
|
const out = outDirectory(options.out, (relative) => project.resolve(relative));
|
|
2608
|
-
await
|
|
3348
|
+
await fs5.mkdir(out.directory, { recursive: true });
|
|
2609
3349
|
const missing = unknownNodes(graph, [...targets, ...extra]);
|
|
2610
3350
|
if (missing.length) throw new Error(`No node ${missing.map((id) => `"${id}"`).join(", ")} in this graph (--node/--out-node take node ids; cascade graph read lists them).`);
|
|
3351
|
+
const imageOutputs = planImageOutputs(graph, { nodes: targets, extra });
|
|
2611
3352
|
for await (const frame of options.frameSource ?? frameRange(options.start, options.end, options.step ?? 1)) {
|
|
2612
3353
|
const frameStarted = performance.now();
|
|
2613
3354
|
if (options.signal?.aborted) {
|
|
@@ -2639,7 +3380,7 @@ async function renderDeterministicProjectFrames(file, document2, options) {
|
|
|
2639
3380
|
`frame ${frame} ${result.status} \u2014 ${result.diagnostics.map((item) => item.message).join("; ") || "no diagnostics"}`
|
|
2640
3381
|
);
|
|
2641
3382
|
}
|
|
2642
|
-
const outputs =
|
|
3383
|
+
const outputs = imageOutputs.collect();
|
|
2643
3384
|
if (!outputs.length) {
|
|
2644
3385
|
throw new Error(
|
|
2645
3386
|
targets.length || extra.length ? `No image output on ${[...targets, ...extra].join(", ")} \u2014 check that the node has an image output (or a Null passing one).` : "No image output to save: the graph has no unconsumed image port. Name the node to render with --node, or a mid-graph one to save as well with --out-node."
|
|
@@ -2651,7 +3392,7 @@ async function renderDeterministicProjectFrames(file, document2, options) {
|
|
|
2651
3392
|
if (!source) continue;
|
|
2652
3393
|
const name = sequenceFileName(output.nodeId, frame, source, width, output.shared ? output.port : void 0);
|
|
2653
3394
|
const relative = source.replace(/^\.?\//, "");
|
|
2654
|
-
await
|
|
3395
|
+
await copyImageFile(project, relative, path6.join(out.directory, name));
|
|
2655
3396
|
files.push(out.spell(name));
|
|
2656
3397
|
written.push({ nodeId: output.nodeId, port: output.port, source: relative, file: out.spell(name) });
|
|
2657
3398
|
}
|
|
@@ -2659,6 +3400,7 @@ async function renderDeterministicProjectFrames(file, document2, options) {
|
|
|
2659
3400
|
await options.onFrame?.({ frame, outputs: written, durationMs: performance.now() - frameStarted });
|
|
2660
3401
|
if (options.verbose) console.log(` frame ${frame}`);
|
|
2661
3402
|
}
|
|
3403
|
+
await settleProjectImages(project);
|
|
2662
3404
|
try {
|
|
2663
3405
|
gpu = { renderer: "dawn", adapter: host.capabilities.gpu.adapterInfo };
|
|
2664
3406
|
} catch {
|
|
@@ -2691,7 +3433,7 @@ async function inspectProjectGraph(file, document2) {
|
|
|
2691
3433
|
};
|
|
2692
3434
|
});
|
|
2693
3435
|
return {
|
|
2694
|
-
file:
|
|
3436
|
+
file: path6.resolve(file),
|
|
2695
3437
|
version: document2.version ?? null,
|
|
2696
3438
|
deterministic: nodes.length > 0 && nodes.every((node) => node.classification === "definition-v1"),
|
|
2697
3439
|
nodes,
|
|
@@ -2702,11 +3444,12 @@ async function prepare(file, document2, compileExecutors) {
|
|
|
2702
3444
|
if (!document2 || typeof document2 !== "object" || !Array.isArray(document2.nodes)) {
|
|
2703
3445
|
throw new Error("Graph document must contain a nodes array");
|
|
2704
3446
|
}
|
|
2705
|
-
const projectRoot =
|
|
3447
|
+
const projectRoot = path6.dirname(path6.resolve(file));
|
|
2706
3448
|
const registrations = [];
|
|
2707
3449
|
const deterministic = /* @__PURE__ */ new Set();
|
|
2708
3450
|
const seen = /* @__PURE__ */ new Set();
|
|
2709
3451
|
const unresolved = [];
|
|
3452
|
+
const shape = [];
|
|
2710
3453
|
for (const node of document2.nodes) {
|
|
2711
3454
|
const id = moduleId(node);
|
|
2712
3455
|
if (!id || seen.has(id)) continue;
|
|
@@ -2728,7 +3471,7 @@ async function prepare(file, document2, compileExecutors) {
|
|
|
2728
3471
|
registrations.push(savedRegistration(id, node));
|
|
2729
3472
|
continue;
|
|
2730
3473
|
}
|
|
2731
|
-
const source = await
|
|
3474
|
+
const source = await fs5.readFile(moduleFile, "utf8");
|
|
2732
3475
|
const mentionsDefinition = /\bexport\s+const\s+definition\b/.test(source);
|
|
2733
3476
|
const extracted = extractNodeDefinition(source, moduleFile, ts);
|
|
2734
3477
|
if (!extracted.ok) {
|
|
@@ -2744,24 +3487,34 @@ async function prepare(file, document2, compileExecutors) {
|
|
|
2744
3487
|
if (architectureDiagnostics.length) {
|
|
2745
3488
|
throw new Error(architectureDiagnostics.map(formatDiagnostic).join("\n"));
|
|
2746
3489
|
}
|
|
3490
|
+
shape.push(...nodeShapeWarnings(source, moduleFile, ts, id));
|
|
2747
3491
|
let executor;
|
|
2748
3492
|
deterministic.add(id);
|
|
2749
3493
|
registrations.push({
|
|
2750
3494
|
kind: "definition-v1",
|
|
2751
3495
|
moduleId: id,
|
|
2752
3496
|
definition: extracted.definition,
|
|
2753
|
-
loadExecute: compileExecutors ? () => executor ??= compileExecute(projectRoot, moduleFile)
|
|
3497
|
+
loadExecute: compileExecutors ? () => executor ??= compileExecute(projectRoot, moduleFile).then((execute) => (
|
|
3498
|
+
// A server node may read an image input with `node:fs`, the one
|
|
3499
|
+
// reader the host cannot make wait, so it starts once every image
|
|
3500
|
+
// written behind is on disk. Portable and browser nodes cannot
|
|
3501
|
+
// reach the filesystem and keep the in-memory handoff.
|
|
3502
|
+
extracted.definition.runsOn === "server" ? async (...args) => {
|
|
3503
|
+
await settleProjectImages({ root: projectRoot });
|
|
3504
|
+
return execute(...args);
|
|
3505
|
+
} : execute
|
|
3506
|
+
)) : async () => {
|
|
2754
3507
|
throw new Error("Static validation never loads execute");
|
|
2755
3508
|
}
|
|
2756
3509
|
});
|
|
2757
3510
|
}
|
|
2758
|
-
return { registrations, deterministic, unresolved };
|
|
3511
|
+
return { registrations, deterministic, unresolved, shape };
|
|
2759
3512
|
}
|
|
2760
3513
|
async function projectModuleFile(root, node, id) {
|
|
2761
3514
|
const tried = [];
|
|
2762
3515
|
const source = node.source;
|
|
2763
3516
|
if (source && typeof source === "object" && source.type === "project" && typeof source.file === "string") {
|
|
2764
|
-
const candidate2 =
|
|
3517
|
+
const candidate2 = path6.resolve(root, source.file);
|
|
2765
3518
|
if (within(root, candidate2)) {
|
|
2766
3519
|
tried.push(candidate2);
|
|
2767
3520
|
if (await exists(candidate2)) return { file: candidate2, tried };
|
|
@@ -2769,7 +3522,7 @@ async function projectModuleFile(root, node, id) {
|
|
|
2769
3522
|
}
|
|
2770
3523
|
if (!id.startsWith("project.")) return { file: null, tried };
|
|
2771
3524
|
const name = id.slice("project.".length);
|
|
2772
|
-
const candidate =
|
|
3525
|
+
const candidate = path6.resolve(root, "nodes", name, "index.ts");
|
|
2773
3526
|
if (!within(root, candidate)) return { file: null, tried };
|
|
2774
3527
|
tried.push(candidate);
|
|
2775
3528
|
return { file: await exists(candidate) ? candidate : null, tried };
|
|
@@ -2787,7 +3540,7 @@ function reExportedDefinition(source) {
|
|
|
2787
3540
|
});
|
|
2788
3541
|
}
|
|
2789
3542
|
function unresolvedModule(id, node, tried, root) {
|
|
2790
|
-
const relative = tried.map((candidate) =>
|
|
3543
|
+
const relative = tried.map((candidate) => path6.relative(root, candidate) || candidate);
|
|
2791
3544
|
const where = relative.length ? `looked for ${relative.join(" and ")}` : `no project file is possible for this module id \u2014 a project module is named "project.<dir>" or carries a source file`;
|
|
2792
3545
|
return {
|
|
2793
3546
|
phase: "prepare",
|
|
@@ -2797,9 +3550,9 @@ function unresolvedModule(id, node, tried, root) {
|
|
|
2797
3550
|
};
|
|
2798
3551
|
}
|
|
2799
3552
|
async function compileExecute(projectRoot, moduleFile) {
|
|
2800
|
-
const cache =
|
|
2801
|
-
await
|
|
2802
|
-
const output =
|
|
3553
|
+
const cache = path6.join(projectRoot, ".cascade-cache", "nodes");
|
|
3554
|
+
await fs5.mkdir(cache, { recursive: true });
|
|
3555
|
+
const output = path6.join(cache, `${path6.basename(path6.dirname(moduleFile))}.mjs`);
|
|
2803
3556
|
const staging = `${output}.${process.pid}.${Date.now()}.tmp`;
|
|
2804
3557
|
await build({
|
|
2805
3558
|
entryPoints: [moduleFile],
|
|
@@ -2812,24 +3565,27 @@ async function compileExecute(projectRoot, moduleFile) {
|
|
|
2812
3565
|
sourcemap: "inline",
|
|
2813
3566
|
plugins: [cliImageModule()]
|
|
2814
3567
|
});
|
|
2815
|
-
await
|
|
3568
|
+
await fs5.rename(staging, output);
|
|
2816
3569
|
const loaded = await import(`${pathToFileURL2(output).href}?v=${Date.now()}`);
|
|
2817
3570
|
if (typeof loaded.execute !== "function") throw new Error(`${moduleFile} does not export execute`);
|
|
2818
3571
|
return loaded.execute;
|
|
2819
3572
|
}
|
|
2820
3573
|
function cliImageModule() {
|
|
2821
|
-
const here =
|
|
2822
|
-
const
|
|
2823
|
-
|
|
3574
|
+
const here = path6.dirname(fileURLToPath(import.meta.url));
|
|
3575
|
+
const shim = (name) => [
|
|
3576
|
+
path6.join(here, "runtime", `${name}.ts`),
|
|
2824
3577
|
// dist/cli/runtime, copied by build-cli
|
|
2825
|
-
|
|
3578
|
+
path6.join(here, "..", "..", "server", "src", "runtime", `${name}.ts`)
|
|
2826
3579
|
// running from src
|
|
2827
|
-
].find((candidate) =>
|
|
3580
|
+
].find((candidate) => existsSync2(candidate));
|
|
3581
|
+
const sources = { image: shim("image"), io: shim("io") };
|
|
2828
3582
|
return {
|
|
2829
3583
|
name: "cascade-cli-image",
|
|
2830
3584
|
setup(builder) {
|
|
2831
|
-
|
|
2832
|
-
|
|
3585
|
+
builder.onResolve({ filter: /^(?:@field\/)?cascade\/(?:image|io)$/ }, (args) => {
|
|
3586
|
+
const source = sources[args.path.endsWith("/io") ? "io" : "image"];
|
|
3587
|
+
return source ? { path: source } : void 0;
|
|
3588
|
+
});
|
|
2833
3589
|
}
|
|
2834
3590
|
};
|
|
2835
3591
|
}
|
|
@@ -2919,12 +3675,12 @@ function formatDiagnostic(diagnostic) {
|
|
|
2919
3675
|
return `${location}${diagnostic.code}: ${diagnostic.message}`;
|
|
2920
3676
|
}
|
|
2921
3677
|
function within(root, candidate) {
|
|
2922
|
-
const relative =
|
|
2923
|
-
return relative === "" || !relative.startsWith("..") && !
|
|
3678
|
+
const relative = path6.relative(path6.resolve(root), candidate);
|
|
3679
|
+
return relative === "" || !relative.startsWith("..") && !path6.isAbsolute(relative);
|
|
2924
3680
|
}
|
|
2925
3681
|
async function exists(file) {
|
|
2926
3682
|
try {
|
|
2927
|
-
await
|
|
3683
|
+
await fs5.access(file);
|
|
2928
3684
|
return true;
|
|
2929
3685
|
} catch {
|
|
2930
3686
|
return false;
|
|
@@ -2989,6 +3745,10 @@ export {
|
|
|
2989
3745
|
validateNodeModuleArchitecture,
|
|
2990
3746
|
installProjectIo,
|
|
2991
3747
|
saveStillImages,
|
|
3748
|
+
installProjectStages,
|
|
3749
|
+
cookingNode,
|
|
3750
|
+
RunTrace,
|
|
3751
|
+
captureNodeConsole,
|
|
2992
3752
|
createProjectHost,
|
|
2993
3753
|
classifyPreflight,
|
|
2994
3754
|
validateProjectGraph,
|
|
@@ -3003,4 +3763,4 @@ export {
|
|
|
3003
3763
|
builtinModuleIds,
|
|
3004
3764
|
diagnoseGraph
|
|
3005
3765
|
};
|
|
3006
|
-
//# sourceMappingURL=chunk-
|
|
3766
|
+
//# sourceMappingURL=chunk-MP653A5O.js.map
|