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> {}