Skip to main content

swiftide_tasks/
node.rs

1//! Typed node traits and handles for task graphs.
2//!
3//! This module defines the contracts for task nodes and the typed identifiers used to wire them
4//! together.
5//!
6//! # Examples
7//!
8//! ```no_run
9//! use swiftide_tasks::{JoinInput, NodeError, Task, Transition};
10//!
11//! let mut task = Task::<i32, i32>::new();
12//! let start = task.register_node_fn(|input: &i32| -> Result<i32, NodeError> { Ok(*input) });
13//! let branch = task.register_node_fn(|input: &i32| -> Result<i32, NodeError> { Ok(*input + 1) });
14//! let join = task.register_node_fn(|input: &JoinInput<i32>| -> Result<i32, NodeError> {
15//!     Ok(input.iter().copied().sum())
16//! });
17//!
18//! task.starts_with(start);
19//! task.register_transition(start, move |value| {
20//!     // `join_with` defines the branch group and the join node that waits for it.
21//!     Transition::fan_out(&branch, value)
22//!         .join_with(join.join())
23//! })?;
24//! // Branches still decide where their own output goes before the join can run. Fan-out branches
25//! // cannot finish independently; use a join node as the single place that finishes the task.
26//! task.register_transition(branch, join.join())?;
27//! # Ok::<(), Box<dyn std::error::Error>>(())
28//! ```
29use super::traits::{NodeArg, TaskNode};
30use super::transition::{
31    AnyJoinInput, AnyJoinTarget, JoinInput, JoinTarget, MarkedTransition, Transition,
32};
33
34/// A typed handle to a registered node in a [`Task`](crate::Task).
35///
36/// `NodeId` keeps the node's type information so transitions can be expressed without manual
37/// downcasts. Use [`NodeId::transitions_with`] for the common linear case,
38/// [`Transition::fan_out`](crate::Transition::fan_out) when building static fan-out
39/// transitions, and [`NodeId::join`] for join nodes.
40#[derive(PartialEq, Eq)]
41pub struct NodeId<T: TaskNode + ?Sized> {
42    id: usize,
43    _marker: std::marker::PhantomData<T>,
44}
45
46impl<T: TaskNode + ?Sized> std::fmt::Debug for NodeId<T> {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        let type_name = std::any::type_name::<T>();
49
50        write!(f, "NodeId<{type_name}>({})", self.id)
51    }
52}
53
54impl<T: TaskNode + ?Sized> NodeId<T> {
55    /// Returns the stable numeric identifier assigned when the node was registered.
56    pub fn id(&self) -> usize {
57        self.id
58    }
59
60    /// Builds a typed transition to this node with the provided input.
61    ///
62    /// This is the most ergonomic way to connect one node to the next in a linear task.
63    ///
64    /// # Examples
65    ///
66    /// ```no_run
67    /// use swiftide_tasks::{NodeError, Task};
68    ///
69    /// let mut task = Task::<i32, i32>::new();
70    /// let start = task.register_node_fn(|input: &i32| -> Result<i32, NodeError> { Ok(*input + 1) });
71    /// let finish = task.register_node_fn(|input: &i32| -> Result<i32, NodeError> { Ok(*input * 2) });
72    ///
73    /// task.starts_with(start);
74    /// task.register_transition(start, move |value| finish.transitions_with(value))?;
75    /// # Ok::<(), Box<dyn std::error::Error>>(())
76    /// ```
77    pub fn transitions_with(&self, context: T::Input) -> MarkedTransition<T> {
78        Transition::next(self, context)
79    }
80}
81
82impl<T, Payload> NodeId<T>
83where
84    T: TaskNode<Input = JoinInput<Payload>> + ?Sized,
85    Payload: NodeArg,
86{
87    /// Creates a join target that waits for every branch in the fan-out group.
88    ///
89    /// Fan-out branches cannot be independent task endings. To model branches that are
90    /// conceptually terminal, transition each branch to a join node and finish the task from that
91    /// join node.
92    ///
93    /// # Examples
94    ///
95    /// ```no_run
96    /// use swiftide_tasks::{JoinInput, NodeError, Task, Transition};
97    ///
98    /// let mut task = Task::<i32, i32>::new();
99    /// let start = task.register_node_fn(|input: &i32| -> Result<i32, NodeError> { Ok(*input) });
100    /// let branch = task.register_node_fn(|input: &i32| -> Result<i32, NodeError> { Ok(*input + 1) });
101    /// let join = task.register_node_fn(|input: &JoinInput<i32>| -> Result<i32, NodeError> {
102    ///     Ok(input.iter().copied().sum())
103    /// });
104    ///
105    /// task.starts_with(start);
106    /// task.register_transition(start, move |value| {
107    ///     Transition::fan_out(&branch, value)
108    ///         .join_with(join.join())
109    /// })?;
110    /// task.register_transition(branch, join.join())?;
111    /// # Ok::<(), Box<dyn std::error::Error>>(())
112    /// ```
113    pub fn join(&self) -> JoinTarget<T, Payload> {
114        JoinTarget::new(*self)
115    }
116}
117
118impl<T> NodeId<T>
119where
120    T: TaskNode<Input = AnyJoinInput> + ?Sized,
121{
122    /// Creates a join target for branches that intentionally contribute mixed payload types.
123    ///
124    /// Prefer [`NodeId::join`] with [`JoinInput<T>`](crate::JoinInput) when all branches
125    /// contribute the same payload type.
126    pub fn join_any(&self) -> AnyJoinTarget<T> {
127        AnyJoinTarget::new(*self)
128    }
129}
130
131impl<T: TaskNode + 'static + ?Sized> NodeId<T> {
132    /// Creates a typed node identifier for an already-registered node.
133    ///
134    /// Prefer [`Task::register_node`](crate::Task::register_node) when building normal task
135    /// graphs. Constructing an id manually is only correct when `id` refers to a node managed by
136    /// the caller's own task-like runtime.
137    pub fn new(id: usize, _node: &T) -> Self {
138        NodeId {
139            id,
140            _marker: std::marker::PhantomData,
141        }
142    }
143    /// Erases the concrete node type while keeping the node's typed input and output contracts.
144    pub fn as_dyn(
145        self,
146    ) -> NodeId<dyn TaskNode<Input = T::Input, Output = T::Output, Error = T::Error>> {
147        NodeId {
148            id: self.id,
149            _marker: std::marker::PhantomData,
150        }
151    }
152}
153
154impl<T: TaskNode + ?Sized> Clone for NodeId<T> {
155    fn clone(&self) -> Self {
156        *self
157    }
158}
159impl<T: TaskNode + ?Sized> Copy for NodeId<T> {}