Skip to main content

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}