@field/cascade 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. package/AGENTS.md +25 -5
  2. package/ARCHITECTURE.md +5 -4
  3. package/CHANGELOG.md +382 -0
  4. package/README.md +1 -2
  5. package/dist/assets/AgentPanel-1JugFjE4.css +1 -0
  6. package/dist/assets/AgentPanel-BDL5MddA.js +5 -0
  7. package/dist/assets/{CodeEditor-DSZBpzdy.js → CodeEditor-C-xYG-Wn.js} +120 -120
  8. package/dist/assets/CodePanel-CtXd50m6.js +2 -0
  9. package/dist/assets/CookInfoPanel-B3NMhKJF.css +1 -0
  10. package/dist/assets/CookInfoPanel-DG4_TRsX.js +2 -0
  11. package/dist/assets/CustomNodeDialog-CHfZUs-r.js +1 -0
  12. package/dist/assets/DefinitionNode-CaRTXQ_b.js +1 -0
  13. package/dist/assets/DefinitionPanel-C3CxZY9r.js +1 -0
  14. package/dist/assets/DefinitionPanel-VNLnAiQD.css +1 -0
  15. package/dist/assets/OpenGraphDialog-BLdr6k_q.js +1 -0
  16. package/dist/assets/ProjectSettings-ZnvmIAir.js +2 -0
  17. package/dist/assets/{SettingsDialog-p25h3tj0.js → SettingsDialog-CBX3Wa87.js} +1 -1
  18. package/dist/assets/TimelinePanel-CDeR0arY.css +1 -0
  19. package/dist/assets/TimelinePanel-CWgnFH1e.js +1 -0
  20. package/dist/assets/{VersionHistoryDialog-DzFLUotB.js → VersionHistoryDialog-BOSoW1R-.js} +1 -1
  21. package/dist/assets/assert-BO2_erPD.js +1 -0
  22. package/dist/assets/assert-hvykGCnP.js +1 -0
  23. package/dist/assets/builder-BmBKcDix.js +1 -0
  24. package/dist/assets/camera-DFF7YA3F.js +1 -0
  25. package/dist/assets/checks-DAvyO9Gf.js +1 -0
  26. package/dist/assets/contactSheet-N1NcaWxw.js +2 -0
  27. package/dist/assets/{core-BICrEt0y.js → core-DB-MkGjj.js} +3 -4
  28. package/dist/assets/coreTypes-DtnDATZ7.js +1 -0
  29. package/dist/assets/{cssMode-D7TXw57I.js → cssMode-DPVckSOc.js} +1 -1
  30. package/dist/assets/definitions-B227e7f_.js +1 -0
  31. package/dist/assets/definitions-D0WOUJd7.js +1 -0
  32. package/dist/assets/{editor.api-DvkCO3ko.js → editor.api-Dkutg_Z1.js} +1 -1
  33. package/dist/assets/expressions-X-Z-GrgW.js +5 -0
  34. package/dist/assets/file-DAL6IfjV.js +1 -0
  35. package/dist/assets/filters-D3k1LRMa.js +1 -0
  36. package/dist/assets/{freemarker2-BA2lxsF1.js → freemarker2-CLiLKk3B.js} +1 -1
  37. package/dist/assets/generators-BhDxRAFC.js +1 -0
  38. package/dist/assets/geo-BsMImJIJ.js +1 -0
  39. package/dist/assets/geo-C4YngDob.js +2 -0
  40. package/dist/assets/{handlebars-BcDiyPd8.js → handlebars-CcD3aMeL.js} +1 -1
  41. package/dist/assets/{html-C7O4TyhX.js → html-DvZWGfqb.js} +1 -1
  42. package/dist/assets/{htmlMode-WaDGF3Mj.js → htmlMode-DUD3VaoN.js} +1 -1
  43. package/dist/assets/image-0SfH-snh.js +1 -0
  44. package/dist/assets/image-Ck-RdGU4.js +2 -0
  45. package/dist/assets/index-BWnrmIB9.js +137 -0
  46. package/dist/assets/index-DIaxEONI.css +1 -0
  47. package/dist/assets/io-BGmt2g0D.js +1 -0
  48. package/dist/assets/{javascript-CpIEK7tz.js → javascript-Cap2ahgh.js} +1 -1
  49. package/dist/assets/{jsonMode-B2FltW0f.js → jsonMode-B0uLjp9C.js} +1 -1
  50. package/dist/assets/{liquid-CN6FFHZE.js → liquid-CnxwhfLn.js} +1 -1
  51. package/dist/assets/{lspLanguageFeatures-QhV9XtLp.js → lspLanguageFeatures-Dn9hlKX6.js} +1 -1
  52. package/dist/assets/math-GeULS-sk.js +1 -0
  53. package/dist/assets/{mdx-DE89pafV.js → mdx-BV3jbrVa.js} +1 -1
  54. package/dist/assets/pop-KMF_qloi.js +1 -0
  55. package/dist/assets/preload-helper-Czpn1I53.js +1 -0
  56. package/dist/assets/{python-ByTllTB9.js → python-CGyFk4XK.js} +1 -1
  57. package/dist/assets/{razor-USYlRRCE.js → razor-CQfzX3-N.js} +1 -1
  58. package/dist/assets/remap-5SjXQU-q.js +1 -0
  59. package/dist/assets/renderMarkdown-ork0Oir7.js +67 -0
  60. package/dist/assets/studioImages-CAyH-0Pl.js +1 -0
  61. package/dist/assets/text-ZpX7qNkx.js +2 -0
  62. package/dist/assets/time-P_OiBhcf.js +1 -0
  63. package/dist/assets/{tsMode-BXpN8iUz.js → tsMode-BOeXzO9H.js} +1 -1
  64. package/dist/assets/{typescript-DInBjGQQ.js → typescript-J1SnE_Tu.js} +1 -1
  65. package/dist/assets/{workers-C7Ye5B7D.js → workers-CYwwTiVF.js} +1 -1
  66. package/dist/assets/{xml-U0Uocqyo.js → xml-DnPRNzK9.js} +1 -1
  67. package/dist/assets/{yaml-CBw_PVYf.js → yaml-BmmYBNa9.js} +1 -1
  68. package/dist/cli/DefinitionNode-P7AA4TAU.js +15 -0
  69. package/dist/cli/Graph-OB6EWSGL.js +2975 -0
  70. package/dist/cli/assert-GROGQNCQ.js +25 -0
  71. package/dist/cli/checks-3WB44XBL.js +189 -0
  72. package/dist/cli/chunk-2JEU3MGB.js +1125 -0
  73. package/dist/cli/chunk-2KXB47FP.js +115 -0
  74. package/dist/cli/chunk-3RXL5GHB.js +816 -0
  75. package/dist/cli/chunk-53LGFPWX.js +138 -0
  76. package/dist/cli/chunk-5ORUQ6ES.js +267 -0
  77. package/dist/cli/chunk-73S4JZXK.js +336 -0
  78. package/dist/cli/chunk-7QE2ZNSN.js +121 -0
  79. package/dist/cli/chunk-BP7UKF36.js +376 -0
  80. package/dist/cli/chunk-BPODRH52.js +201 -0
  81. package/dist/cli/chunk-BZ63XQ3I.js +1301 -0
  82. package/dist/cli/chunk-CKS2FQWR.js +89 -0
  83. package/dist/cli/chunk-CO22XHNG.js +593 -0
  84. package/dist/cli/chunk-CTPLRSWF.js +2197 -0
  85. package/dist/cli/chunk-DEQSEFFH.js +153 -0
  86. package/dist/cli/chunk-EU4ZFZ7Y.js +122 -0
  87. package/dist/cli/chunk-EZRWJ4G2.js +104 -0
  88. package/dist/cli/chunk-FWMSGKEH.js +71 -0
  89. package/dist/cli/chunk-G23A2BZZ.js +705 -0
  90. package/dist/cli/chunk-GRIN2DDP.js +1465 -0
  91. package/dist/cli/chunk-HFHWYL55.js +139 -0
  92. package/dist/cli/chunk-HSNOBIVT.js +263 -0
  93. package/dist/cli/chunk-ICH7N5PN.js +26 -0
  94. package/dist/cli/chunk-J7VK7MNF.js +418 -0
  95. package/dist/cli/chunk-JVOCK5W4.js +83 -0
  96. package/dist/cli/chunk-KBGUO27Q.js +15 -0
  97. package/dist/cli/chunk-KH4MUBKF.js +1551 -0
  98. package/dist/cli/chunk-L6B6FUQP.js +330 -0
  99. package/dist/cli/chunk-LTXWBXOM.js +438 -0
  100. package/dist/cli/chunk-MI5JLQYA.js +220 -0
  101. package/dist/cli/chunk-MP653A5O.js +3766 -0
  102. package/dist/cli/chunk-NJFMDXHR.js +320 -0
  103. package/dist/cli/chunk-OM6BEME6.js +267 -0
  104. package/dist/cli/chunk-OWWBYLHC.js +136 -0
  105. package/dist/cli/chunk-QQPA2ZHH.js +273 -0
  106. package/dist/cli/chunk-RUU3YWXU.js +99 -0
  107. package/dist/cli/chunk-RXDIMXN5.js +98 -0
  108. package/dist/cli/chunk-ST6LROUG.js +267 -0
  109. package/dist/cli/chunk-TJW67QSI.js +225 -0
  110. package/dist/cli/chunk-WD62JIP6.js +118 -0
  111. package/dist/cli/chunk-WGKPJZDY.js +364 -0
  112. package/dist/cli/chunk-X4WKAOQP.js +634 -0
  113. package/dist/cli/chunk-X566FWBV.js +156 -0
  114. package/dist/cli/chunk-YH637PDL.js +317 -0
  115. package/dist/cli/chunk-ZL4JTG5Y.js +37 -0
  116. package/dist/cli/chunk-ZSCUZE6I.js +112 -0
  117. package/dist/cli/cliCommands-JJN5Z3ZC.js +3470 -0
  118. package/dist/cli/commands-PHYFYA3F.js +1374 -0
  119. package/dist/cli/contactSheet-2PQVRUFW.js +11 -0
  120. package/dist/cli/definitions-BPOPW52L.js +32 -0
  121. package/dist/cli/doctorCommand-ORWQRNYP.js +106 -0
  122. package/dist/cli/edit-S5KEQ5RA.js +28 -0
  123. package/dist/cli/editorCommands-NFR32JOY.js +41 -0
  124. package/dist/cli/encode-ICPEPMH5.js +17 -0
  125. package/dist/cli/file-3HTQHPYQ.js +41 -0
  126. package/dist/cli/filters-6ODFWYK4.js +379 -0
  127. package/dist/cli/frames-LSAUBZ7P.js +16 -0
  128. package/dist/cli/generators-YU7GB4MD.js +210 -0
  129. package/dist/cli/geo-6ZPGKZRU.js +43 -0
  130. package/dist/cli/graphCommands-EOUOBOIJ.js +184 -0
  131. package/dist/cli/graphFile-YTGJB5F5.js +292 -0
  132. package/dist/cli/headlessCanvas-ENRHC7GS.js +16 -0
  133. package/dist/cli/helpCommand-PULH6ZYB.js +157 -0
  134. package/dist/cli/image-TXELZZK5.js +55 -0
  135. package/dist/cli/index.js +100 -30756
  136. package/dist/cli/math-RSYRQ3C6.js +16 -0
  137. package/dist/cli/mcp-JVZFQOEX.js +164 -0
  138. package/dist/cli/parity-X6DEH2DV.js +334 -0
  139. package/dist/cli/pop-B7SIIYKN.js +39 -0
  140. package/dist/cli/project-VYGIA7JX.js +12 -0
  141. package/dist/cli/projectDefinition-F3D7UIHJ.js +27 -0
  142. package/dist/cli/projectRuntime-2Q4JKEH3.js +55 -0
  143. package/dist/cli/registerStandardNodes-BXJCB6E4.js +744 -0
  144. package/dist/cli/render-PT4MWI6U.js +438 -0
  145. package/dist/cli/runCommand-HNPQVZMS.js +198 -0
  146. package/dist/cli/runner-5XPXVW7J.js +43 -0
  147. package/dist/cli/runtime/image.ts +302 -0
  148. package/dist/cli/runtime/io.ts +78 -0
  149. package/dist/cli/runtime/stage.ts +4 -1
  150. package/dist/cli/share-MHF6TNRK.js +166 -0
  151. package/dist/cli/stageCache-TY2BFZJR.js +15 -0
  152. package/dist/cli/stageRunner-KY2EJP7X.js +10 -0
  153. package/dist/cli/text-MB3EKR4H.js +126 -0
  154. package/dist/cli/verbs-PI2JH5FI.js +19 -0
  155. package/dist/cli/webBuild-WPPMGGW7.js +44 -0
  156. package/dist/contracts/capabilities.d.ts +18 -1
  157. package/dist/contracts/color.d.ts +8 -0
  158. package/dist/contracts/color.js +23 -0
  159. package/dist/contracts/definition.d.ts +10 -0
  160. package/dist/contracts/documentFormat.d.ts +18 -0
  161. package/dist/contracts/documentFormat.js +195 -0
  162. package/dist/contracts/editor.d.ts +17 -0
  163. package/dist/contracts/index.d.ts +1 -0
  164. package/dist/contracts/index.js +1 -0
  165. package/dist/contracts/schema.d.ts +1 -1
  166. package/dist/contracts/schema.js +1 -0
  167. package/dist/contracts/validate.js +11 -1
  168. package/dist/contracts/values.d.ts +24 -0
  169. package/dist/image.d.ts +156 -0
  170. package/dist/image.js +161 -0
  171. package/dist/index.html +8 -2
  172. package/dist/io.d.ts +29 -0
  173. package/dist/io.js +40 -0
  174. package/dist/player/app.js +3 -2
  175. package/dist/player/editor.js +35 -9
  176. package/dist/player/editorEngine.js +3 -2
  177. package/dist/runtime/builtins/assert/checks.d.ts +18 -0
  178. package/dist/runtime/builtins/assert/checks.js +207 -0
  179. package/dist/runtime/builtins/assert/definitions.d.ts +130 -0
  180. package/dist/runtime/builtins/assert/definitions.js +54 -0
  181. package/dist/runtime/builtins/assert/index.d.ts +152 -0
  182. package/dist/runtime/builtins/assert/index.js +36 -0
  183. package/dist/runtime/builtins/image/contactSheet.d.ts +64 -0
  184. package/dist/runtime/builtins/image/contactSheet.js +150 -0
  185. package/dist/runtime/builtins/image/definitions.d.ts +950 -0
  186. package/dist/runtime/builtins/image/definitions.js +343 -0
  187. package/dist/runtime/builtins/image/file.d.ts +8 -0
  188. package/dist/runtime/builtins/image/file.js +37 -0
  189. package/dist/runtime/builtins/image/filters.d.ts +8 -0
  190. package/dist/runtime/builtins/image/filters.js +394 -0
  191. package/dist/runtime/builtins/image/generators.d.ts +7 -0
  192. package/dist/runtime/builtins/image/generators.js +144 -0
  193. package/dist/runtime/builtins/image/index.d.ts +952 -0
  194. package/dist/runtime/builtins/image/index.js +44 -0
  195. package/dist/runtime/builtins/image/io.d.ts +100 -0
  196. package/dist/runtime/builtins/image/io.js +142 -0
  197. package/dist/runtime/builtins/image/planar.d.ts +55 -0
  198. package/dist/runtime/builtins/image/planar.js +274 -0
  199. package/dist/runtime/builtins/image/simplex.d.ts +22 -0
  200. package/dist/runtime/builtins/image/simplex.js +92 -0
  201. package/dist/runtime/builtins/image/text.d.ts +4 -0
  202. package/dist/runtime/builtins/image/text.js +141 -0
  203. package/dist/runtime/builtins/index.d.ts +3 -0
  204. package/dist/runtime/builtins/index.js +7 -0
  205. package/dist/runtime/gpu.d.ts +25 -0
  206. package/dist/runtime/gpu.js +40 -0
  207. package/dist/runtime/runtime.d.ts +2 -0
  208. package/dist/runtime/runtime.js +268 -44
  209. package/dist/runtime/types.d.ts +30 -0
  210. package/dist/stage.js +4 -1
  211. package/doc/CLI_REFERENCE.md +91 -2
  212. package/doc/EDITOR_EXPORT.md +37 -2
  213. package/doc/MCP.md +18 -1
  214. package/doc/NODE_REFERENCE.md +424 -10
  215. package/doc/PROJECT_AUTHORING.md +222 -7
  216. package/package.json +19 -4
  217. package/dist/assets/CodePanel-sY6sZa7B.js +0 -2
  218. package/dist/assets/CustomNodeDialog-BRKnqZhp.js +0 -1
  219. package/dist/assets/ExportDialog-C7dVGua4.js +0 -358
  220. package/dist/assets/ExportDialog-DOq0flMV.css +0 -1
  221. package/dist/assets/OpenGraphDialog-ZsgL-iwr.js +0 -1
  222. package/dist/assets/ProjectSettings-Cw17nNe3.js +0 -2
  223. package/dist/assets/definitions-DUww2Rt-.js +0 -1
  224. package/dist/assets/geo-C2RHOVHC.js +0 -1
  225. package/dist/assets/image-DR4oeyJB.js +0 -2062
  226. package/dist/assets/index-7vae6YFC.css +0 -1
  227. package/dist/assets/index-DyvDC6pS.js +0 -219
  228. package/dist/assets/math-DaDEgWKt.js +0 -1
  229. package/dist/assets/pop-pLH7L0PW.js +0 -1
@@ -1,11 +1,11 @@
1
- import { bundleFieldType, bundleSchemasCompatible, bundleValueProblem, canConnectTypes, isBundleValue, selectBundleFields, suggestName, unknownFieldMessage, validateNodeDefinition, } from "@field/cascade/contracts";
1
+ import { bundleFieldType, bundleSchemasCompatible, bundleValueProblem, canConnectTypes, Color, isBundleValue, selectBundleFields, suggestName, unknownFieldMessage, validateNodeDefinition, } from "@field/cascade/contracts";
2
2
  import { CascadeRuntimeError } from "./error.js";
3
3
  import { builtinNodeRegistrations } from "./builtins/index.js";
4
4
  import { deserializeChannel, isEmptyChannel } from "./animation/channel.js";
5
5
  import { PropAnimator, isBoundProp } from "./params/index.js";
6
6
  import { bundleFieldReferences, parseBundleFieldPath, remoteBundlePath, } from "./expressions/bundlePaths.js";
7
7
  import { DEFAULT_FPS, MIN_FRAME } from "./expressions/time.js";
8
- import { isGpuHost } from "./gpu.js";
8
+ import { gpuLimitShortfall, isGpuHost } from "./gpu.js";
9
9
  import { DEFAULT_GRAPH_SEED, graphSeed, nodeSeed, seededRandom } from "./seed.js";
10
10
  /**
11
11
  * Preflight codes that describe something other than "this graph cannot run
@@ -113,16 +113,23 @@ class MutableAbortSignal {
113
113
  }
114
114
  }
115
115
  export function createRuntime(options) {
116
- return new Runtime(options.host, [...builtinNodeRegistrations, ...(options.nodes ?? [])]);
116
+ return new Runtime(options.host, [...builtinNodeRegistrations, ...(options.nodes ?? [])], {
117
+ values: options.values === "trust" ? "trust" : "freeze",
118
+ concurrency: Math.max(1, Math.floor(Number(options.concurrency ?? 1)) || 1),
119
+ ...(options.nodeScope ? { nodeScope: options.nodeScope } : {}),
120
+ });
117
121
  }
122
+ const SERIAL_EXECUTION = { values: "freeze", concurrency: 1 };
118
123
  class Runtime {
119
124
  host;
125
+ execution;
120
126
  registrations = new Map();
121
127
  graphs = new Set();
122
128
  sealed = false;
123
129
  disposed = false;
124
- constructor(host, registrations) {
130
+ constructor(host, registrations, execution = SERIAL_EXECUTION) {
125
131
  this.host = host;
132
+ this.execution = execution;
126
133
  for (const registration of registrations)
127
134
  this.registerNode(registration);
128
135
  }
@@ -166,7 +173,7 @@ class Runtime {
166
173
  const connections = (document.connections ?? []).map((connection, index) => materializeConnection(connection, index, byId));
167
174
  validateCardinality(byId, connections);
168
175
  validateTopology(nodes, connections);
169
- const graph = new Graph(this.host, nodes, connections, () => this.graphs.delete(graph), document.annotations, graphSeed(document));
176
+ const graph = new Graph(this.host, nodes, connections, () => this.graphs.delete(graph), document.annotations, graphSeed(document), this.execution);
170
177
  // The graph's own frame rate, saved with it; a run's `fps` still overrides.
171
178
  const fps = document.timeline?.fps;
172
179
  if (typeof fps === "number" && Number.isFinite(fps) && fps > 0)
@@ -188,6 +195,7 @@ class Graph {
188
195
  connections;
189
196
  onDispose;
190
197
  seed;
198
+ execution;
191
199
  graphState = "ready";
192
200
  byId;
193
201
  dataConnectionsByInput;
@@ -213,14 +221,20 @@ class Graph {
213
221
  animator;
214
222
  frame = MIN_FRAME;
215
223
  fps = DEFAULT_FPS;
224
+ /** What happens to a value a node hands over; see `RuntimeValuePolicy`. */
225
+ values;
216
226
  constructor(host, nodes, connections, onDispose, annotations = [],
217
227
  /** The document's seed; each node's `context.seed` derives from it. */
218
- seed = DEFAULT_GRAPH_SEED) {
228
+ seed = DEFAULT_GRAPH_SEED,
229
+ /** Value policy, concurrency, node scope; see `CreateRuntimeOptions`. */
230
+ execution = SERIAL_EXECUTION) {
219
231
  this.host = host;
220
232
  this.nodes = nodes;
221
233
  this.connections = connections;
222
234
  this.onDispose = onDispose;
223
235
  this.seed = seed;
236
+ this.execution = execution;
237
+ this.values = execution.values;
224
238
  this.byId = new Map(nodes.map((node) => [node.id, node]));
225
239
  const byInput = new Map();
226
240
  const byNode = new Map();
@@ -316,7 +330,7 @@ class Graph {
316
330
  type: definition.kind === "trigger" ? "trigger" : definition.type,
317
331
  connected: this.connectedOutputs.has(`${node.id}\0${name}`),
318
332
  ...(definition.kind === "data" && node.outputs.has(name)
319
- ? { value: snapshot(node.outputs.get(name)) }
333
+ ? { value: this.expose(node.outputs.get(name)) }
320
334
  : {}),
321
335
  },
322
336
  ])),
@@ -479,7 +493,15 @@ class Graph {
479
493
  return () => this.listeners.delete(listener);
480
494
  }
481
495
  getOutput(nodeId, outputName) {
482
- return snapshot(this.requireNode(nodeId).outputs.get(outputName));
496
+ return this.expose(this.requireNode(nodeId).outputs.get(outputName));
497
+ }
498
+ /**
499
+ * An output value as callers see it. Under `"freeze"` every output was
500
+ * deep-frozen when it was set, so this is the value itself (the snapshot is
501
+ * a lookup); under `"trust"` it is handed out as the producer made it.
502
+ */
503
+ expose(value) {
504
+ return this.values === "trust" ? value : snapshot(value);
483
505
  }
484
506
  getGraphOutput(name) {
485
507
  const node = this.graphOutputs.get(name);
@@ -490,7 +512,7 @@ class Graph {
490
512
  getOutputs(nodeId) {
491
513
  return immutableMap([...this.requireNode(nodeId).outputs].map(([name, value]) => [
492
514
  name,
493
- snapshot(value),
515
+ this.expose(value),
494
516
  ]));
495
517
  }
496
518
  dispose() {
@@ -578,19 +600,23 @@ class Graph {
578
600
  const ordered = topological(this.nodes, this.connections).filter((node) => initial.includes(node) && !insideFeedback.has(node.id));
579
601
  /** Nodes that cooked in this run, for `reuse: "strict"`. */
580
602
  const cooked = new Set();
581
- for (const node of ordered) {
582
- if (signal.aborted)
583
- break;
584
- if (reuse && this.reusable(node) && !(reuse === "strict" && this.feedsFrom(node, cooked))) {
585
- this.emit({ type: "node:start", runId, nodeId: node.id });
586
- this.emit({ type: "node:complete", runId, nodeId: node.id });
587
- continue;
603
+ const skip = (node) => !!reuse && this.reusable(node) && !(reuse === "strict" && this.feedsFrom(node, cooked));
604
+ if (this.execution.concurrency > 1)
605
+ await this.cookConcurrently(ordered, runId, signal, enqueue, skip, cooked);
606
+ else
607
+ for (const node of ordered) {
608
+ if (signal.aborted)
609
+ break;
610
+ if (skip(node)) {
611
+ this.emit({ type: "node:start", runId, nodeId: node.id });
612
+ this.emit({ type: "node:complete", runId, nodeId: node.id });
613
+ continue;
614
+ }
615
+ await this.executeNode(node, runId, signal, {}, enqueue);
616
+ cooked.add(node.id);
617
+ if (!signal.aborted)
618
+ node.lastCook = this.cookKey(node);
588
619
  }
589
- await this.executeNode(node, runId, signal, {}, enqueue);
590
- cooked.add(node.id);
591
- if (!signal.aborted)
592
- node.lastCook = this.cookKey(node);
593
- }
594
620
  if (injection && !signal.aborted)
595
621
  enqueue(injection.nodeId, injection.inputName, injection.payload, {
596
622
  kind: "external",
@@ -628,7 +654,90 @@ class Graph {
628
654
  this.active = undefined;
629
655
  }
630
656
  }
657
+ /**
658
+ * The run's main order with up to `concurrency` nodes cooking at once.
659
+ *
660
+ * Nodes START strictly in topological order: the next node starts once
661
+ * everything it reads has completed and a slot is free, and nothing later
662
+ * in the order overtakes it. So the `node:start` events — and a run trace
663
+ * built from them — come in the same order as a serial run on every run;
664
+ * only completions interleave. A Kahn order puts siblings side by side (the
665
+ * morph's four renders are adjacent), which is where the overlap is.
666
+ *
667
+ * A feedback container, a node with trigger ports and a GPU node cook
668
+ * alone: their state (the carried step, the trigger queue's sequence, one
669
+ * device) is ordered by the serial loop and not worth sharing.
670
+ *
671
+ * On a failure nothing new starts, the nodes already cooking finish, and
672
+ * the failure earliest in the order is the run's, so the message does not
673
+ * depend on which stage happened to answer first.
674
+ */
675
+ async cookConcurrently(ordered, runId, signal, enqueue, skip, cooked) {
676
+ const position = new Map(ordered.map((node, index) => [node.id, index]));
677
+ const needs = new Map();
678
+ for (const dependency of graphDependencies(this.nodes, this.connections)) {
679
+ if (!position.has(dependency.sourceId) || !position.has(dependency.targetId))
680
+ continue;
681
+ const list = needs.get(dependency.targetId) ?? [];
682
+ list.push(dependency.sourceId);
683
+ needs.set(dependency.targetId, list);
684
+ }
685
+ const done = new Set();
686
+ const running = new Map();
687
+ let exclusiveRunning = false;
688
+ let failure;
689
+ let next = 0;
690
+ for (;;) {
691
+ while (!failure &&
692
+ !signal.aborted &&
693
+ !exclusiveRunning &&
694
+ next < ordered.length &&
695
+ running.size < this.execution.concurrency) {
696
+ const node = ordered[next];
697
+ if (!(needs.get(node.id) ?? []).every((id) => done.has(id)))
698
+ break;
699
+ const alone = cooksAlone(node);
700
+ if (alone && running.size)
701
+ break;
702
+ next++;
703
+ if (skip(node)) {
704
+ this.emit({ type: "node:start", runId, nodeId: node.id });
705
+ this.emit({ type: "node:complete", runId, nodeId: node.id });
706
+ done.add(node.id);
707
+ continue;
708
+ }
709
+ if (alone)
710
+ exclusiveRunning = true;
711
+ const index = position.get(node.id);
712
+ const task = this.executeNode(node, runId, signal, {}, enqueue).then(() => {
713
+ cooked.add(node.id);
714
+ if (!signal.aborted)
715
+ node.lastCook = this.cookKey(node);
716
+ done.add(node.id);
717
+ }, (error) => {
718
+ if (!failure || index < failure.index)
719
+ failure = { index, error };
720
+ }).finally(() => {
721
+ running.delete(node.id);
722
+ if (alone)
723
+ exclusiveRunning = false;
724
+ });
725
+ running.set(node.id, task);
726
+ }
727
+ if (!running.size)
728
+ break;
729
+ await Promise.race(running.values());
730
+ }
731
+ if (failure)
732
+ throw failure.error;
733
+ }
631
734
  async executeNode(node, runId, signal, triggers, enqueue) {
735
+ const scope = this.execution.nodeScope;
736
+ if (!scope)
737
+ return this.cookNode(node, runId, signal, triggers, enqueue);
738
+ return scope(node.id, () => this.cookNode(node, runId, signal, triggers, enqueue));
739
+ }
740
+ async cookNode(node, runId, signal, triggers, enqueue) {
632
741
  this.emit({ type: "node:start", runId, nodeId: node.id });
633
742
  /**
634
743
  * `cascade.core.Previous` reads the carried state off its container, the
@@ -710,7 +819,7 @@ class Graph {
710
819
  const definition = node.definition.outputs?.[name];
711
820
  if (!definition || definition.kind !== "data")
712
821
  continue;
713
- const value = outputValue(response.outputs[name], definition, `${node.id}.${name}`);
822
+ const value = outputValue(response.outputs[name], definition, `${node.id}.${name}`, this.values);
714
823
  node.outputs.set(name, value);
715
824
  this.emit({
716
825
  type: "node:output",
@@ -739,7 +848,7 @@ class Graph {
739
848
  }),
740
849
  }
741
850
  : {
742
- set: (value) => pendingData.set(name, outputValue(value, definition, `${node.id}.${name}`)),
851
+ set: (value) => pendingData.set(name, outputValue(value, definition, `${node.id}.${name}`, this.values)),
743
852
  },
744
853
  ]));
745
854
  const capabilityEntries = [];
@@ -747,7 +856,11 @@ class Graph {
747
856
  const installed = this.host.capabilities[name];
748
857
  if (name === "gpu" && isGpuHost(installed)) {
749
858
  await installed.ensure();
750
- capabilityEntries.push([name, installed.forNode(node.id)]);
859
+ const gpu = installed.forNode(node.id);
860
+ const shortfall = gpuLimitShortfall(node.id, node.definition.gpuLimits, gpu);
861
+ if (shortfall)
862
+ throw new Error(shortfall);
863
+ capabilityEntries.push([name, gpu]);
751
864
  }
752
865
  else {
753
866
  capabilityEntries.push([
@@ -801,12 +914,14 @@ class Graph {
801
914
  if (pendingData.has(name)) {
802
915
  const value = pendingData.get(name);
803
916
  node.outputs.set(name, value);
917
+ // The stored value is already frozen (or trusted), so the event
918
+ // carries it as is rather than a fresh copy nobody may read.
804
919
  this.emit({
805
920
  type: "node:output",
806
921
  runId,
807
922
  nodeId: node.id,
808
923
  outputName: name,
809
- value: snapshot(value),
924
+ value: this.expose(value),
810
925
  });
811
926
  }
812
927
  for (const emitted of pendingTriggers)
@@ -1262,7 +1377,8 @@ class Graph {
1262
1377
  }
1263
1378
  return Object.freeze({
1264
1379
  result: carried,
1265
- history: Object.freeze(history),
1380
+ // Recorded as settled, so reading `history` back is not a copy.
1381
+ history: settle(history),
1266
1382
  });
1267
1383
  }
1268
1384
  finally {
@@ -1406,7 +1522,10 @@ function materialize(authored, registration) {
1406
1522
  const object = saved && typeof saved === "object" && "value" in saved
1407
1523
  ? saved
1408
1524
  : null;
1409
- const value = runtimeValue(object ? object.value : saved, definition.type, false, `${authored.id}.${name}`);
1525
+ const stored = object ? object.value : saved;
1526
+ const value = runtimeValue(
1527
+ // A colour saved as `{ r, g, b }` by the class image nodes reads as one.
1528
+ definition.type === "color" ? Color.upgrade(stored) : stored, definition.type, false, `${authored.id}.${name}`);
1410
1529
  props[name] = value;
1411
1530
  // A stored plain value is an author's decision and beats the declared
1412
1531
  // default expression, so any binding seeded above has to go.
@@ -1702,6 +1821,18 @@ function snapshotRegistration(registration) {
1702
1821
  const definition = snapshot(registration.definition);
1703
1822
  return Object.freeze({ ...registration, definition });
1704
1823
  }
1824
+ /** A node `cookConcurrently` never overlaps with another; see there. */
1825
+ function cooksAlone(node) {
1826
+ if (node.definition.container === "feedback")
1827
+ return true;
1828
+ if (node.definition.capabilities?.includes("gpu"))
1829
+ return true;
1830
+ const ports = [
1831
+ ...Object.values(node.definition.inputs ?? {}),
1832
+ ...Object.values(node.definition.outputs ?? {}),
1833
+ ];
1834
+ return ports.some((port) => port.kind === "trigger");
1835
+ }
1705
1836
  function isGated(node) {
1706
1837
  return Object.values(node.definition.inputs ?? {}).some((input) => input.kind === "trigger");
1707
1838
  }
@@ -1882,7 +2013,103 @@ function misuse(code, message) {
1882
2013
  throw runtimeError(code, message);
1883
2014
  }
1884
2015
  function snapshot(value) {
1885
- return cloneValue(value, new WeakSet());
2016
+ const clone = cloneValue(value, new WeakSet());
2017
+ if (clone !== null && typeof clone === "object")
2018
+ settled.add(clone);
2019
+ return clone;
2020
+ }
2021
+ /**
2022
+ * Plain arrays and objects already checked and deep-frozen — by `cloneValue`
2023
+ * or `settle` — so a second snapshot of the same value is a lookup, not a
2024
+ * copy. A runtime value is read far more often than it is made: the producer
2025
+ * sets it once, then the event, `getOutputs`, `finish` and `inspect` each
2026
+ * used to clone it again (a 1M-point array four times a frame).
2027
+ *
2028
+ * Only roots and containers of objects are recorded; a leaf of primitives
2029
+ * (a `[x, y]` point) is cheaper to re-walk than to record a million times.
2030
+ */
2031
+ const settled = new WeakSet();
2032
+ /** Plain containers copied by `cloneValue`, for the copy-count tests. */
2033
+ let copiedContainers = 0;
2034
+ /** How many plain arrays/objects the runtime has copied in this process. */
2035
+ export function runtimeValueCopies() {
2036
+ return copiedContainers;
2037
+ }
2038
+ function isPlainContainer(value) {
2039
+ if (Array.isArray(value))
2040
+ return true;
2041
+ const prototype = Object.getPrototypeOf(value);
2042
+ return prototype === Object.prototype || prototype === null;
2043
+ }
2044
+ function checkScalar(value) {
2045
+ if (typeof value === "number" && !Number.isFinite(value))
2046
+ misuse("runtime/non-serializable-value", "Runtime numbers must be finite");
2047
+ if (typeof value === "function" ||
2048
+ typeof value === "symbol" ||
2049
+ typeof value === "bigint")
2050
+ misuse("runtime/non-serializable-value", `Runtime values cannot contain ${typeof value}`);
2051
+ }
2052
+ /**
2053
+ * Check a value a node handed over and deep-freeze it where it stands: the
2054
+ * same rules as `cloneValue` (finite numbers, no functions, no cycles, class
2055
+ * instances and typed arrays passed through untouched), without allocating.
2056
+ * The producer gave it away with `set()`; freezing is what keeps the
2057
+ * consumers' view of it from changing under them.
2058
+ */
2059
+ function settle(value) {
2060
+ if (value === null || typeof value !== "object") {
2061
+ checkScalar(value);
2062
+ return value;
2063
+ }
2064
+ if (!settled.has(value) && isPlainContainer(value)) {
2065
+ settleContainer(value, new WeakSet());
2066
+ settled.add(value);
2067
+ }
2068
+ return value;
2069
+ }
2070
+ /** Returns whether `value` held any plain container (and so was recorded). */
2071
+ function settleContainer(value, ancestors) {
2072
+ let nested = false;
2073
+ const visit = (item) => {
2074
+ if (item === null || typeof item !== "object") {
2075
+ checkScalar(item);
2076
+ return;
2077
+ }
2078
+ if (settled.has(item) || !isPlainContainer(item))
2079
+ return;
2080
+ if (ancestors.has(item))
2081
+ misuse("runtime/non-serializable-value", "Runtime values cannot contain cycles");
2082
+ if (!nested) {
2083
+ nested = true;
2084
+ ancestors.add(value);
2085
+ }
2086
+ if (settleContainer(item, ancestors))
2087
+ settled.add(item);
2088
+ };
2089
+ if (Array.isArray(value))
2090
+ for (let index = 0; index < value.length; index++)
2091
+ visit(value[index]);
2092
+ else
2093
+ for (const key of Object.keys(value))
2094
+ visit(value[key]);
2095
+ if (nested)
2096
+ ancestors.delete(value);
2097
+ Object.freeze(value);
2098
+ return nested;
2099
+ }
2100
+ /**
2101
+ * `"trust"`: the top level is checked by `runtimeValue`'s type test and
2102
+ * frozen shallowly, so `push` or `sort` on a handed-over array still throws;
2103
+ * nested contents are not walked.
2104
+ */
2105
+ function trust(value) {
2106
+ if (value === null || typeof value !== "object") {
2107
+ checkScalar(value);
2108
+ return value;
2109
+ }
2110
+ if (isPlainContainer(value))
2111
+ Object.freeze(value);
2112
+ return value;
1886
2113
  }
1887
2114
  function immutableMap(entries) {
1888
2115
  const source = new Map(entries);
@@ -1902,22 +2129,15 @@ function immutableMap(entries) {
1902
2129
  return view;
1903
2130
  }
1904
2131
  function cloneValue(value, ancestors) {
1905
- if (typeof value === "number" && !Number.isFinite(value))
1906
- misuse("runtime/non-serializable-value", "Runtime numbers must be finite");
1907
- if (typeof value === "function" ||
1908
- typeof value === "symbol" ||
1909
- typeof value === "bigint")
1910
- misuse("runtime/non-serializable-value", `Runtime values cannot contain ${typeof value}`);
2132
+ checkScalar(value);
1911
2133
  if (value === undefined || value === null || typeof value !== "object")
1912
2134
  return value;
1913
- const prototype = Object.getPrototypeOf(value);
1914
- if (!Array.isArray(value) &&
1915
- prototype !== Object.prototype &&
1916
- prototype !== null)
2135
+ if (settled.has(value) || !isPlainContainer(value))
1917
2136
  return value;
1918
2137
  if (ancestors.has(value))
1919
2138
  misuse("runtime/non-serializable-value", "Runtime values cannot contain cycles");
1920
2139
  ancestors.add(value);
2140
+ copiedContainers++;
1921
2141
  const clone = Array.isArray(value)
1922
2142
  ? value.map((item) => cloneValue(item, ancestors))
1923
2143
  : Object.fromEntries(Object.entries(value).map(([key, item]) => [
@@ -1943,17 +2163,19 @@ const TUPLE_LENGTHS = {
1943
2163
  * An output value, checked against its declaration. A bundle with declared
1944
2164
  * fields may only set those, and each one must be a valid value of its type.
1945
2165
  */
1946
- function outputValue(value, definition, path) {
2166
+ function outputValue(value, definition, path, policy) {
2167
+ // Handed over by the producer, so adopted (frozen where it stands), not copied.
2168
+ const adopt = policy === "trust" ? trust : settle;
1947
2169
  if (definition.type !== "bundle" || !definition.fields)
1948
- return runtimeValue(value, definition.type, false, path);
2170
+ return runtimeValue(value, definition.type, false, path, adopt);
1949
2171
  const fields = definition.fields;
1950
2172
  const problem = bundleValueProblem(value, fields);
1951
2173
  if (problem)
1952
2174
  misuse("runtime/value-type", `${path} ${problem}`);
1953
2175
  if (isBundleValue(value))
1954
2176
  for (const [field, item] of Object.entries(value))
1955
- runtimeValue(item, bundleFieldType(fields[field]), true, `${path}.${field}`);
1956
- return runtimeValue(value, "bundle", false, path);
2177
+ runtimeValue(item, bundleFieldType(fields[field]), true, `${path}.${field}`, (checked) => checked);
2178
+ return runtimeValue(value, "bundle", false, path, adopt);
1957
2179
  }
1958
2180
  /** Every path an expression names through ch/chs/chv. */
1959
2181
  function expressionPaths(expression) {
@@ -1975,7 +2197,9 @@ function readsBundleFields(node) {
1975
2197
  }
1976
2198
  return false;
1977
2199
  }
1978
- function runtimeValue(value, type, allowUndefined, path) {
2200
+ function runtimeValue(value, type, allowUndefined, path,
2201
+ /** How a checked value is kept: copied (a caller's value) by default. */
2202
+ keep = snapshot) {
1979
2203
  if (value === undefined) {
1980
2204
  if (allowUndefined)
1981
2205
  return undefined;
@@ -2001,7 +2225,7 @@ function runtimeValue(value, type, allowUndefined, path) {
2001
2225
  type !== "string");
2002
2226
  if (!valid)
2003
2227
  misuse("runtime/value-type", `${path} is not a valid ${type}`);
2004
- return snapshot(value);
2228
+ return keep(value);
2005
2229
  }
2006
2230
  function sameValues(a, b, equal) {
2007
2231
  return a.length === b.length && a.every((value, index) => equal(value, b[index]));
@@ -216,9 +216,39 @@ export type RuntimeEvent = {
216
216
  readonly type: "run:complete";
217
217
  readonly result: RunResult;
218
218
  };
219
+ /**
220
+ * What the runtime does with a value a node hands to `outputs.x.set()`.
221
+ *
222
+ * - `"freeze"` (default): checked and deep-frozen in place, never copied. A
223
+ * node that mutates its output after handing it over, or mutates an input,
224
+ * throws instead of corrupting another node's view.
225
+ * - `"trust"`: checked at the top level only and shallow-frozen; nested
226
+ * contents are the producer's word. For headless runs (`cascade run`), where
227
+ * a 1M-point array walked every frame was most of the frame.
228
+ *
229
+ * Either way nothing is cloned on the way through: reading an output back
230
+ * (`getOutputs`, `inspect`, `node:output`) hands out the same frozen value.
231
+ */
232
+ export type RuntimeValuePolicy = "freeze" | "trust";
219
233
  export interface CreateRuntimeOptions {
220
234
  readonly host: RuntimeHost;
221
235
  readonly nodes?: readonly NodeRegistration[];
236
+ /** See `RuntimeValuePolicy`. Default `"freeze"`. */
237
+ readonly values?: RuntimeValuePolicy;
238
+ /**
239
+ * How many nodes one run may cook at once (default 1: one after another).
240
+ * Nodes still START in topological order, a node only once everything it
241
+ * reads has completed, so the `node:start` sequence and the outputs are the
242
+ * same as a serial run; only completions interleave. Feedback containers,
243
+ * trigger-wired and GPU nodes always cook alone.
244
+ */
245
+ readonly concurrency?: number;
246
+ /**
247
+ * Wraps each node's execute. A Node host uses it to carry the node id
248
+ * through the cook's async work (AsyncLocalStorage), so a stage call or a
249
+ * log line is attributed to its node when several cook at once.
250
+ */
251
+ readonly nodeScope?: <T>(nodeId: string, run: () => T) => T;
222
252
  }
223
253
  export interface CascadeRuntime {
224
254
  registerNode(registration: NodeRegistration): void;
package/dist/stage.js CHANGED
@@ -53,7 +53,10 @@ async function overHttp(stage, args, signal) {
53
53
  const postStage = async () => fetch("/api/exec", {
54
54
  method: "POST",
55
55
  headers: { "Content-Type": "application/json", "X-Cascade-Exec-Capability": await capability() },
56
- body: JSON.stringify({ stage, args }),
56
+ // `cook`: a node's cook is a pure function of its inputs, so the server's
57
+ // default stage cache may serve it (see StageCachePolicy). A panel posts
58
+ // through its own api.runStage, without this, and is never cached unasked.
59
+ body: JSON.stringify({ stage, args, caller: "cook" }),
57
60
  signal
58
61
  });
59
62
  let response;