async_nodes/node.rs
1//! The node contract: re-executable units over consumer state `S`.
2//!
3//! Nodes are the consumer's computation units; the engine owns topology, scheduling, and
4//! state movement. A node **never names its successors** — it reads its gathered inputs,
5//! mutates [`RunContext::state`], stages outputs, and the engine decides what happens next
6//! by evaluating the outgoing edges. A node may run zero, one, or many times per run.
7
8use async_trait::async_trait;
9use indexmap::IndexMap;
10
11use super::{
12 BspContext,
13 error::BspError,
14 graph::{NodeId, PortIndex},
15 recovery::RecoveryPolicy,
16 run::RunContext,
17 value::BspValue,
18};
19
20/// How a node joins a frontier, i.e. how its pending activation tokens are counted.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
22pub enum Activation {
23 /// Fire as soon as at least one incoming edge holds a pending token (control-flow
24 /// default). Exactly one token is consumed per firing (from the lowest-declared
25 /// pending edge), so co-pending tokens re-fire the node on later supersteps — an
26 /// `Any`-join fires once per arriving branch.
27 #[default]
28 Any,
29 /// Fire only when *every* incoming edge holds ≥1 pending token (dataflow default).
30 /// One token per incoming edge is consumed.
31 ///
32 /// **Beware inside loops:** an `All`-join on a cycle is the classic self-deadlock —
33 /// one predecessor's token arrives once per iteration while the other's arrives once
34 /// ever, so the join waits forever. Joins inside loops are almost always `Any` or
35 /// merger-joins; validation warns on this shape.
36 All,
37}
38
39/// Defines the evaluated outcome of a single node's execution turn.
40#[derive(Debug)]
41pub enum NodeOutcome {
42 /// Node executed successfully, returning values mapped to specific output ports.
43 /// Uses `IndexMap` to guarantee deterministic iteration order.
44 Success(IndexMap<PortIndex, BspValue>),
45
46 /// Node requested execution pause (e.g., waiting for external input).
47 Suspend {
48 /// Arbitrary context parameters passed back to the host application.
49 payload: serde_json::Value,
50 /// Explicit indices of inputs that must be provided to resume.
51 waiting_inputs: Vec<PortIndex>,
52 },
53}
54
55/// The programmatic contract for writing execution nodes.
56///
57/// `S` is the consumer-owned run state: the engine is generic over it and never inspects
58/// it except through explicitly defined lenses (conditions, mergers). Implementors get
59/// control-flow defaults; the `#[node]` / `#[node_blocking]` macros generate dataflow
60/// profile nodes (`Activation::All`, named ports) over this same trait.
61#[async_trait]
62pub trait BspNode<S>: Send + Sync {
63 /// Semantic name used for logs, interceptors, and the GUI.
64 fn name(&self) -> &str;
65
66 /// Registry type name used for (de)serialization, e.g. `"my-crate.generate"`.
67 /// Defaults to [`BspNode::name`]; override for stable cross-version identity.
68 fn type_name(&self) -> &str {
69 self.name()
70 }
71
72 /// Dataflow profile: the unique indices of input ports required for execution
73 /// (empty for pure control-flow nodes).
74 fn input_ports(&self) -> Vec<PortIndex> {
75 Vec::new()
76 }
77
78 /// Dataflow profile: the unique indices of output ports produced upon success.
79 fn output_ports(&self) -> Vec<PortIndex> {
80 Vec::new()
81 }
82
83 /// Indicates whether output ports are evaluated dynamically at runtime.
84 fn is_dynamic_output(&self) -> bool {
85 false
86 }
87
88 /// How this node joins a frontier. Default: [`Activation::Any`] (control-flow).
89 fn activation(&self) -> Activation {
90 Activation::Any
91 }
92
93 /// Per-node recovery policy. Default: fail-fast (first error aborts the run).
94 fn recovery(&self) -> RecoveryPolicy {
95 RecoveryPolicy::FailFast
96 }
97
98 /// Optional name of a consumer-registered [`crate::merge::Merger`] used when this
99 /// node joins concurrently produced branch states at a superstep barrier.
100 fn merger(&self) -> Option<&str> {
101 None
102 }
103
104 /// Optional circuit breaker on how many times this node may execute per run
105 /// (poll-until style nodes).
106 fn max_executions(&self) -> Option<u64> {
107 None
108 }
109
110 /// Core computation. Mutates state and the value store through `run`; the engine
111 /// decides what happens next from the edges — nodes never name their successors.
112 async fn execute(&self, run: &mut RunContext<'_, S>) -> Result<NodeOutcome, BspError>;
113}
114
115#[async_trait]
116impl<S: Send> BspNode<S> for Box<dyn BspNode<S>> {
117 fn name(&self) -> &str {
118 (**self).name()
119 }
120
121 fn type_name(&self) -> &str {
122 (**self).type_name()
123 }
124
125 fn input_ports(&self) -> Vec<PortIndex> {
126 (**self).input_ports()
127 }
128
129 fn output_ports(&self) -> Vec<PortIndex> {
130 (**self).output_ports()
131 }
132
133 fn is_dynamic_output(&self) -> bool {
134 (**self).is_dynamic_output()
135 }
136
137 fn activation(&self) -> Activation {
138 (**self).activation()
139 }
140
141 fn recovery(&self) -> RecoveryPolicy {
142 (**self).recovery()
143 }
144
145 fn merger(&self) -> Option<&str> {
146 (**self).merger()
147 }
148
149 fn max_executions(&self) -> Option<u64> {
150 (**self).max_executions()
151 }
152
153 async fn execute(&self, run: &mut RunContext<'_, S>) -> Result<NodeOutcome, BspError> {
154 (**self).execute(run).await
155 }
156}
157
158/// The dataflow-profile authoring trait: plain-Rust node authoring without macros.
159///
160/// The `#[node]` / `#[node_blocking]` macros generate this same shape under the hood;
161/// implement `DataflowNode` directly when you want the classic
162/// `(node_id, inputs, ctx) -> outputs` signature with no macro magic — e.g. when a
163/// registry or GUI bridge (like the snarl one) constructs nodes from descriptors.
164///
165/// Every `DataflowNode` automatically implements [`BspNode`] over any consumer state
166/// `S`, with join semantics ([`Activation::All`]) and outputs packed into
167/// [`NodeOutcome::Success`]. The two authoring styles compose freely in one graph.
168///
169/// Note: implement *either* `DataflowNode` *or* [`BspNode`] for a type, never both —
170/// the blanket impl would collide.
171#[async_trait]
172pub trait DataflowNode: Send + Sync {
173 /// Semantic name used for logs, interceptors, and the GUI.
174 fn name(&self) -> &str;
175
176 /// The unique indices of input ports required for execution.
177 fn input_ports(&self) -> Vec<PortIndex>;
178
179 /// The unique indices of output ports produced upon success.
180 fn output_ports(&self) -> Vec<PortIndex>;
181
182 /// Indicates whether output ports are evaluated dynamically at runtime.
183 fn is_dynamic_output(&self) -> bool {
184 false
185 }
186
187 /// Core computation over the gathered pin inputs.
188 async fn execute(
189 &self,
190 node_id: NodeId,
191 inputs: IndexMap<PortIndex, BspValue>,
192 ctx: &BspContext,
193 ) -> Result<IndexMap<PortIndex, BspValue>, BspError>;
194}
195
196#[async_trait]
197impl<S: Send, T: DataflowNode> BspNode<S> for T {
198 fn name(&self) -> &str {
199 DataflowNode::name(self)
200 }
201
202 fn type_name(&self) -> &str {
203 DataflowNode::name(self)
204 }
205
206 fn input_ports(&self) -> Vec<PortIndex> {
207 DataflowNode::input_ports(self)
208 }
209
210 fn output_ports(&self) -> Vec<PortIndex> {
211 DataflowNode::output_ports(self)
212 }
213
214 fn is_dynamic_output(&self) -> bool {
215 DataflowNode::is_dynamic_output(self)
216 }
217
218 fn activation(&self) -> Activation {
219 Activation::All
220 }
221
222 async fn execute(&self, run: &mut RunContext<'_, S>) -> Result<NodeOutcome, BspError> {
223 let inputs = std::mem::take(&mut run.inputs);
224 let outputs = DataflowNode::execute(self, run.node_id, inputs, run.ctx).await?;
225 Ok(NodeOutcome::Success(outputs))
226 }
227}