swiftide_tasks/transition.rs
1//! Transition values describe how task execution should continue after a node finishes.
2//!
3//! This module contains the public building blocks for linear transitions, fan-out, join
4//! configuration, and join payload inspection.
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 left = task.register_node_fn(|input: &i32| -> Result<i32, NodeError> { Ok(*input + 1) });
14//! let right = task.register_node_fn(|input: &i32| -> Result<i32, NodeError> { Ok(*input + 2) });
15//! let join = task.register_node_fn(|input: &JoinInput<i32>| -> Result<i32, NodeError> {
16//! Ok(input.iter().copied().sum())
17//! });
18//!
19//! task.starts_with(start);
20//! task.register_transition(start, move |value| {
21//! // `join_with` defines the branch group and the join node that waits for it.
22//! Transition::fan_out(&left, value)
23//! .and(&right, value)
24//! .join_with(join.join())
25//! })?;
26//! // Branches still decide where their own output goes before the join can run. Fan-out branches
27//! // cannot finish independently; use a join node as the single place that finishes the task.
28//! task.register_transition(left, join.join())?;
29//! task.register_transition(right, join.join())?;
30//! # Ok::<(), Box<dyn std::error::Error>>(())
31//! ```
32use std::{any::Any, marker::PhantomData, sync::Arc};
33
34use super::{
35 errors::TaskError,
36 node::NodeId,
37 traits::{NodeArg, TaskNode},
38};
39
40/// Identifies a concrete runtime branch within a task run.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42pub(crate) struct BranchId(pub(crate) usize);
43
44/// Controls whether newly scheduled work should run one branch at a time or concurrently.
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
46pub enum ConcurrencyModel {
47 /// Run one branch to completion before starting the next runnable branch.
48 #[default]
49 Sequential,
50 /// Allow multiple runnable branches to execute at the same time.
51 Parallel,
52}
53
54#[derive(Debug, Clone)]
55pub(crate) struct NextNode {
56 pub(crate) node_id: usize,
57 pub(crate) context: Arc<dyn Any + Send + Sync>,
58}
59
60impl NextNode {
61 pub(crate) fn new<T: TaskNode + ?Sized>(node_id: NodeId<T>, context: T::Input) -> Self
62 where
63 <T as TaskNode>::Input: 'static,
64 {
65 Self {
66 node_id: node_id.id(),
67 context: Arc::new(context) as Arc<dyn Any + Send + Sync>,
68 }
69 }
70}
71
72type ErasedPayload = Arc<dyn Any + Send + Sync>;
73
74#[derive(Clone, Copy)]
75enum JoinInputFactory {
76 Fallible(fn(Vec<ErasedPayload>) -> Result<ErasedPayload, TaskError>),
77 Infallible(fn(Vec<ErasedPayload>) -> ErasedPayload),
78}
79
80impl JoinInputFactory {
81 fn build(self, payloads: Vec<ErasedPayload>) -> Result<ErasedPayload, TaskError> {
82 match self {
83 JoinInputFactory::Fallible(factory) => factory(payloads),
84 JoinInputFactory::Infallible(factory) => Ok(factory(payloads)),
85 }
86 }
87}
88
89#[doc(hidden)]
90#[derive(Clone, Copy)]
91pub struct JoinDefinition {
92 pub(crate) join_node_id: usize,
93 input_factory: JoinInputFactory,
94}
95
96impl std::fmt::Debug for JoinDefinition {
97 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98 f.debug_struct("JoinDefinition")
99 .field("join_node_id", &self.join_node_id)
100 .finish_non_exhaustive()
101 }
102}
103
104impl JoinDefinition {
105 fn typed<Payload: NodeArg>(join_node_id: usize) -> Self {
106 Self {
107 join_node_id,
108 input_factory: JoinInputFactory::Fallible(typed_join_input::<Payload>),
109 }
110 }
111
112 fn any(join_node_id: usize) -> Self {
113 Self {
114 join_node_id,
115 input_factory: JoinInputFactory::Infallible(any_join_input),
116 }
117 }
118
119 pub(crate) fn into_input(
120 self,
121 payloads: Vec<ErasedPayload>,
122 ) -> Result<ErasedPayload, TaskError> {
123 self.input_factory.build(payloads)
124 }
125}
126
127fn typed_join_input<Payload: NodeArg>(
128 payloads: Vec<ErasedPayload>,
129) -> Result<ErasedPayload, TaskError> {
130 let branches = payloads
131 .into_iter()
132 .map(|payload| {
133 payload.downcast::<Payload>().map_err(|_| {
134 TaskError::invalid_state(format!(
135 "Join payload expected type {}",
136 std::any::type_name::<Payload>()
137 ))
138 })
139 })
140 .collect::<Result<Vec<_>, _>>()?;
141
142 Ok(Arc::new(JoinInput::<Payload>::new(branches)) as ErasedPayload)
143}
144
145fn any_join_input(payloads: Vec<ErasedPayload>) -> ErasedPayload {
146 Arc::new(AnyJoinInput::new(payloads)) as ErasedPayload
147}
148
149/// A configured join destination for branches that should converge into a join node.
150///
151/// Build a `JoinTarget` from a join node with [`NodeId::join`](crate::NodeId::join),
152/// and then register it through
153/// [`Task::register_transition`](crate::Task::register_transition).
154/// Users normally do not construct this type directly.
155#[must_use]
156pub struct JoinTarget<T: TaskNode<Input = JoinInput<Payload>> + ?Sized, Payload: NodeArg> {
157 pub(crate) definition: JoinDefinition,
158 _marker: PhantomData<(*const T, Payload)>,
159}
160
161/// A join target with a synchronous payload mapping step.
162///
163/// This is returned by [`JoinTarget::map`] and registered with
164/// [`Task::register_transition`](crate::Task::register_transition).
165#[must_use]
166pub struct MappedJoinTarget<T: TaskNode<Input = JoinInput<Payload>> + ?Sized, Payload: NodeArg, F> {
167 pub(crate) join_target: JoinTarget<T, Payload>,
168 pub(crate) map: F,
169}
170
171/// A configured join destination for branches with mixed payload types.
172///
173/// Build an `AnyJoinTarget` from an [`AnyJoinInput`] node with
174/// [`NodeId::join_any`](crate::NodeId::join_any).
175#[must_use]
176pub struct AnyJoinTarget<T: TaskNode<Input = AnyJoinInput> + ?Sized> {
177 pub(crate) definition: JoinDefinition,
178 _marker: PhantomData<T>,
179}
180
181/// A value accepted by [`FanOutTransition::join_with`].
182#[doc(hidden)]
183pub trait JoinDestination {
184 #[doc(hidden)]
185 fn into_definition(self) -> JoinDefinition;
186}
187
188impl<T, Payload> JoinDestination for JoinTarget<T, Payload>
189where
190 T: TaskNode<Input = JoinInput<Payload>> + ?Sized,
191 Payload: NodeArg,
192{
193 fn into_definition(self) -> JoinDefinition {
194 self.definition
195 }
196}
197
198impl<T> JoinDestination for AnyJoinTarget<T>
199where
200 T: TaskNode<Input = AnyJoinInput> + ?Sized,
201{
202 fn into_definition(self) -> JoinDefinition {
203 self.definition
204 }
205}
206
207impl<T, Payload> JoinTarget<T, Payload>
208where
209 T: TaskNode<Input = JoinInput<Payload>> + ?Sized,
210 Payload: NodeArg,
211{
212 pub(crate) fn new(node_id: NodeId<T>) -> Self {
213 Self {
214 definition: JoinDefinition::typed::<Payload>(node_id.id()),
215 _marker: PhantomData,
216 }
217 }
218
219 /// Maps each joining branch output into the payload stored for the join node.
220 ///
221 /// # Examples
222 ///
223 /// ```no_run
224 /// use swiftide_tasks::{JoinInput, NodeError, Task, Transition};
225 ///
226 /// let mut task = Task::<i32, i32>::new();
227 /// let start = task.register_node_fn(|input: &i32| -> Result<i32, NodeError> { Ok(*input) });
228 /// let branch = task.register_node_fn(|input: &i32| -> Result<i32, NodeError> { Ok(*input + 1) });
229 /// let join = task.register_node_fn(|input: &JoinInput<i32>| -> Result<i32, NodeError> {
230 /// Ok(input.iter().copied().sum())
231 /// });
232 ///
233 /// task.starts_with(start);
234 /// task.register_transition(start, move |value| {
235 /// Transition::fan_out(&branch, value)
236 /// .join_with(join.join())
237 /// })?;
238 /// task.register_transition(branch, join.join().map(|value| value * 2))?;
239 /// # Ok::<(), Box<dyn std::error::Error>>(())
240 /// ```
241 pub fn map<F>(self, map: F) -> MappedJoinTarget<T, Payload, F>
242 where
243 F: Send + Sync + 'static,
244 {
245 MappedJoinTarget {
246 join_target: self,
247 map,
248 }
249 }
250
251 /// Maps each joining branch output asynchronously before storing it for the join node.
252 ///
253 /// # Examples
254 ///
255 /// ```no_run
256 /// use swiftide_tasks::{JoinInput, NodeError, Task, Transition};
257 ///
258 /// let mut task = Task::<i32, i32>::new();
259 /// let start = task.register_node_fn(|input: &i32| -> Result<i32, NodeError> { Ok(*input) });
260 /// let branch = task.register_node_fn(|input: &i32| -> Result<i32, NodeError> { Ok(*input + 1) });
261 /// let join = task.register_node_fn(|input: &JoinInput<i32>| -> Result<i32, NodeError> {
262 /// Ok(input.iter().copied().sum())
263 /// });
264 ///
265 /// task.starts_with(start);
266 /// task.register_transition(start, move |value| {
267 /// Transition::fan_out(&branch, value)
268 /// .join_with(join.join())
269 /// })?;
270 /// task.register_transition_async(branch, join.join().map_async(|value| async move { value * 2 }))?;
271 /// # Ok::<(), Box<dyn std::error::Error>>(())
272 /// ```
273 pub fn map_async<F>(self, map: F) -> MappedJoinTarget<T, Payload, F>
274 where
275 F: Send + Sync + 'static,
276 {
277 MappedJoinTarget {
278 join_target: self,
279 map,
280 }
281 }
282}
283
284impl<T: TaskNode<Input = AnyJoinInput> + ?Sized> AnyJoinTarget<T> {
285 pub(crate) fn new(node_id: NodeId<T>) -> Self {
286 Self {
287 definition: JoinDefinition::any(node_id.id()),
288 _marker: PhantomData,
289 }
290 }
291}
292
293/// Describes how task execution should continue after a node completes.
294///
295/// Most transitions are created either through [`NodeId::transitions_with`] for the linear case or
296/// [`Transition::fan_out`] plus [`FanOutTransition::join_with`] for branching. Use
297/// [`Transition::pause`] and [`Transition::error`] when a transition closure needs to control task
298/// execution directly.
299///
300/// # Examples
301///
302/// ```no_run
303/// use swiftide_tasks::{NodeError, Task, TaskRunOutcome, Transition};
304///
305/// # #[tokio::main(flavor = "current_thread")]
306/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
307/// let mut task = Task::<i32, i32>::new();
308///
309/// let start = task.register_node_fn(|input: &i32| -> Result<i32, NodeError> { Ok(*input) });
310/// let left = task.register_node_fn(|input: &i32| -> Result<i32, NodeError> { Ok(*input + 1) });
311/// let right = task.register_node_fn(|input: &i32| -> Result<i32, NodeError> { Ok(*input + 2) });
312/// let join = task.register_node_fn(
313/// |input: &swiftide_tasks::JoinInput<i32>| -> Result<i32, NodeError> {
314/// Ok(input.iter().copied().sum())
315/// },
316/// );
317///
318/// task.starts_with(start);
319/// task.register_transition(start, move |value| {
320/// Transition::fan_out(&left, value)
321/// .and(&right, value)
322/// .join_with(join.join())
323/// .concurrency_model(swiftide_tasks::ConcurrencyModel::Parallel)
324/// })?;
325/// task.register_transition(left, join.join())?;
326/// task.register_transition(right, join.join())?;
327/// task.register_transition(join, task.transitions_to_finish())?;
328///
329/// assert_eq!(task.run(1).await?, TaskRunOutcome::Completed(5));
330/// # Ok(())
331/// # }
332/// ```
333#[derive(Debug)]
334#[must_use]
335pub struct Transition {
336 pub(crate) action: TransitionAction,
337 pub(crate) concurrency_model: Option<ConcurrencyModel>,
338}
339
340/// A linear transition that preserves the destination node type.
341///
342/// This is returned by [`Transition::next`] and [`NodeId::transitions_with`], then accepted by
343/// [`Task::register_transition`](crate::Task::register_transition) closures.
344#[must_use]
345pub struct MarkedTransition<To: TaskNode + ?Sized>(Transition, PhantomData<To>);
346
347impl<To: TaskNode + ?Sized> MarkedTransition<To> {
348 pub(crate) fn new(transition: Transition) -> Self {
349 Self(transition, PhantomData)
350 }
351
352 pub(crate) fn into_inner(self) -> Transition {
353 self.0
354 }
355
356 /// Overrides the concurrency model for work scheduled by this transition.
357 pub fn concurrency_model(self, concurrency_model: ConcurrencyModel) -> Self {
358 Self::new(self.0.concurrency_model(concurrency_model))
359 }
360}
361
362impl<To: TaskNode + ?Sized> From<MarkedTransition<To>> for Transition {
363 fn from(marked_transition: MarkedTransition<To>) -> Self {
364 marked_transition.into_inner()
365 }
366}
367
368/// A typed fan-out transition builder that must be connected to a join before it can run.
369///
370/// Build one with [`Transition::fan_out`] and add additional branch targets with
371/// [`FanOutTransition::and`]. Each branch input is type-checked before the branch target is erased
372/// for runtime scheduling.
373///
374/// Fan-out always represents a joined branch group. True unjoined fan-out with multiple
375/// independent task endings is not supported; model that shape by sending each branch to a join
376/// node and finishing from the join node.
377#[derive(Debug)]
378#[must_use]
379pub struct FanOutTransition {
380 targets: Vec<NextNode>,
381 concurrency_model: Option<ConcurrencyModel>,
382}
383
384impl FanOutTransition {
385 /// Adds another typed branch target to this fan-out.
386 ///
387 /// The branch input must convert into the target node's input type.
388 pub fn and<T, Input>(mut self, node_id: &NodeId<T>, context: Input) -> Self
389 where
390 T: TaskNode + ?Sized,
391 Input: Into<T::Input>,
392 {
393 self.targets.push(NextNode::new(*node_id, context.into()));
394 self
395 }
396
397 /// Attaches every branch from this fan-out to the provided join target.
398 ///
399 /// This defines the branch group and the join node that waits for every branch in that group.
400 /// Each branch still needs its own registered transition to the same join target when it is
401 /// ready to contribute its output. If branches are conceptually terminal, register each branch
402 /// to transition to this join target and finish from the join node.
403 pub fn join_with<J>(self, join_target: J) -> Transition
404 where
405 J: JoinDestination,
406 {
407 Transition {
408 action: TransitionAction::FanOut {
409 targets: self.targets,
410 join: join_target.into_definition(),
411 },
412 concurrency_model: self.concurrency_model,
413 }
414 }
415
416 /// Overrides the concurrency model for work scheduled by this fan-out.
417 pub fn concurrency_model(mut self, concurrency_model: ConcurrencyModel) -> Self {
418 self.concurrency_model = Some(concurrency_model);
419 self
420 }
421}
422
423#[derive(Debug)]
424pub(crate) enum TransitionAction {
425 Next(NextNode),
426 FanOut {
427 targets: Vec<NextNode>,
428 join: JoinDefinition,
429 },
430 Pause,
431 Error(Box<dyn std::error::Error + Send + Sync>),
432 Finish(Arc<dyn Any + Send + Sync>),
433}
434
435impl Transition {
436 pub(crate) fn from_next_node(next_node: NextNode) -> Self {
437 Self {
438 action: TransitionAction::Next(next_node),
439 concurrency_model: None,
440 }
441 }
442
443 /// Continues execution at `node_id` with the provided input.
444 ///
445 /// # Examples
446 ///
447 /// ```no_run
448 /// use swiftide_tasks::{NodeError, Task, Transition};
449 ///
450 /// let mut task = Task::<i32, i32>::new();
451 /// let start = task.register_node_fn(|input: &i32| -> Result<i32, NodeError> { Ok(*input + 1) });
452 /// let finish = task.register_node_fn(|input: &i32| -> Result<i32, NodeError> { Ok(*input * 2) });
453 ///
454 /// task.starts_with(start);
455 /// task.register_transition(start, move |value| Transition::next(&finish, value))?;
456 /// # Ok::<(), Box<dyn std::error::Error>>(())
457 /// ```
458 pub fn next<T: TaskNode + ?Sized>(
459 node_id: &NodeId<T>,
460 context: T::Input,
461 ) -> MarkedTransition<T> {
462 MarkedTransition::new(Self::from_next_node(NextNode::new(*node_id, context)))
463 }
464
465 /// Builds a fan-out that schedules one or more branches from the current node output.
466 ///
467 /// The returned [`FanOutTransition`] must attach an explicit join with
468 /// [`FanOutTransition::join_with`] before it can be returned from a task transition.
469 /// Branches in that fan-out are joined branches, not independent task endings. To express
470 /// multiple logical endings, transition each branch to a join node and finish the task from the
471 /// join node.
472 ///
473 /// # Examples
474 ///
475 /// ```no_run
476 /// use swiftide_tasks::{NodeError, Task, Transition};
477 ///
478 /// let mut task = Task::<i32, i32>::new();
479 /// let start = task.register_node_fn(|input: &i32| -> Result<i32, NodeError> { Ok(*input) });
480 /// let left = task.register_node_fn(|input: &i32| -> Result<i32, NodeError> { Ok(*input + 1) });
481 /// let right = task.register_node_fn(|input: &i32| -> Result<i32, NodeError> { Ok(*input + 2) });
482 ///
483 /// task.starts_with(start);
484 /// let join = task.register_node_fn(
485 /// |input: &swiftide_tasks::JoinInput<i32>| -> Result<i32, NodeError> {
486 /// Ok(input.iter().copied().sum())
487 /// },
488 /// );
489 ///
490 /// task.register_transition(start, move |value| {
491 /// Transition::fan_out(&left, value)
492 /// .and(&right, value)
493 /// .join_with(join.join())
494 /// })?;
495 /// # Ok::<(), Box<dyn std::error::Error>>(())
496 /// ```
497 pub fn fan_out<T, Input>(node_id: &NodeId<T>, context: Input) -> FanOutTransition
498 where
499 T: TaskNode + ?Sized,
500 Input: Into<T::Input>,
501 {
502 FanOutTransition {
503 targets: vec![NextNode::new(*node_id, context.into())],
504 concurrency_model: None,
505 }
506 }
507
508 /// Pauses the current branch.
509 ///
510 /// # Examples
511 ///
512 /// ```no_run
513 /// use swiftide_tasks::{NodeError, Task, TaskRunOutcome, Transition};
514 ///
515 /// # #[tokio::main(flavor = "current_thread")]
516 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
517 /// let mut task = Task::<i32, ()>::new();
518 /// let start = task.register_node_fn(|input: &i32| -> Result<i32, NodeError> { Ok(*input) });
519 ///
520 /// task.starts_with(start);
521 /// task.register_transition(start, |_value| Transition::pause())?;
522 ///
523 /// assert_eq!(task.run(1).await?, TaskRunOutcome::Paused);
524 /// # Ok(())
525 /// # }
526 /// ```
527 pub fn pause() -> Self {
528 Self {
529 action: TransitionAction::Pause,
530 concurrency_model: None,
531 }
532 }
533
534 /// Fails the current branch with the provided error.
535 ///
536 /// # Examples
537 ///
538 /// ```no_run
539 /// use std::io::Error;
540 ///
541 /// use swiftide_tasks::{NodeError, Task, Transition};
542 ///
543 /// # #[tokio::main(flavor = "current_thread")]
544 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
545 /// let mut task = Task::<i32, i32>::new();
546 /// let start = task.register_node_fn(|input: &i32| -> Result<i32, NodeError> { Ok(*input) });
547 ///
548 /// task.starts_with(start);
549 /// task.register_transition(start, |_value| Transition::error(Error::other("boom")))?;
550 ///
551 /// assert!(task.run(1).await.is_err());
552 /// # Ok(())
553 /// # }
554 /// ```
555 pub fn error(error: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
556 Self {
557 action: TransitionAction::Error(error.into()),
558 concurrency_model: None,
559 }
560 }
561
562 pub(crate) fn finish<T: NodeArg>(output: T) -> Self {
563 Self {
564 action: TransitionAction::Finish(Arc::new(output) as Arc<dyn Any + Send + Sync>),
565 concurrency_model: None,
566 }
567 }
568
569 /// Overrides the concurrency model for work scheduled by this transition.
570 pub fn concurrency_model(mut self, concurrency_model: ConcurrencyModel) -> Self {
571 self.concurrency_model = Some(concurrency_model);
572 self
573 }
574}
575
576/// The typed aggregated view of branches that have reached a join node.
577///
578/// `JoinInput<T>` is the normal join input. It is only available when every branch contributes a
579/// `T`, or maps its output into `T` before joining. For mixed payload types, use [`AnyJoinInput`].
580#[derive(Clone)]
581pub struct JoinInput<T: NodeArg> {
582 branches: Vec<Arc<T>>,
583}
584
585impl<T: NodeArg> std::fmt::Debug for JoinInput<T> {
586 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
587 f.debug_struct("JoinInput")
588 .field("payload_type", &std::any::type_name::<T>())
589 .field("len", &self.branches.len())
590 .finish()
591 }
592}
593
594impl<T: NodeArg> JoinInput<T> {
595 pub(crate) fn new(branches: Vec<Arc<T>>) -> Self {
596 Self { branches }
597 }
598
599 /// Iterates over every ready branch payload in stable branch creation order.
600 ///
601 /// This is the usual way to read join payloads.
602 pub fn iter(&self) -> impl Iterator<Item = &T> {
603 self.branches.iter().map(Arc::as_ref)
604 }
605
606 /// Returns the number of branch payloads in this join.
607 pub fn len(&self) -> usize {
608 self.branches.len()
609 }
610
611 /// Returns `true` when this join has no branch payloads.
612 pub fn is_empty(&self) -> bool {
613 self.branches.is_empty()
614 }
615}
616
617/// The type-erased aggregated view of branches that have reached a join node.
618///
619/// Use this for joins that intentionally collect mixed branch payload types. Homogeneous joins
620/// should prefer [`JoinInput<T>`](JoinInput), which is checked when transitions are registered.
621#[derive(Clone)]
622pub struct AnyJoinInput {
623 branches: Vec<ErasedPayload>,
624}
625
626impl std::fmt::Debug for AnyJoinInput {
627 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
628 f.debug_struct("AnyJoinInput")
629 .field("len", &self.branches.len())
630 .finish()
631 }
632}
633
634impl AnyJoinInput {
635 pub(crate) fn new(branches: Vec<ErasedPayload>) -> Self {
636 Self { branches }
637 }
638
639 /// Iterates over every type-erased branch payload in stable branch creation order.
640 pub fn iter_any(&self) -> impl Iterator<Item = &(dyn Any + Send + Sync)> {
641 self.branches.iter().map(Arc::as_ref)
642 }
643
644 /// Iterates over every ready branch payload that can be downcast to `T`.
645 ///
646 /// Values are yielded in stable branch creation order. This is the usual way to read join
647 /// payloads from a mixed join when you only need one payload type.
648 ///
649 /// # Examples
650 ///
651 /// ```no_run
652 /// use swiftide_tasks::{AnyJoinInput, NodeError, Task, Transition};
653 ///
654 /// let mut task = Task::<i32, i32>::new();
655 /// let start = task.register_node_fn(|input: &i32| -> Result<i32, NodeError> { Ok(*input) });
656 /// let left = task.register_node_fn(|input: &i32| -> Result<i32, NodeError> { Ok(*input + 1) });
657 /// let right = task.register_node_fn(|input: &i32| -> Result<i32, NodeError> { Ok(*input + 2) });
658 /// let join = task.register_node_fn(|input: &AnyJoinInput| -> Result<i32, NodeError> {
659 /// Ok(input.iter::<i32>().copied().sum())
660 /// });
661 ///
662 /// task.starts_with(start);
663 /// task.register_transition(start, move |value| {
664 /// Transition::fan_out(&left, value)
665 /// .and(&right, value)
666 /// .join_with(join.join_any())
667 /// })?;
668 /// task.register_transition(left, join.join_any())?;
669 /// task.register_transition(right, join.join_any())?;
670 /// # Ok::<(), Box<dyn std::error::Error>>(())
671 /// ```
672 pub fn iter<T: NodeArg>(&self) -> impl Iterator<Item = &T> {
673 self.iter_any()
674 .filter_map(|value| value.downcast_ref::<T>())
675 }
676
677 /// Returns the number of branch payloads in this join.
678 pub fn len(&self) -> usize {
679 self.branches.len()
680 }
681
682 /// Returns `true` when this join has no branch payloads.
683 pub fn is_empty(&self) -> bool {
684 self.branches.is_empty()
685 }
686}