@field/cascade 0.11.0 → 0.13.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 +8 -5
- package/ARCHITECTURE.md +5 -4
- package/CHANGELOG.md +245 -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-3JTJHF7V.js → chunk-2L34ERS2.js} +4 -4
- package/dist/cli/chunk-53LGFPWX.js +138 -0
- package/dist/cli/chunk-5ORUQ6ES.js +267 -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-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-WKURDVYP.js → chunk-JEW5MPGI.js} +28 -331
- 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-A4OAMOZH.js → chunk-NJFMDXHR.js} +13 -3
- package/dist/cli/{chunk-DUIUFVU2.js → chunk-OBZAP3GG.js} +49 -23
- package/dist/cli/chunk-QAGATJGE.js +336 -0
- package/dist/cli/{chunk-EUIAC3U3.js → chunk-QKL7CCRS.js} +966 -132
- package/dist/cli/{chunk-RLCAJBVR.js → chunk-TJW67QSI.js} +8 -7
- package/dist/cli/{chunk-RQZ4SX5R.js → chunk-W5PJ5RMP.js} +156 -84
- package/dist/cli/chunk-W7OUAT7J.js +122 -0
- 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-XVFRD7CF.js} +23 -17
- package/dist/cli/{commands-Q23M26QM.js → commands-M2XLTPTZ.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-RAEI7GVY.js} +36 -6
- package/dist/cli/{edit-JHCPTQFT.js → edit-S5KEQ5RA.js} +3 -3
- package/dist/cli/editorCommands-5BBG4S6V.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-MULQ7VD2.js} +4 -4
- package/dist/cli/{graphFile-VPSQ6OF7.js → graphFile-CITF36ZM.js} +25 -19
- package/dist/cli/{headlessCanvas-5XLDZT7M.js → headlessCanvas-ENRHC7GS.js} +3 -2
- package/dist/cli/{helpCommand-3VRE3KVO.js → helpCommand-X2QDANM3.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-YRIGRLYQ.js} +4 -4
- package/dist/cli/{parity-KINARCP2.js → parity-3N3HTKWX.js} +42 -36
- package/dist/cli/{pop-USDFXB7H.js → pop-B7SIIYKN.js} +5 -5
- package/dist/cli/project-SJF5ZF3F.js +12 -0
- package/dist/cli/{projectDefinition-DMTZN2NE.js → projectDefinition-F3D7UIHJ.js} +8 -8
- package/dist/cli/{projectRuntime-Z5PJOH47.js → projectRuntime-NY4XMDFH.js} +19 -12
- package/dist/cli/registerStandardNodes-BXJCB6E4.js +744 -0
- package/dist/cli/{render-YURAPPUI.js → render-Y23W6MY6.js} +29 -22
- package/dist/cli/{runCommand-PI764LEG.js → runCommand-XOG7AVC5.js} +3 -3
- package/dist/cli/runner-DNJO5WO2.js +43 -0
- package/dist/cli/runtime/image.ts +27 -2
- package/dist/cli/runtime/io.ts +78 -0
- package/dist/cli/share-B7HTCOJX.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-2K4COCGY.js +44 -0
- package/dist/contracts/capabilities.d.ts +39 -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 +11 -7
- package/dist/player/editor.js +3 -2
- package/dist/player/editorEngine.js +3 -2
- package/dist/player/embed.js +2 -0
- package/dist/player/types.d.ts +6 -0
- 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/definition/architecture.js +89 -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 +158 -4
- package/doc/WEB_PLAYER.md +9 -1
- 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-W5PJ5RMP.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]));
|
|
@@ -2047,6 +2237,9 @@ function validateNodeModuleArchitecture(source, fileName, ts2) {
|
|
|
2047
2237
|
(statement) => ts2.isVariableStatement(statement) && isDefinition(statement)
|
|
2048
2238
|
);
|
|
2049
2239
|
for (const statement of file.statements) {
|
|
2240
|
+
if (ts2.isVariableStatement(statement) && !isDefinition(statement) && !supersededByDefinition(statement) && isPureConstant(statement)) {
|
|
2241
|
+
continue;
|
|
2242
|
+
}
|
|
2050
2243
|
if (ts2.isVariableStatement(statement) && !isDefinition(statement)) {
|
|
2051
2244
|
const superseded = supersededByDefinition(statement);
|
|
2052
2245
|
add(
|
|
@@ -2111,6 +2304,48 @@ function validateNodeModuleArchitecture(source, fileName, ts2) {
|
|
|
2111
2304
|
if (!declaration || !ts2.isIdentifier(declaration.name)) return null;
|
|
2112
2305
|
return declaration.name.text === "icon" || declaration.name.text === "runsOn" ? declaration.name.text : null;
|
|
2113
2306
|
}
|
|
2307
|
+
function isPureConstant(statement) {
|
|
2308
|
+
if (!(statement.declarationList.flags & ts2.NodeFlags.Const)) return false;
|
|
2309
|
+
return statement.declarationList.declarations.every(
|
|
2310
|
+
(declaration) => ts2.isIdentifier(declaration.name) && declaration.initializer !== void 0 && isPure(declaration.initializer, false)
|
|
2311
|
+
);
|
|
2312
|
+
}
|
|
2313
|
+
function isPure(node, frozen) {
|
|
2314
|
+
if (ts2.isNumericLiteral(node) || ts2.isStringLiteral(node) || ts2.isBigIntLiteral(node) || ts2.isNoSubstitutionTemplateLiteral(node) || node.kind === ts2.SyntaxKind.TrueKeyword || node.kind === ts2.SyntaxKind.FalseKeyword || node.kind === ts2.SyntaxKind.NullKeyword) return true;
|
|
2315
|
+
if (ts2.isIdentifier(node)) return true;
|
|
2316
|
+
if (ts2.isArrowFunction(node) || ts2.isFunctionExpression(node)) return true;
|
|
2317
|
+
if (ts2.isParenthesizedExpression(node)) return isPure(node.expression, frozen);
|
|
2318
|
+
if (ts2.isAsExpression(node))
|
|
2319
|
+
return isPure(node.expression, frozen || isConstAssertion(node.type));
|
|
2320
|
+
if (ts2.isSatisfiesExpression(node) || ts2.isNonNullExpression(node) || ts2.isTypeAssertionExpression(node))
|
|
2321
|
+
return isPure(node.expression, frozen);
|
|
2322
|
+
if (ts2.isPrefixUnaryExpression(node))
|
|
2323
|
+
return node.operator !== ts2.SyntaxKind.PlusPlusToken && node.operator !== ts2.SyntaxKind.MinusMinusToken && isPure(node.operand, false);
|
|
2324
|
+
if (ts2.isBinaryExpression(node))
|
|
2325
|
+
return !isAssignment(node.operatorToken.kind) && isPure(node.left, false) && isPure(node.right, false);
|
|
2326
|
+
if (ts2.isConditionalExpression(node))
|
|
2327
|
+
return isPure(node.condition, false) && isPure(node.whenTrue, frozen) && isPure(node.whenFalse, frozen);
|
|
2328
|
+
if (ts2.isTemplateExpression(node))
|
|
2329
|
+
return node.templateSpans.every((span) => isPure(span.expression, false));
|
|
2330
|
+
if (ts2.isPropertyAccessExpression(node)) return isPure(node.expression, false);
|
|
2331
|
+
if (ts2.isElementAccessExpression(node))
|
|
2332
|
+
return isPure(node.expression, false) && isPure(node.argumentExpression, false);
|
|
2333
|
+
if (ts2.isArrayLiteralExpression(node))
|
|
2334
|
+
return frozen && node.elements.every((element) => !ts2.isSpreadElement(element) && isPure(element, true));
|
|
2335
|
+
if (ts2.isObjectLiteralExpression(node))
|
|
2336
|
+
return frozen && node.properties.every((property) => ts2.isPropertyAssignment(property) ? !ts2.isComputedPropertyName(property.name) && isPure(property.initializer, true) : ts2.isShorthandPropertyAssignment(property));
|
|
2337
|
+
if (ts2.isCallExpression(node) && ts2.isPropertyAccessExpression(node.expression) && ts2.isIdentifier(node.expression.expression) && node.expression.expression.text === "Object" && node.expression.name.text === "freeze" && node.arguments.length === 1) {
|
|
2338
|
+
const [argument] = node.arguments;
|
|
2339
|
+
return ts2.isArrayLiteralExpression(argument) ? argument.elements.every((element) => !ts2.isSpreadElement(element) && isPure(element, false)) : ts2.isObjectLiteralExpression(argument) ? argument.properties.every((property) => ts2.isPropertyAssignment(property) && !ts2.isComputedPropertyName(property.name) && isPure(property.initializer, false)) : false;
|
|
2340
|
+
}
|
|
2341
|
+
return false;
|
|
2342
|
+
}
|
|
2343
|
+
function isConstAssertion(type) {
|
|
2344
|
+
return ts2.isTypeReferenceNode(type) && ts2.isIdentifier(type.typeName) && type.typeName.text === "const";
|
|
2345
|
+
}
|
|
2346
|
+
function isAssignment(kind) {
|
|
2347
|
+
return kind >= ts2.SyntaxKind.FirstAssignment && kind <= ts2.SyntaxKind.LastAssignment;
|
|
2348
|
+
}
|
|
2114
2349
|
function isDefinition(statement) {
|
|
2115
2350
|
return Boolean(statement.declarationList.flags & ts2.NodeFlags.Const) && Boolean(statement.modifiers?.some((item) => item.kind === ts2.SyntaxKind.ExportKeyword)) && statement.declarationList.declarations.length === 1 && ts2.isIdentifier(statement.declarationList.declarations[0].name) && statement.declarationList.declarations[0].name.text === "definition";
|
|
2116
2351
|
}
|
|
@@ -2147,25 +2382,41 @@ function normalize(value) {
|
|
|
2147
2382
|
}
|
|
2148
2383
|
|
|
2149
2384
|
// src/cli/nodeFiles.ts
|
|
2385
|
+
import { randomBytes } from "node:crypto";
|
|
2150
2386
|
import fs from "node:fs/promises";
|
|
2151
2387
|
import path from "node:path";
|
|
2388
|
+
var KEYED_DIR = ".cascade-cache/keyed";
|
|
2389
|
+
var KEY = /^[A-Za-z0-9][A-Za-z0-9._-]*(?:\/[A-Za-z0-9][A-Za-z0-9._-]*)*$/;
|
|
2152
2390
|
function createNodeFileCapability(project) {
|
|
2153
2391
|
const within2 = (relative, what) => {
|
|
2154
2392
|
try {
|
|
2155
|
-
|
|
2393
|
+
const clean = relative.replace(/^\.?\//, "");
|
|
2394
|
+
return what === "writing" ? project.resolve(clean) : project.resolveMedia(clean);
|
|
2156
2395
|
} catch (error) {
|
|
2157
2396
|
throw new Error(
|
|
2158
2397
|
`files: ${what} outside the project is refused \u2014 ${relative} (${error instanceof Error ? error.message : String(error)})`
|
|
2159
2398
|
);
|
|
2160
2399
|
}
|
|
2161
2400
|
};
|
|
2162
|
-
const
|
|
2401
|
+
const writable = (relative) => {
|
|
2163
2402
|
const clean = relative.replace(/^\.?\//, "");
|
|
2164
|
-
|
|
2403
|
+
const full = within2(clean, "writing");
|
|
2404
|
+
if (!project.writeFolderOf(clean))
|
|
2165
2405
|
throw new Error(
|
|
2166
|
-
`files: writes are confined to .cascade-cache/ \u2014 got ${clean}. A node's output is derived data
|
|
2406
|
+
`files: writes are confined to .cascade-cache/ and the folders cascade.json "writes" declares \u2014 got ${clean}. A node's output is derived data; declare an export folder such as "writes": ["renders"] to write named files.`
|
|
2167
2407
|
);
|
|
2168
|
-
return
|
|
2408
|
+
return full;
|
|
2409
|
+
};
|
|
2410
|
+
const writeWhole2 = async (full, data) => {
|
|
2411
|
+
await fs.mkdir(path.dirname(full), { recursive: true });
|
|
2412
|
+
const staging = `${full}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
|
|
2413
|
+
await fs.writeFile(staging, data);
|
|
2414
|
+
await fs.rename(staging, full);
|
|
2415
|
+
};
|
|
2416
|
+
const keyed = (key) => {
|
|
2417
|
+
if (typeof key !== "string" || key.length > 512 || !KEY.test(key))
|
|
2418
|
+
throw new Error(`files.cache: key ${JSON.stringify(key)} must be letters, digits, ".", "_" and "-" in "/"-separated parts, each starting with a letter or digit`);
|
|
2419
|
+
return within2(`${KEYED_DIR}/${key}`, "writing");
|
|
2169
2420
|
};
|
|
2170
2421
|
const live = (options) => {
|
|
2171
2422
|
if (options.signal.aborted) throw new Error("files: cancelled");
|
|
@@ -2177,9 +2428,22 @@ function createNodeFileCapability(project) {
|
|
|
2177
2428
|
},
|
|
2178
2429
|
async write(relative, data, options) {
|
|
2179
2430
|
live(options);
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2431
|
+
await writeWhole2(writable(relative), data);
|
|
2432
|
+
},
|
|
2433
|
+
cache: {
|
|
2434
|
+
async get(key, options) {
|
|
2435
|
+
live(options);
|
|
2436
|
+
try {
|
|
2437
|
+
return new Uint8Array(await fs.readFile(keyed(key)));
|
|
2438
|
+
} catch (error) {
|
|
2439
|
+
if (error.code === "ENOENT") return null;
|
|
2440
|
+
throw error;
|
|
2441
|
+
}
|
|
2442
|
+
},
|
|
2443
|
+
async put(key, data, options) {
|
|
2444
|
+
live(options);
|
|
2445
|
+
await writeWhole2(keyed(key), data);
|
|
2446
|
+
}
|
|
2183
2447
|
},
|
|
2184
2448
|
async list(relative, options) {
|
|
2185
2449
|
live(options);
|
|
@@ -2211,56 +2475,198 @@ import { pathToFileURL } from "node:url";
|
|
|
2211
2475
|
|
|
2212
2476
|
// src/cli/headlessIo.ts
|
|
2213
2477
|
import crypto from "node:crypto";
|
|
2478
|
+
import { existsSync } from "node:fs";
|
|
2214
2479
|
import fs2 from "node:fs/promises";
|
|
2215
2480
|
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
2481
|
var CACHE_DIR = ".cascade-cache";
|
|
2230
2482
|
function contentAddressed(relative, body) {
|
|
2231
|
-
|
|
2483
|
+
return addressed(relative, crypto.createHash("sha1").update(body).digest("hex").slice(0, 12));
|
|
2484
|
+
}
|
|
2485
|
+
function pixelAddressed(relative, pixels) {
|
|
2486
|
+
const hash = crypto.createHash("sha1").update(`${pixels.width}x${pixels.height}\0`).update(pixels.data).digest("hex").slice(0, 12);
|
|
2487
|
+
return addressed(relative, hash);
|
|
2488
|
+
}
|
|
2489
|
+
function addressed(relative, hash) {
|
|
2232
2490
|
const directory = path2.posix.dirname(relative);
|
|
2233
2491
|
const name = path2.posix.basename(relative);
|
|
2234
2492
|
const [stem, ...rest] = name.split(".");
|
|
2235
2493
|
const tail = rest.length ? `.${rest.join(".")}` : "";
|
|
2236
2494
|
return path2.posix.join(directory, `${stem}.${hash}${tail}`);
|
|
2237
2495
|
}
|
|
2496
|
+
var INTERMEDIATE_PNG = { level: 1, adaptiveFiltering: false };
|
|
2497
|
+
var FINAL_PNG = { level: 6, adaptiveFiltering: true };
|
|
2498
|
+
var sharpLoad;
|
|
2499
|
+
function loadSharp() {
|
|
2500
|
+
sharpLoad ??= import("sharp").then((module) => module.default, () => null);
|
|
2501
|
+
return sharpLoad;
|
|
2502
|
+
}
|
|
2503
|
+
async function encodePixelsPng(pixels, options = INTERMEDIATE_PNG) {
|
|
2504
|
+
const sharp = await loadSharp();
|
|
2505
|
+
if (sharp) {
|
|
2506
|
+
const raw = Buffer.from(pixels.data.buffer, pixels.data.byteOffset, pixels.data.byteLength);
|
|
2507
|
+
return sharp(raw, { raw: { width: pixels.width, height: pixels.height, channels: 4 } }).png({ compressionLevel: options.level, adaptiveFiltering: options.adaptiveFiltering }).toBuffer();
|
|
2508
|
+
}
|
|
2509
|
+
const Canvas = globalThis.OffscreenCanvas;
|
|
2510
|
+
const Data = globalThis.ImageData;
|
|
2511
|
+
if (!Canvas || !Data) throw new Error("cascade/io: cannot encode a PNG here (neither sharp nor a canvas is installed)");
|
|
2512
|
+
const canvas = new Canvas(pixels.width, pixels.height);
|
|
2513
|
+
canvas.getContext("2d").putImageData(new Data(pixels.data, pixels.width, pixels.height), 0, 0);
|
|
2514
|
+
return new Uint8Array(await (await canvas.convertToBlob({ type: "image/png" })).arrayBuffer());
|
|
2515
|
+
}
|
|
2516
|
+
async function writeWhole(full, data) {
|
|
2517
|
+
await fs2.mkdir(path2.dirname(full), { recursive: true });
|
|
2518
|
+
const staging = `${full}.${process.pid}.${crypto.randomBytes(4).toString("hex")}.tmp`;
|
|
2519
|
+
await fs2.writeFile(staging, data);
|
|
2520
|
+
await fs2.rename(staging, full);
|
|
2521
|
+
}
|
|
2522
|
+
var RESIDENT_BUDGET_BYTES = 512 * 1024 * 1024;
|
|
2523
|
+
var ResidentImages = class {
|
|
2524
|
+
constructor(project, budget = RESIDENT_BUDGET_BYTES) {
|
|
2525
|
+
this.project = project;
|
|
2526
|
+
this.budget = budget;
|
|
2527
|
+
this.held = /* @__PURE__ */ new Map();
|
|
2528
|
+
this.bytes = 0;
|
|
2529
|
+
/** Files written behind, for a test or a trace. */
|
|
2530
|
+
this.encodes = 0;
|
|
2531
|
+
}
|
|
2532
|
+
/** Hold the pixels, start the write behind, return the path at once. */
|
|
2533
|
+
keep(relative, pixels) {
|
|
2534
|
+
const target = pixelAddressed(relative, pixels);
|
|
2535
|
+
const known = this.held.get(target);
|
|
2536
|
+
if (known) {
|
|
2537
|
+
this.touch(target, known);
|
|
2538
|
+
return target;
|
|
2539
|
+
}
|
|
2540
|
+
const full = this.project.resolve(target);
|
|
2541
|
+
const entry = {
|
|
2542
|
+
pixels,
|
|
2543
|
+
done: false,
|
|
2544
|
+
written: (async () => {
|
|
2545
|
+
if (existsSync(full)) return;
|
|
2546
|
+
this.encodes += 1;
|
|
2547
|
+
await writeWhole(full, await encodePixelsPng(pixels, INTERMEDIATE_PNG));
|
|
2548
|
+
})()
|
|
2549
|
+
};
|
|
2550
|
+
entry.written.then(() => {
|
|
2551
|
+
entry.done = true;
|
|
2552
|
+
this.trim();
|
|
2553
|
+
}, () => {
|
|
2554
|
+
entry.done = true;
|
|
2555
|
+
});
|
|
2556
|
+
this.held.set(target, entry);
|
|
2557
|
+
this.bytes += pixels.data.byteLength;
|
|
2558
|
+
this.trim();
|
|
2559
|
+
return target;
|
|
2560
|
+
}
|
|
2561
|
+
/** The pixels behind a path, while held. */
|
|
2562
|
+
pixels(relative) {
|
|
2563
|
+
const entry = this.held.get(relative);
|
|
2564
|
+
if (!entry) return void 0;
|
|
2565
|
+
this.touch(relative, entry);
|
|
2566
|
+
return entry.pixels;
|
|
2567
|
+
}
|
|
2568
|
+
/** Resolve when `relative` (or every held image) is a whole file. */
|
|
2569
|
+
async settle(relative) {
|
|
2570
|
+
if (relative !== void 0) {
|
|
2571
|
+
await this.held.get(relative)?.written;
|
|
2572
|
+
return;
|
|
2573
|
+
}
|
|
2574
|
+
await Promise.all([...this.held.values()].filter((entry) => !entry.done).map((entry) => entry.written));
|
|
2575
|
+
}
|
|
2576
|
+
touch(key, entry) {
|
|
2577
|
+
this.held.delete(key);
|
|
2578
|
+
this.held.set(key, entry);
|
|
2579
|
+
}
|
|
2580
|
+
/** Let the oldest written images go until under budget; a pending one stays. */
|
|
2581
|
+
trim() {
|
|
2582
|
+
if (this.bytes <= this.budget) return;
|
|
2583
|
+
for (const [key, entry] of this.held) {
|
|
2584
|
+
if (this.bytes <= this.budget) break;
|
|
2585
|
+
if (!entry.done) continue;
|
|
2586
|
+
this.held.delete(key);
|
|
2587
|
+
this.bytes -= entry.pixels.data.byteLength;
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
};
|
|
2591
|
+
var residentStores = /* @__PURE__ */ new Map();
|
|
2592
|
+
function residentImages(project) {
|
|
2593
|
+
return residentStores.get(project.root);
|
|
2594
|
+
}
|
|
2595
|
+
async function settleProjectImages(project) {
|
|
2596
|
+
await residentImages(project)?.settle();
|
|
2597
|
+
}
|
|
2598
|
+
async function copyImageFile(project, relative, destination) {
|
|
2599
|
+
const clean = relative.replace(/^\.?\//, "");
|
|
2600
|
+
const store = residentImages(project);
|
|
2601
|
+
const pixels = /\.png$/i.test(clean) ? store?.pixels(clean) : void 0;
|
|
2602
|
+
if (pixels) {
|
|
2603
|
+
await writeWhole(destination, await encodePixelsPng(pixels, FINAL_PNG));
|
|
2604
|
+
return;
|
|
2605
|
+
}
|
|
2606
|
+
await store?.settle(clean);
|
|
2607
|
+
await fs2.copyFile(project.resolveMedia(clean), destination);
|
|
2608
|
+
}
|
|
2238
2609
|
function createIoBridge(project) {
|
|
2239
|
-
|
|
2610
|
+
const confined = (relative) => {
|
|
2611
|
+
const clean = relative.replace(/^\.?\//, "");
|
|
2612
|
+
if (!clean.startsWith(`${CACHE_DIR}/`)) {
|
|
2613
|
+
throw new Error(`cascade/io: writes are confined to ${CACHE_DIR}/ \u2014 got ${clean}`);
|
|
2614
|
+
}
|
|
2615
|
+
return clean;
|
|
2616
|
+
};
|
|
2617
|
+
const bridge = {
|
|
2618
|
+
/** An intermediate image: held in memory when the run keeps pixels, else a fast PNG now. */
|
|
2619
|
+
async keep(relative, pixels) {
|
|
2620
|
+
const clean = confined(relative);
|
|
2621
|
+
if (!pixels.width || !pixels.height) throw new Error(`cascade/io: refusing to store an empty image to ${clean}`);
|
|
2622
|
+
const store = residentImages(project);
|
|
2623
|
+
if (store) return store.keep(clean, pixels);
|
|
2624
|
+
const target = pixelAddressed(clean, pixels);
|
|
2625
|
+
const full = project.resolve(target);
|
|
2626
|
+
if (!existsSync(full)) await writeWhole(full, await encodePixelsPng(pixels, INTERMEDIATE_PNG));
|
|
2627
|
+
return target;
|
|
2628
|
+
},
|
|
2629
|
+
pixels(relative) {
|
|
2630
|
+
return residentImages(project)?.pixels(relative.replace(/^\.?\//, ""));
|
|
2631
|
+
},
|
|
2240
2632
|
async read(relative) {
|
|
2241
|
-
const
|
|
2633
|
+
const clean = relative.replace(/^\.?\//, "");
|
|
2634
|
+
const full = project.resolveMedia(clean);
|
|
2242
2635
|
try {
|
|
2636
|
+
await residentImages(project)?.settle(clean);
|
|
2243
2637
|
return await fs2.readFile(full);
|
|
2244
2638
|
} catch (error) {
|
|
2245
2639
|
throw new Error(`cascade/io: cannot load ${relative} \u2014 ${error instanceof Error ? error.message : error}`);
|
|
2246
2640
|
}
|
|
2247
2641
|
},
|
|
2248
2642
|
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
|
-
}
|
|
2643
|
+
const clean = confined(relative);
|
|
2253
2644
|
if (!data.byteLength) throw new Error(`cascade/io: refusing to write an empty file to ${clean}`);
|
|
2254
2645
|
const target = contentAddressed(clean, data);
|
|
2255
|
-
|
|
2256
|
-
await fs2.mkdir(path2.dirname(full), { recursive: true });
|
|
2257
|
-
await fs2.writeFile(full, data);
|
|
2646
|
+
await writeWhole(project.resolve(target), data);
|
|
2258
2647
|
return target;
|
|
2259
2648
|
}
|
|
2260
2649
|
};
|
|
2650
|
+
return bridge;
|
|
2261
2651
|
}
|
|
2262
|
-
function installProjectIo(project) {
|
|
2263
|
-
|
|
2652
|
+
function installProjectIo(project, options = {}) {
|
|
2653
|
+
let removeStore = () => {
|
|
2654
|
+
};
|
|
2655
|
+
if (options.resident) {
|
|
2656
|
+
const store = new ResidentImages(project);
|
|
2657
|
+
const previous = residentStores.get(project.root);
|
|
2658
|
+
residentStores.set(project.root, store);
|
|
2659
|
+
removeStore = () => {
|
|
2660
|
+
if (residentStores.get(project.root) !== store) return;
|
|
2661
|
+
if (previous) residentStores.set(project.root, previous);
|
|
2662
|
+
else residentStores.delete(project.root);
|
|
2663
|
+
};
|
|
2664
|
+
}
|
|
2665
|
+
const removeBridge = installIoBridge(createIoBridge(project));
|
|
2666
|
+
return () => {
|
|
2667
|
+
removeBridge();
|
|
2668
|
+
removeStore();
|
|
2669
|
+
};
|
|
2264
2670
|
}
|
|
2265
2671
|
|
|
2266
2672
|
// src/cli/nodeAssets.ts
|
|
@@ -2280,7 +2686,18 @@ function createNodeAssetCapability(project) {
|
|
|
2280
2686
|
const written = await io.write(`${CACHE_DIR2}/${assetFileName(metadata.suggestedName)}`, data);
|
|
2281
2687
|
return { path: written, ...metadata.mediaType ? { mediaType: metadata.mediaType } : {} };
|
|
2282
2688
|
},
|
|
2689
|
+
// `cascade/image`'s `writeImage` hands an intermediate over as pixels:
|
|
2690
|
+
// the same file policy as `write`, through the bridge's fast PNG.
|
|
2691
|
+
async keepImage(pixels, metadata) {
|
|
2692
|
+
const written = await io.keep(`${CACHE_DIR2}/${assetFileName(metadata.suggestedName)}`, pixels);
|
|
2693
|
+
return { path: written, mediaType: "image/png" };
|
|
2694
|
+
},
|
|
2695
|
+
// And reads them back from memory while the run holds them.
|
|
2696
|
+
imagePixels(ref) {
|
|
2697
|
+
return io.pixels(String(ref.path));
|
|
2698
|
+
},
|
|
2283
2699
|
async resolveUrl(ref) {
|
|
2700
|
+
await settleProjectImages(project);
|
|
2284
2701
|
const full = project.resolveMedia(String(ref.path).replace(/^\.?\//, ""));
|
|
2285
2702
|
return { value: pathToFileURL(full).href, release: () => {
|
|
2286
2703
|
} };
|
|
@@ -2300,24 +2717,43 @@ function looksLikeImage(value) {
|
|
|
2300
2717
|
return IMAGE_EXTENSIONS.has(path4.extname(file).toLowerCase());
|
|
2301
2718
|
}
|
|
2302
2719
|
function collectImageOutputs(graph, options = {}) {
|
|
2720
|
+
return planImageOutputs(graph, options).collect();
|
|
2721
|
+
}
|
|
2722
|
+
function planImageOutputs(graph, options = {}) {
|
|
2303
2723
|
const wanted = new Set(options.nodes ?? []);
|
|
2304
2724
|
const extra = new Set(options.extra ?? []);
|
|
2305
|
-
const found = [];
|
|
2306
2725
|
const inspection = graph.inspect();
|
|
2307
2726
|
const isAssertion = (moduleId2) => moduleId2.startsWith("cascade.assert.");
|
|
2308
2727
|
const assertions = new Set(inspection.nodes.filter((node) => isAssertion(node.moduleId)).map((node) => node.id));
|
|
2309
2728
|
const consumed = new Set(inspection.connections.filter((connection) => !assertions.has(connection.target.nodeId)).map((connection) => `${connection.source.nodeId}\0${connection.source.outputName}`));
|
|
2729
|
+
const candidates = [];
|
|
2310
2730
|
for (const node of inspection.nodes) {
|
|
2311
2731
|
const named = wanted.has(node.id) || extra.has(node.id);
|
|
2312
2732
|
if (wanted.size && !named) continue;
|
|
2313
2733
|
if (!named && assertions.has(node.id)) continue;
|
|
2314
|
-
const ports = Object.entries(node.outputs).filter(([, port]) => port.
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2734
|
+
const ports = Object.entries(node.outputs).filter(([, port]) => port.type === "image" || port.type === "any").map(([name, port]) => ({
|
|
2735
|
+
name,
|
|
2736
|
+
type: port.type,
|
|
2737
|
+
// Consumed downstream: not a final image, though it still counts
|
|
2738
|
+
// towards `shared` when it carries one.
|
|
2739
|
+
skip: !named && port.connected && consumed.has(`${node.id}\0${name}`)
|
|
2740
|
+
}));
|
|
2741
|
+
if (ports.length) candidates.push({ nodeId: node.id, ports });
|
|
2319
2742
|
}
|
|
2320
|
-
return
|
|
2743
|
+
return {
|
|
2744
|
+
collect() {
|
|
2745
|
+
const found = [];
|
|
2746
|
+
for (const candidate of candidates) {
|
|
2747
|
+
const values = graph.getOutputs(candidate.nodeId);
|
|
2748
|
+
const ports = candidate.ports.map((port) => ({ ...port, value: values.get(port.name) })).filter((port) => port.value !== void 0 && (port.type === "image" || looksLikeImage(port.value)));
|
|
2749
|
+
for (const port of ports) {
|
|
2750
|
+
if (port.skip) continue;
|
|
2751
|
+
found.push({ nodeId: candidate.nodeId, port: port.name, value: port.value, shared: ports.length > 1 });
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
return found;
|
|
2755
|
+
}
|
|
2756
|
+
};
|
|
2321
2757
|
}
|
|
2322
2758
|
function unknownNodes(graph, ids) {
|
|
2323
2759
|
const known = new Set(graph.inspect().nodes.map((node) => node.id));
|
|
@@ -2346,7 +2782,8 @@ async function saveStillImages(graph, project, options) {
|
|
|
2346
2782
|
continue;
|
|
2347
2783
|
}
|
|
2348
2784
|
const name = stillFileName(output, relative);
|
|
2349
|
-
await fs3.copyFile(source, path4.join(out.directory, name));
|
|
2785
|
+
if (path4.isAbsolute(relative)) await fs3.copyFile(source, path4.join(out.directory, name));
|
|
2786
|
+
else await copyImageFile(project, relative, path4.join(out.directory, name));
|
|
2350
2787
|
saved.push({ node: output.nodeId, port: output.port, source, file: out.spell(name) });
|
|
2351
2788
|
}
|
|
2352
2789
|
if (out && !saved.length) {
|
|
@@ -2378,7 +2815,7 @@ function createDawnGpuHost() {
|
|
|
2378
2815
|
try {
|
|
2379
2816
|
const adapter = await gpu.requestAdapter();
|
|
2380
2817
|
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
|
|
2818
|
+
device = await requestDeviceWithAdapterLimits(adapter);
|
|
2382
2819
|
const info = adapter.info;
|
|
2383
2820
|
return { device, info: {
|
|
2384
2821
|
vendor: info.vendor,
|
|
@@ -2397,9 +2834,376 @@ function createDawnGpuHost() {
|
|
|
2397
2834
|
});
|
|
2398
2835
|
}
|
|
2399
2836
|
|
|
2837
|
+
// src/cli/nodeShape.ts
|
|
2838
|
+
var LONG_EXECUTE_LINES = 150;
|
|
2839
|
+
var MIXED_JOB_COUNT = 4;
|
|
2840
|
+
var JOB_ORDER = ["load", "generate", "colour", "draw", "export"];
|
|
2841
|
+
var BY_NAME = {
|
|
2842
|
+
load: /^(readFile(Sync)?|loadImage|loadFont|loadJson|readJson|fetch|decodeImage)$/,
|
|
2843
|
+
generate: /^(random|rng|hash\d*|noise\w*|\w*[nN]oise\d*|fbm|simplex\w*|perlin\w*|worley\w*|poisson\w*|scatter\w*)$/,
|
|
2844
|
+
colour: /^(oklch|oklab|\w*[oO]klab\w*|\w*[oO]klch\w*|okMix|hsl\w*|palette\w*|mixColou?r|lerpColou?r|\w*Palette)$/,
|
|
2845
|
+
draw: /^(getContext|beginPath|fillRect|strokeRect|drawImage|putImageData|createImageData|arc|lineTo|createRenderPipeline|createComputePipeline|draw|drawIndexed|dispatchWorkgroups)$/,
|
|
2846
|
+
export: /^(writeFile(Sync)?|savePng|saveSvg|saveJpe?g|toBuffer|toBlob|toDataURL|encodePng|encodeJpe?g|encodeImage)$/
|
|
2847
|
+
};
|
|
2848
|
+
var CAPABILITY_RECEIVER = /(^|\.)(files|assets)$/;
|
|
2849
|
+
function measureNodeShape(source, fileName, ts2) {
|
|
2850
|
+
const file = ts2.createSourceFile(fileName, source, ts2.ScriptTarget.Latest, true, ts2.ScriptKind.TS);
|
|
2851
|
+
let executeLines = null;
|
|
2852
|
+
let executeLine = null;
|
|
2853
|
+
const jobs = /* @__PURE__ */ new Set();
|
|
2854
|
+
for (const statement of file.statements) {
|
|
2855
|
+
const body = executeBody(statement);
|
|
2856
|
+
if (!body) continue;
|
|
2857
|
+
const start = file.getLineAndCharacterOfPosition(body.getStart(file)).line;
|
|
2858
|
+
const end = file.getLineAndCharacterOfPosition(body.getEnd()).line;
|
|
2859
|
+
executeLines = end - start + 1;
|
|
2860
|
+
executeLine = file.getLineAndCharacterOfPosition(statement.getStart(file)).line + 1;
|
|
2861
|
+
}
|
|
2862
|
+
visit(file);
|
|
2863
|
+
return { executeLines, executeLine, jobs: JOB_ORDER.filter((job) => jobs.has(job)) };
|
|
2864
|
+
function executeBody(statement) {
|
|
2865
|
+
if (ts2.isFunctionDeclaration(statement) && statement.name?.text === "execute") return statement.body;
|
|
2866
|
+
if (ts2.isVariableStatement(statement)) {
|
|
2867
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
2868
|
+
if (!ts2.isIdentifier(declaration.name) || declaration.name.text !== "execute") continue;
|
|
2869
|
+
const init = declaration.initializer;
|
|
2870
|
+
if (init && (ts2.isArrowFunction(init) || ts2.isFunctionExpression(init))) return init.body;
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
return void 0;
|
|
2874
|
+
}
|
|
2875
|
+
function visit(node) {
|
|
2876
|
+
if (ts2.isCallExpression(node)) classify(node.expression);
|
|
2877
|
+
ts2.forEachChild(node, visit);
|
|
2878
|
+
}
|
|
2879
|
+
function classify(callee) {
|
|
2880
|
+
let name;
|
|
2881
|
+
if (ts2.isIdentifier(callee)) name = callee.text;
|
|
2882
|
+
else if (ts2.isPropertyAccessExpression(callee)) {
|
|
2883
|
+
name = callee.name.text;
|
|
2884
|
+
const receiver = callee.expression.getText(file);
|
|
2885
|
+
if (CAPABILITY_RECEIVER.test(receiver)) {
|
|
2886
|
+
if (/^(read|list|stat|resolveUrl)$/.test(name)) jobs.add("load");
|
|
2887
|
+
if (name === "write") jobs.add("export");
|
|
2888
|
+
}
|
|
2889
|
+
if (receiver === "Math" && name === "random") {
|
|
2890
|
+
jobs.add("generate");
|
|
2891
|
+
return;
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
if (!name) return;
|
|
2895
|
+
for (const job of JOB_ORDER) if (BY_NAME[job].test(name)) jobs.add(job);
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
function nodeShapeWarnings(source, fileName, ts2, moduleId2) {
|
|
2899
|
+
const shape = measureNodeShape(source, fileName, ts2);
|
|
2900
|
+
const where = moduleId2 ?? fileName;
|
|
2901
|
+
const out = [];
|
|
2902
|
+
if (shape.executeLines !== null && shape.executeLines > LONG_EXECUTE_LINES) {
|
|
2903
|
+
out.push({
|
|
2904
|
+
phase: "shape",
|
|
2905
|
+
code: "shape/long-execute",
|
|
2906
|
+
message: `${where}: execute is ${shape.executeLines} lines (over ${LONG_EXECUTE_LINES}). Split it into single-job nodes wired in the graph.`,
|
|
2907
|
+
file: fileName,
|
|
2908
|
+
line: shape.executeLine ?? void 0
|
|
2909
|
+
});
|
|
2910
|
+
}
|
|
2911
|
+
const loadsAndExports = shape.jobs.includes("load") && shape.jobs.includes("export") && shape.jobs.length > 2;
|
|
2912
|
+
if (shape.jobs.length >= MIXED_JOB_COUNT || loadsAndExports) {
|
|
2913
|
+
out.push({
|
|
2914
|
+
phase: "shape",
|
|
2915
|
+
code: "shape/mixed-jobs",
|
|
2916
|
+
message: `${where}: one node does ${shape.jobs.join(", ")} (judged from the calls it makes). Give each job its own node.`,
|
|
2917
|
+
file: fileName
|
|
2918
|
+
});
|
|
2919
|
+
}
|
|
2920
|
+
return out;
|
|
2921
|
+
}
|
|
2922
|
+
function reportNodeShape(warnings) {
|
|
2923
|
+
if (!warnings.length) return;
|
|
2924
|
+
console.warn([
|
|
2925
|
+
`Node shape \u2014 ${warnings.length === 1 ? "one warning" : `${warnings.length} warnings`} (advice only, the check still passes):`,
|
|
2926
|
+
...warnings.map((item) => ` ${item.code}: ${item.message}`),
|
|
2927
|
+
' See "Design the graph" in doc/PROJECT_AUTHORING.md.'
|
|
2928
|
+
].join("\n"));
|
|
2929
|
+
}
|
|
2930
|
+
|
|
2931
|
+
// server/src/runtime/stage.ts
|
|
2932
|
+
var BRIDGE_KEY = "__cascadeStageBridge";
|
|
2933
|
+
function installStageBridge(bridge) {
|
|
2934
|
+
const globals = globalThis;
|
|
2935
|
+
const previous = globals[BRIDGE_KEY];
|
|
2936
|
+
globals[BRIDGE_KEY] = bridge ?? void 0;
|
|
2937
|
+
return () => {
|
|
2938
|
+
globals[BRIDGE_KEY] = previous;
|
|
2939
|
+
};
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2942
|
+
// src/cli/projectStages.ts
|
|
2943
|
+
function stageWorkerCount(config, poolSize) {
|
|
2944
|
+
if (poolSize !== void 0) return Math.max(1, Math.floor(poolSize));
|
|
2945
|
+
const fromEnv = Number(process.env.CASCADE_STAGE_WORKERS);
|
|
2946
|
+
if (Number.isInteger(fromEnv) && fromEnv >= 1) return fromEnv;
|
|
2947
|
+
return config.workers ?? defaultStageWorkerCount();
|
|
2948
|
+
}
|
|
2949
|
+
var LANES_KEY = "__cascadeStageLanes";
|
|
2950
|
+
function installedStageLanes() {
|
|
2951
|
+
const lanes = globalThis[LANES_KEY];
|
|
2952
|
+
return typeof lanes === "number" && lanes > 1 ? lanes : 1;
|
|
2953
|
+
}
|
|
2954
|
+
function installProjectStages(project, options = {}) {
|
|
2955
|
+
let worker = null;
|
|
2956
|
+
const warmWorker = (config) => {
|
|
2957
|
+
if (options.worker === false || !config.worker) return null;
|
|
2958
|
+
const entrypoint = config.worker;
|
|
2959
|
+
const sources = stageSources(project, config);
|
|
2960
|
+
const size = stageWorkerCount(config, options.poolSize);
|
|
2961
|
+
const key = `${config.python}\0${entrypoint}\0${sources.roots.join("\0")}\0${size}`;
|
|
2962
|
+
if (worker?.key !== key) {
|
|
2963
|
+
worker?.process.shutdown();
|
|
2964
|
+
worker = {
|
|
2965
|
+
key,
|
|
2966
|
+
process: new PythonWorkerPool(size, () => new PythonWorker(project, entrypoint, config.python, process.env, {
|
|
2967
|
+
sources,
|
|
2968
|
+
// The one-shot path's bound, not Studio's two minutes: an offline
|
|
2969
|
+
// render may run a stage for several.
|
|
2970
|
+
requestTimeoutMs: 10 * 60 * 1e3,
|
|
2971
|
+
// What a stage prints is shown with --verbose only, as before.
|
|
2972
|
+
onStderr: options.onOutput ? (text) => {
|
|
2973
|
+
for (const line2 of text.split("\n")) options.onOutput(null, "stderr", line2);
|
|
2974
|
+
} : options.verbose ? (text) => {
|
|
2975
|
+
for (const line2 of text.split("\n")) console.warn(`[stage-worker] ${line2}`);
|
|
2976
|
+
} : () => {
|
|
2977
|
+
}
|
|
2978
|
+
}))
|
|
2979
|
+
};
|
|
2980
|
+
}
|
|
2981
|
+
return worker.process;
|
|
2982
|
+
};
|
|
2983
|
+
const globals = globalThis;
|
|
2984
|
+
const previousLanes = globals[LANES_KEY];
|
|
2985
|
+
try {
|
|
2986
|
+
const config = readStageConfig(project);
|
|
2987
|
+
globals[LANES_KEY] = options.worker === false || !config.worker ? 1 : stageWorkerCount(config, options.poolSize);
|
|
2988
|
+
} catch {
|
|
2989
|
+
globals[LANES_KEY] = 1;
|
|
2990
|
+
}
|
|
2991
|
+
if (options.prewarm) {
|
|
2992
|
+
try {
|
|
2993
|
+
warmWorker(readStageConfig(project))?.warm();
|
|
2994
|
+
} catch {
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2997
|
+
const dispose = installStageBridge(async (stage, args, callOptions) => {
|
|
2998
|
+
await settleProjectImages(project);
|
|
2999
|
+
const config = readStageConfig(project);
|
|
3000
|
+
const warm = warmWorker(config);
|
|
3001
|
+
if (warm) {
|
|
3002
|
+
const { result: result2 } = await callStage(project, config, stage, args, (envelope) => warm.callWithMeta(stage, args, { signal: callOptions?.signal, cascade: envelope }), { onTrace: options.onTrace });
|
|
3003
|
+
return result2;
|
|
3004
|
+
}
|
|
3005
|
+
const { result } = await callStage(project, config, stage, args, async (envelope) => ({
|
|
3006
|
+
result: await runProjectStage(project, stage, args, {
|
|
3007
|
+
// A superseded cook kills its subprocess rather than waiting for it.
|
|
3008
|
+
signal: callOptions?.signal,
|
|
3009
|
+
cascade: envelope,
|
|
3010
|
+
codeHash: envelope.code,
|
|
3011
|
+
/**
|
|
3012
|
+
* A stage can log, and until 0.8 nobody could read it: `stderr` was
|
|
3013
|
+
* consulted only on failure and `stdout` only for its last line. Behind
|
|
3014
|
+
* `--verbose`, because a stage that prints per mark would bury the
|
|
3015
|
+
* render it is part of; prefixed with the stage name, because a graph
|
|
3016
|
+
* runs several.
|
|
3017
|
+
*/
|
|
3018
|
+
onOutput: options.onOutput ? (stream, text) => {
|
|
3019
|
+
for (const line2 of text.split("\n")) options.onOutput(stage, stream, line2);
|
|
3020
|
+
} : options.verbose ? (stream, text) => {
|
|
3021
|
+
const write = stream === "stderr" ? console.warn : console.log;
|
|
3022
|
+
for (const line2 of text.split("\n")) write(`[${stage}] ${line2}`);
|
|
3023
|
+
} : void 0
|
|
3024
|
+
})
|
|
3025
|
+
}), { onTrace: options.onTrace });
|
|
3026
|
+
return result;
|
|
3027
|
+
});
|
|
3028
|
+
return () => {
|
|
3029
|
+
dispose();
|
|
3030
|
+
globals[LANES_KEY] = previousLanes;
|
|
3031
|
+
worker?.process.shutdown();
|
|
3032
|
+
worker = null;
|
|
3033
|
+
};
|
|
3034
|
+
}
|
|
3035
|
+
|
|
3036
|
+
// src/cli/runTrace.ts
|
|
3037
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3038
|
+
import fs4 from "node:fs";
|
|
3039
|
+
import path5 from "node:path";
|
|
3040
|
+
var MAX_NODE_LOGS = 500;
|
|
3041
|
+
var MAX_LOG_LINE = 2e3;
|
|
3042
|
+
var cookingNode = new AsyncLocalStorage();
|
|
3043
|
+
function traceNodeScope(nodeId, run) {
|
|
3044
|
+
return cookingNode.run(nodeId, run);
|
|
3045
|
+
}
|
|
3046
|
+
var RunTrace = class {
|
|
3047
|
+
constructor(graphFile, cascade, seed) {
|
|
3048
|
+
this.graphFile = graphFile;
|
|
3049
|
+
this.cascade = cascade;
|
|
3050
|
+
this.seed = seed;
|
|
3051
|
+
this.runs = [];
|
|
3052
|
+
this.current = null;
|
|
3053
|
+
/** Nodes of the latest run still cooking, by id. */
|
|
3054
|
+
this.cooking = /* @__PURE__ */ new Map();
|
|
3055
|
+
/** Stage calls made outside any node (a run with no graph events). */
|
|
3056
|
+
this.loose = [];
|
|
3057
|
+
this.startedAt = /* @__PURE__ */ new Date();
|
|
3058
|
+
this.runStarted = null;
|
|
3059
|
+
this.envNames = [];
|
|
3060
|
+
}
|
|
3061
|
+
/** The node a stage call or log line belongs to. */
|
|
3062
|
+
owner() {
|
|
3063
|
+
const id = cookingNode.getStore();
|
|
3064
|
+
if (id !== void 0) {
|
|
3065
|
+
const scoped = this.cooking.get(id)?.node;
|
|
3066
|
+
if (scoped) return scoped;
|
|
3067
|
+
}
|
|
3068
|
+
return this.current?.node;
|
|
3069
|
+
}
|
|
3070
|
+
/** Follow one loaded graph's events. Returns the unsubscribe. */
|
|
3071
|
+
attach(graph) {
|
|
3072
|
+
const modules = new Map(graph.inspect().nodes.map((node) => [node.id, node.moduleId]));
|
|
3073
|
+
return graph.subscribe((event) => {
|
|
3074
|
+
if (event.type === "run:start") {
|
|
3075
|
+
this.runs.push({ runId: event.runId, frame: graph.getFrame(), nodes: [] });
|
|
3076
|
+
this.runStarted = performance.now();
|
|
3077
|
+
} else if (event.type === "run:complete") {
|
|
3078
|
+
const run = this.runs[this.runs.length - 1];
|
|
3079
|
+
if (run && this.runStarted !== null) run.ms = Math.round(performance.now() - this.runStarted);
|
|
3080
|
+
this.runStarted = null;
|
|
3081
|
+
} else if (event.type === "node:start") {
|
|
3082
|
+
const run = this.runs[this.runs.length - 1];
|
|
3083
|
+
if (!run) return;
|
|
3084
|
+
const node = { id: event.nodeId, module: modules.get(event.nodeId) ?? "", ms: 0, stages: [] };
|
|
3085
|
+
run.nodes.push(node);
|
|
3086
|
+
this.current = { node, started: performance.now() };
|
|
3087
|
+
this.cooking.set(event.nodeId, this.current);
|
|
3088
|
+
} else if (event.type === "node:complete") {
|
|
3089
|
+
const cooking = this.cooking.get(event.nodeId);
|
|
3090
|
+
if (!cooking) return;
|
|
3091
|
+
cooking.node.ms = Math.round(performance.now() - cooking.started);
|
|
3092
|
+
this.cooking.delete(event.nodeId);
|
|
3093
|
+
if (this.current === cooking) this.current = null;
|
|
3094
|
+
}
|
|
3095
|
+
});
|
|
3096
|
+
}
|
|
3097
|
+
/**
|
|
3098
|
+
* The latest run's whole frame took `frameMs`, its outputs saved: what is
|
|
3099
|
+
* left after the cook is the save. Called by the frame loop per frame and
|
|
3100
|
+
* once by a still run.
|
|
3101
|
+
*/
|
|
3102
|
+
recordSaved(frameMs) {
|
|
3103
|
+
const run = this.runs[this.runs.length - 1];
|
|
3104
|
+
if (!run) return;
|
|
3105
|
+
run.frameMs = Math.round(frameMs);
|
|
3106
|
+
if (run.ms !== void 0) run.saveMs = Math.max(0, run.frameMs - run.ms);
|
|
3107
|
+
}
|
|
3108
|
+
/** Record a stage call against the node cooking now. */
|
|
3109
|
+
recordStage(trace) {
|
|
3110
|
+
const node = this.owner();
|
|
3111
|
+
if (node) node.stages.push(trace);
|
|
3112
|
+
else this.loose.push(trace);
|
|
3113
|
+
}
|
|
3114
|
+
/**
|
|
3115
|
+
* Record a line printed while a node cooks, against that node. Returns
|
|
3116
|
+
* false when no node is cooking, so the caller leaves the line alone (the
|
|
3117
|
+
* CLI's own messages are not node output).
|
|
3118
|
+
*/
|
|
3119
|
+
recordLog(level, text) {
|
|
3120
|
+
const node = this.owner();
|
|
3121
|
+
if (!node) return false;
|
|
3122
|
+
for (const line2 of text.split("\n")) {
|
|
3123
|
+
if (!line2.trim()) continue;
|
|
3124
|
+
node.logs ??= [];
|
|
3125
|
+
if (node.logs.length >= MAX_NODE_LOGS) {
|
|
3126
|
+
node.logsDropped = (node.logsDropped ?? 0) + 1;
|
|
3127
|
+
continue;
|
|
3128
|
+
}
|
|
3129
|
+
node.logs.push({ level, text: line2.length > MAX_LOG_LINE ? `${line2.slice(0, MAX_LOG_LINE)}\u2026` : line2 });
|
|
3130
|
+
}
|
|
3131
|
+
return true;
|
|
3132
|
+
}
|
|
3133
|
+
/** Every logged line in order, with its node. */
|
|
3134
|
+
logs() {
|
|
3135
|
+
return this.runs.flatMap((run) => run.nodes.flatMap((node) => (node.logs ?? []).map((line2) => ({ node: node.id, ...line2 }))));
|
|
3136
|
+
}
|
|
3137
|
+
/** The names (never the values) of variables `--env` set, for the trace. */
|
|
3138
|
+
setEnvNames(names) {
|
|
3139
|
+
this.envNames = [...names];
|
|
3140
|
+
}
|
|
3141
|
+
/** Every stage call in order, with its node. */
|
|
3142
|
+
stages() {
|
|
3143
|
+
const list = [];
|
|
3144
|
+
for (const run of this.runs) {
|
|
3145
|
+
for (const node of run.nodes) for (const stage of node.stages) list.push({ node: node.id, ...stage });
|
|
3146
|
+
}
|
|
3147
|
+
for (const stage of this.loose) list.push({ node: null, ...stage });
|
|
3148
|
+
return list;
|
|
3149
|
+
}
|
|
3150
|
+
toJSON() {
|
|
3151
|
+
return {
|
|
3152
|
+
format: 1,
|
|
3153
|
+
graph: this.graphFile,
|
|
3154
|
+
startedAt: this.startedAt.toISOString(),
|
|
3155
|
+
cascade: this.cascade,
|
|
3156
|
+
seed: this.seed,
|
|
3157
|
+
...this.envNames.length ? { env: this.envNames } : {},
|
|
3158
|
+
runs: this.runs
|
|
3159
|
+
};
|
|
3160
|
+
}
|
|
3161
|
+
/** Write the trace into the project and return its project-relative path. */
|
|
3162
|
+
write(projectRoot) {
|
|
3163
|
+
const stamp = this.startedAt.toISOString().replace(/[:.]/g, "-");
|
|
3164
|
+
const name = `${stamp}-${path5.basename(this.graphFile, ".cascade")}.json`;
|
|
3165
|
+
const relative = path5.posix.join(".cascade", "runs", name);
|
|
3166
|
+
const full = path5.join(projectRoot, relative);
|
|
3167
|
+
fs4.mkdirSync(path5.dirname(full), { recursive: true });
|
|
3168
|
+
fs4.writeFileSync(full, JSON.stringify(this.toJSON(), null, 2) + "\n");
|
|
3169
|
+
pruneTraces(path5.dirname(full));
|
|
3170
|
+
return relative;
|
|
3171
|
+
}
|
|
3172
|
+
};
|
|
3173
|
+
var CONSOLE_LEVELS = ["log", "info", "warn", "error", "debug"];
|
|
3174
|
+
function captureNodeConsole(trace) {
|
|
3175
|
+
const original = Object.fromEntries(CONSOLE_LEVELS.map((level) => [level, console[level]]));
|
|
3176
|
+
for (const level of CONSOLE_LEVELS) {
|
|
3177
|
+
console[level] = (...args) => {
|
|
3178
|
+
trace.recordLog(level, args.map((arg) => typeof arg === "string" ? arg : arg instanceof Error ? arg.stack ?? arg.message : safeJson(arg)).join(" "));
|
|
3179
|
+
original[level](...args);
|
|
3180
|
+
};
|
|
3181
|
+
}
|
|
3182
|
+
return () => {
|
|
3183
|
+
for (const level of CONSOLE_LEVELS) console[level] = original[level];
|
|
3184
|
+
};
|
|
3185
|
+
}
|
|
3186
|
+
function safeJson(value) {
|
|
3187
|
+
try {
|
|
3188
|
+
return JSON.stringify(value) ?? String(value);
|
|
3189
|
+
} catch {
|
|
3190
|
+
return String(value);
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
3193
|
+
var KEPT_TRACES = 200;
|
|
3194
|
+
function pruneTraces(directory) {
|
|
3195
|
+
const traces = fs4.readdirSync(directory).filter((name) => name.endsWith(".json")).sort();
|
|
3196
|
+
for (const name of traces.slice(0, Math.max(0, traces.length - KEPT_TRACES))) {
|
|
3197
|
+
fs4.rmSync(path5.join(directory, name), { force: true });
|
|
3198
|
+
}
|
|
3199
|
+
}
|
|
3200
|
+
|
|
2400
3201
|
// src/cli/projectRuntime.ts
|
|
3202
|
+
function headlessExecution() {
|
|
3203
|
+
return { values: "trust", concurrency: installedStageLanes(), nodeScope: traceNodeScope };
|
|
3204
|
+
}
|
|
2401
3205
|
function createProjectHost(file, registrations) {
|
|
2402
|
-
const project = new ProjectRoot(
|
|
3206
|
+
const project = new ProjectRoot(path6.dirname(path6.resolve(file)));
|
|
2403
3207
|
return createNodeRuntimeHost({
|
|
2404
3208
|
modules: resolver(registrations),
|
|
2405
3209
|
shell: project.shell,
|
|
@@ -2524,6 +3328,7 @@ async function checkProjectGraph(file, document2, strict = false) {
|
|
|
2524
3328
|
throw new Error(`Static check requires definition-v1 modules; dynamic modules: ${dynamic.join(", ")}`);
|
|
2525
3329
|
}
|
|
2526
3330
|
await preflightAgainstRunHost(file, prepared.registrations, document2);
|
|
3331
|
+
reportNodeShape(prepared.shape);
|
|
2527
3332
|
return unresolved;
|
|
2528
3333
|
}
|
|
2529
3334
|
async function runDeterministicProjectGraph(file, document2, entryNode, observe, afterRun) {
|
|
@@ -2533,7 +3338,10 @@ async function runDeterministicProjectGraph(file, document2, entryNode, observe,
|
|
|
2533
3338
|
const host = createProjectHost(file, prepared.registrations);
|
|
2534
3339
|
const runtime = createRuntime({
|
|
2535
3340
|
host,
|
|
2536
|
-
nodes: prepared.registrations
|
|
3341
|
+
nodes: prepared.registrations,
|
|
3342
|
+
// A run trusts what its nodes hand over instead of walking it to freeze
|
|
3343
|
+
// (Studio and `cook` keep `"freeze"`), and overlaps independent stages.
|
|
3344
|
+
...headlessExecution()
|
|
2537
3345
|
});
|
|
2538
3346
|
let graph;
|
|
2539
3347
|
try {
|
|
@@ -2547,6 +3355,7 @@ async function runDeterministicProjectGraph(file, document2, entryNode, observe,
|
|
|
2547
3355
|
if (result.status !== "completed") {
|
|
2548
3356
|
throw new Error(result.diagnostics.map((item) => item.message).join("\n") || `Graph ${result.status}`);
|
|
2549
3357
|
}
|
|
3358
|
+
await settleProjectImages({ root: path6.dirname(path6.resolve(file)) });
|
|
2550
3359
|
await afterRun?.(graph);
|
|
2551
3360
|
return true;
|
|
2552
3361
|
} finally {
|
|
@@ -2570,6 +3379,7 @@ async function cookProjectGraph(file, document2, target, observe) {
|
|
|
2570
3379
|
const result = await graph.run(target ? { target: { kind: "node", nodeId: target } } : {});
|
|
2571
3380
|
if (result.status !== "completed")
|
|
2572
3381
|
throw new Error(result.diagnostics.map((item) => item.message).join("\n") || `Graph ${result.status}`);
|
|
3382
|
+
await settleProjectImages({ root: path6.dirname(path6.resolve(file)) });
|
|
2573
3383
|
const outputs = /* @__PURE__ */ new Map();
|
|
2574
3384
|
for (const node of graph.inspect().nodes) outputs.set(node.id, new Map(graph.getOutputs(node.id)));
|
|
2575
3385
|
return { outputs, assets: host.capabilities?.assets };
|
|
@@ -2585,11 +3395,15 @@ async function renderDeterministicProjectFrames(file, document2, options) {
|
|
|
2585
3395
|
const prepared = await prepare(file, document2, true);
|
|
2586
3396
|
reportUnresolved(prepared);
|
|
2587
3397
|
if (!usesDeterministicHost(document2, prepared)) return null;
|
|
2588
|
-
const project = new ProjectRoot(
|
|
3398
|
+
const project = new ProjectRoot(path6.dirname(path6.resolve(file)));
|
|
2589
3399
|
const host = createProjectHost(file, prepared.registrations);
|
|
2590
3400
|
const runtime = createRuntime({
|
|
2591
3401
|
host,
|
|
2592
|
-
nodes: prepared.registrations
|
|
3402
|
+
nodes: prepared.registrations,
|
|
3403
|
+
// Every frame hands over fresh values; walking them to freeze was most
|
|
3404
|
+
// of a big-array frame. Independent stages (the morph's four renders)
|
|
3405
|
+
// cook side by side on the stage pool.
|
|
3406
|
+
...headlessExecution()
|
|
2593
3407
|
});
|
|
2594
3408
|
let graph;
|
|
2595
3409
|
const width = sequenceWidth(options.end);
|
|
@@ -2605,9 +3419,10 @@ async function renderDeterministicProjectFrames(file, document2, options) {
|
|
|
2605
3419
|
const targets = options.nodes?.length ? options.nodes : options.entryNode ? [options.entryNode] : [];
|
|
2606
3420
|
const extra = options.outNodes ?? [];
|
|
2607
3421
|
const out = outDirectory(options.out, (relative) => project.resolve(relative));
|
|
2608
|
-
await
|
|
3422
|
+
await fs5.mkdir(out.directory, { recursive: true });
|
|
2609
3423
|
const missing = unknownNodes(graph, [...targets, ...extra]);
|
|
2610
3424
|
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).`);
|
|
3425
|
+
const imageOutputs = planImageOutputs(graph, { nodes: targets, extra });
|
|
2611
3426
|
for await (const frame of options.frameSource ?? frameRange(options.start, options.end, options.step ?? 1)) {
|
|
2612
3427
|
const frameStarted = performance.now();
|
|
2613
3428
|
if (options.signal?.aborted) {
|
|
@@ -2639,7 +3454,7 @@ async function renderDeterministicProjectFrames(file, document2, options) {
|
|
|
2639
3454
|
`frame ${frame} ${result.status} \u2014 ${result.diagnostics.map((item) => item.message).join("; ") || "no diagnostics"}`
|
|
2640
3455
|
);
|
|
2641
3456
|
}
|
|
2642
|
-
const outputs =
|
|
3457
|
+
const outputs = imageOutputs.collect();
|
|
2643
3458
|
if (!outputs.length) {
|
|
2644
3459
|
throw new Error(
|
|
2645
3460
|
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 +3466,7 @@ async function renderDeterministicProjectFrames(file, document2, options) {
|
|
|
2651
3466
|
if (!source) continue;
|
|
2652
3467
|
const name = sequenceFileName(output.nodeId, frame, source, width, output.shared ? output.port : void 0);
|
|
2653
3468
|
const relative = source.replace(/^\.?\//, "");
|
|
2654
|
-
await
|
|
3469
|
+
await copyImageFile(project, relative, path6.join(out.directory, name));
|
|
2655
3470
|
files.push(out.spell(name));
|
|
2656
3471
|
written.push({ nodeId: output.nodeId, port: output.port, source: relative, file: out.spell(name) });
|
|
2657
3472
|
}
|
|
@@ -2659,6 +3474,7 @@ async function renderDeterministicProjectFrames(file, document2, options) {
|
|
|
2659
3474
|
await options.onFrame?.({ frame, outputs: written, durationMs: performance.now() - frameStarted });
|
|
2660
3475
|
if (options.verbose) console.log(` frame ${frame}`);
|
|
2661
3476
|
}
|
|
3477
|
+
await settleProjectImages(project);
|
|
2662
3478
|
try {
|
|
2663
3479
|
gpu = { renderer: "dawn", adapter: host.capabilities.gpu.adapterInfo };
|
|
2664
3480
|
} catch {
|
|
@@ -2691,7 +3507,7 @@ async function inspectProjectGraph(file, document2) {
|
|
|
2691
3507
|
};
|
|
2692
3508
|
});
|
|
2693
3509
|
return {
|
|
2694
|
-
file:
|
|
3510
|
+
file: path6.resolve(file),
|
|
2695
3511
|
version: document2.version ?? null,
|
|
2696
3512
|
deterministic: nodes.length > 0 && nodes.every((node) => node.classification === "definition-v1"),
|
|
2697
3513
|
nodes,
|
|
@@ -2702,11 +3518,12 @@ async function prepare(file, document2, compileExecutors) {
|
|
|
2702
3518
|
if (!document2 || typeof document2 !== "object" || !Array.isArray(document2.nodes)) {
|
|
2703
3519
|
throw new Error("Graph document must contain a nodes array");
|
|
2704
3520
|
}
|
|
2705
|
-
const projectRoot =
|
|
3521
|
+
const projectRoot = path6.dirname(path6.resolve(file));
|
|
2706
3522
|
const registrations = [];
|
|
2707
3523
|
const deterministic = /* @__PURE__ */ new Set();
|
|
2708
3524
|
const seen = /* @__PURE__ */ new Set();
|
|
2709
3525
|
const unresolved = [];
|
|
3526
|
+
const shape = [];
|
|
2710
3527
|
for (const node of document2.nodes) {
|
|
2711
3528
|
const id = moduleId(node);
|
|
2712
3529
|
if (!id || seen.has(id)) continue;
|
|
@@ -2728,7 +3545,7 @@ async function prepare(file, document2, compileExecutors) {
|
|
|
2728
3545
|
registrations.push(savedRegistration(id, node));
|
|
2729
3546
|
continue;
|
|
2730
3547
|
}
|
|
2731
|
-
const source = await
|
|
3548
|
+
const source = await fs5.readFile(moduleFile, "utf8");
|
|
2732
3549
|
const mentionsDefinition = /\bexport\s+const\s+definition\b/.test(source);
|
|
2733
3550
|
const extracted = extractNodeDefinition(source, moduleFile, ts);
|
|
2734
3551
|
if (!extracted.ok) {
|
|
@@ -2744,24 +3561,34 @@ async function prepare(file, document2, compileExecutors) {
|
|
|
2744
3561
|
if (architectureDiagnostics.length) {
|
|
2745
3562
|
throw new Error(architectureDiagnostics.map(formatDiagnostic).join("\n"));
|
|
2746
3563
|
}
|
|
3564
|
+
shape.push(...nodeShapeWarnings(source, moduleFile, ts, id));
|
|
2747
3565
|
let executor;
|
|
2748
3566
|
deterministic.add(id);
|
|
2749
3567
|
registrations.push({
|
|
2750
3568
|
kind: "definition-v1",
|
|
2751
3569
|
moduleId: id,
|
|
2752
3570
|
definition: extracted.definition,
|
|
2753
|
-
loadExecute: compileExecutors ? () => executor ??= compileExecute(projectRoot, moduleFile)
|
|
3571
|
+
loadExecute: compileExecutors ? () => executor ??= compileExecute(projectRoot, moduleFile).then((execute) => (
|
|
3572
|
+
// A server node may read an image input with `node:fs`, the one
|
|
3573
|
+
// reader the host cannot make wait, so it starts once every image
|
|
3574
|
+
// written behind is on disk. Portable and browser nodes cannot
|
|
3575
|
+
// reach the filesystem and keep the in-memory handoff.
|
|
3576
|
+
extracted.definition.runsOn === "server" ? async (...args) => {
|
|
3577
|
+
await settleProjectImages({ root: projectRoot });
|
|
3578
|
+
return execute(...args);
|
|
3579
|
+
} : execute
|
|
3580
|
+
)) : async () => {
|
|
2754
3581
|
throw new Error("Static validation never loads execute");
|
|
2755
3582
|
}
|
|
2756
3583
|
});
|
|
2757
3584
|
}
|
|
2758
|
-
return { registrations, deterministic, unresolved };
|
|
3585
|
+
return { registrations, deterministic, unresolved, shape };
|
|
2759
3586
|
}
|
|
2760
3587
|
async function projectModuleFile(root, node, id) {
|
|
2761
3588
|
const tried = [];
|
|
2762
3589
|
const source = node.source;
|
|
2763
3590
|
if (source && typeof source === "object" && source.type === "project" && typeof source.file === "string") {
|
|
2764
|
-
const candidate2 =
|
|
3591
|
+
const candidate2 = path6.resolve(root, source.file);
|
|
2765
3592
|
if (within(root, candidate2)) {
|
|
2766
3593
|
tried.push(candidate2);
|
|
2767
3594
|
if (await exists(candidate2)) return { file: candidate2, tried };
|
|
@@ -2769,7 +3596,7 @@ async function projectModuleFile(root, node, id) {
|
|
|
2769
3596
|
}
|
|
2770
3597
|
if (!id.startsWith("project.")) return { file: null, tried };
|
|
2771
3598
|
const name = id.slice("project.".length);
|
|
2772
|
-
const candidate =
|
|
3599
|
+
const candidate = path6.resolve(root, "nodes", name, "index.ts");
|
|
2773
3600
|
if (!within(root, candidate)) return { file: null, tried };
|
|
2774
3601
|
tried.push(candidate);
|
|
2775
3602
|
return { file: await exists(candidate) ? candidate : null, tried };
|
|
@@ -2787,7 +3614,7 @@ function reExportedDefinition(source) {
|
|
|
2787
3614
|
});
|
|
2788
3615
|
}
|
|
2789
3616
|
function unresolvedModule(id, node, tried, root) {
|
|
2790
|
-
const relative = tried.map((candidate) =>
|
|
3617
|
+
const relative = tried.map((candidate) => path6.relative(root, candidate) || candidate);
|
|
2791
3618
|
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
3619
|
return {
|
|
2793
3620
|
phase: "prepare",
|
|
@@ -2797,9 +3624,9 @@ function unresolvedModule(id, node, tried, root) {
|
|
|
2797
3624
|
};
|
|
2798
3625
|
}
|
|
2799
3626
|
async function compileExecute(projectRoot, moduleFile) {
|
|
2800
|
-
const cache =
|
|
2801
|
-
await
|
|
2802
|
-
const output =
|
|
3627
|
+
const cache = path6.join(projectRoot, ".cascade-cache", "nodes");
|
|
3628
|
+
await fs5.mkdir(cache, { recursive: true });
|
|
3629
|
+
const output = path6.join(cache, `${path6.basename(path6.dirname(moduleFile))}.mjs`);
|
|
2803
3630
|
const staging = `${output}.${process.pid}.${Date.now()}.tmp`;
|
|
2804
3631
|
await build({
|
|
2805
3632
|
entryPoints: [moduleFile],
|
|
@@ -2812,24 +3639,27 @@ async function compileExecute(projectRoot, moduleFile) {
|
|
|
2812
3639
|
sourcemap: "inline",
|
|
2813
3640
|
plugins: [cliImageModule()]
|
|
2814
3641
|
});
|
|
2815
|
-
await
|
|
3642
|
+
await fs5.rename(staging, output);
|
|
2816
3643
|
const loaded = await import(`${pathToFileURL2(output).href}?v=${Date.now()}`);
|
|
2817
3644
|
if (typeof loaded.execute !== "function") throw new Error(`${moduleFile} does not export execute`);
|
|
2818
3645
|
return loaded.execute;
|
|
2819
3646
|
}
|
|
2820
3647
|
function cliImageModule() {
|
|
2821
|
-
const here =
|
|
2822
|
-
const
|
|
2823
|
-
|
|
3648
|
+
const here = path6.dirname(fileURLToPath(import.meta.url));
|
|
3649
|
+
const shim = (name) => [
|
|
3650
|
+
path6.join(here, "runtime", `${name}.ts`),
|
|
2824
3651
|
// dist/cli/runtime, copied by build-cli
|
|
2825
|
-
|
|
3652
|
+
path6.join(here, "..", "..", "server", "src", "runtime", `${name}.ts`)
|
|
2826
3653
|
// running from src
|
|
2827
|
-
].find((candidate) =>
|
|
3654
|
+
].find((candidate) => existsSync2(candidate));
|
|
3655
|
+
const sources = { image: shim("image"), io: shim("io") };
|
|
2828
3656
|
return {
|
|
2829
3657
|
name: "cascade-cli-image",
|
|
2830
3658
|
setup(builder) {
|
|
2831
|
-
|
|
2832
|
-
|
|
3659
|
+
builder.onResolve({ filter: /^(?:@field\/)?cascade\/(?:image|io)$/ }, (args) => {
|
|
3660
|
+
const source = sources[args.path.endsWith("/io") ? "io" : "image"];
|
|
3661
|
+
return source ? { path: source } : void 0;
|
|
3662
|
+
});
|
|
2833
3663
|
}
|
|
2834
3664
|
};
|
|
2835
3665
|
}
|
|
@@ -2919,12 +3749,12 @@ function formatDiagnostic(diagnostic) {
|
|
|
2919
3749
|
return `${location}${diagnostic.code}: ${diagnostic.message}`;
|
|
2920
3750
|
}
|
|
2921
3751
|
function within(root, candidate) {
|
|
2922
|
-
const relative =
|
|
2923
|
-
return relative === "" || !relative.startsWith("..") && !
|
|
3752
|
+
const relative = path6.relative(path6.resolve(root), candidate);
|
|
3753
|
+
return relative === "" || !relative.startsWith("..") && !path6.isAbsolute(relative);
|
|
2924
3754
|
}
|
|
2925
3755
|
async function exists(file) {
|
|
2926
3756
|
try {
|
|
2927
|
-
await
|
|
3757
|
+
await fs5.access(file);
|
|
2928
3758
|
return true;
|
|
2929
3759
|
} catch {
|
|
2930
3760
|
return false;
|
|
@@ -2989,6 +3819,10 @@ export {
|
|
|
2989
3819
|
validateNodeModuleArchitecture,
|
|
2990
3820
|
installProjectIo,
|
|
2991
3821
|
saveStillImages,
|
|
3822
|
+
installProjectStages,
|
|
3823
|
+
cookingNode,
|
|
3824
|
+
RunTrace,
|
|
3825
|
+
captureNodeConsole,
|
|
2992
3826
|
createProjectHost,
|
|
2993
3827
|
classifyPreflight,
|
|
2994
3828
|
validateProjectGraph,
|
|
@@ -3003,4 +3837,4 @@ export {
|
|
|
3003
3837
|
builtinModuleIds,
|
|
3004
3838
|
diagnoseGraph
|
|
3005
3839
|
};
|
|
3006
|
-
//# sourceMappingURL=chunk-
|
|
3840
|
+
//# sourceMappingURL=chunk-QKL7CCRS.js.map
|