Files
dagx/src/node.rs
T

157 lines
5.6 KiB
Rust

//! Internal node types for DAG execution.
//!
//! Provides type erasure to store tasks with different types in a single collection.
//! The public API remains fully type-safe; type erasure is purely an internal implementation detail.
//!
//! - **TypedNode\<T\>**: Stores a task with full type information
//! - **ExecutableNode**: Trait for executing type-erased tasks
use std::any::Any;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use crate::error::DagError;
use crate::types::NodeId;
/// Type-erased listeners (for receiving inputs from dependencies)
type ErasedListeners = Vec<Box<dyn Any + Send>>;
/// Type-erased async execution result (Arc-wrapped for efficient fanout)
type ExecuteFuture =
Pin<Box<dyn Future<Output = Result<Arc<dyn Any + Send + Sync>, DagError>> + Send>>;
/// Type-erased async execution result for TaskExecute trait (returns String errors)
type TaskExecuteFuture =
Pin<Box<dyn Future<Output = Result<Arc<dyn Any + Send + Sync>, String>> + Send>>;
/// Internal trait for executing heterogeneous tasks.
///
/// This trait provides type erasure necessary for storing different task types in a single
/// collection. While the public API (Task, Handle, Node) is fully type-safe, internally we
/// need dynamic dispatch to execute tasks with different input/output types.
///
/// This is the ONLY place where type erasure occurs - announcements are created with full type
/// information and only erased at this trait boundary.
///
/// This is an implementation detail and not part of the public API.
pub(crate) trait ExecutableNode: Send {
/// Create a type-erased announcement for this node's output.
///
/// Returns a tuple of (Announcer, Announcement) both type-erased.
/// The Announcer will be used by this node to broadcast its output.
/// The Announcement can be used to create listeners for dependents.
fn create_announcement(&self) -> (Box<dyn Any + Send>, Box<dyn Any + Send>);
/// Create a type-erased listener from an announcement.
///
/// This allows dependents to listen to this node's output.
fn create_listener(&self, announcement: &dyn Any) -> Box<dyn Any + Send>;
/// Execute the task with type-erased input listeners and an output announcer.
///
/// The TypedNode implementation knows the concrete types and can safely downcast.
/// Consumes the node and returns the output value.
fn execute_with_listeners(
self: Box<Self>,
listeners: ErasedListeners,
announcer: Box<dyn Any + Send>,
) -> ExecuteFuture;
}
/// Fully-typed node storage for a single task.
///
/// Stores a task that has an `execute()` method (generated by the #[task] macro).
/// Implements ExecutableNode for type erasure, allowing heterogeneous tasks to be
/// stored in a single Vec.
///
/// # Ownership Model
///
/// The task is owned directly (no Mutex needed) and consumed during execution.
/// Each task executes exactly once, taking ownership and producing an output.
///
/// # Announcement Broadcasting
///
/// Outputs are communicated via announcement channels for inter-task communication.
/// Each task has ONE announcer that broadcasts to multiple listeners.
/// The output is also returned from execute_with_listeners for central storage.
pub(crate) struct TypedNode<T> {
pub(crate) id: NodeId,
pub(crate) task: T,
pub(crate) _output_type: std::marker::PhantomData<fn() -> ()>,
}
impl<T> TypedNode<T> {
pub(crate) fn new(id: NodeId, task: T) -> Self {
Self {
id,
task,
_output_type: std::marker::PhantomData,
}
}
}
// Implementation for tasks created with the #[task] macro
// These tasks have an execute() method generated by the macro
impl<T: 'static + Send> ExecutableNode for TypedNode<T>
where
T: TaskExecute,
{
fn create_announcement(&self) -> (Box<dyn Any + Send>, Box<dyn Any + Send>) {
T::create_announcement_erased()
}
fn create_listener(&self, announcement: &dyn Any) -> Box<dyn Any + Send> {
T::create_listener_erased(announcement)
}
fn execute_with_listeners(
self: Box<Self>,
listeners: ErasedListeners,
announcer: Box<dyn Any + Send>,
) -> ExecuteFuture {
Box::pin(async move {
// Call the task's execute() method (generated by the #[task] macro)
// This handles listener extraction, calling run(), and announcing
let result = self.task.execute(listeners, announcer).await;
match result {
Ok(output_arc) => Ok(output_arc),
Err(err_msg) => {
// Execution failed - convert error message to DagError
eprintln!("Task execution failed: {}", err_msg);
Err(DagError::InvalidDependency { task_id: self.id.0 })
}
}
})
}
}
/// Trait for tasks that have an execute() method (generated by #[task] macro).
///
/// This trait is automatically implemented by the macro and should not be
/// implemented manually.
pub trait TaskExecute: Send {
/// Input type as a tuple of parameter types (for dependency checking)
/// - `()` for no parameters
/// - `T` for single parameter
/// - `(A, B, C, ...)` for multiple parameters
type Input: Send;
/// Output type of the task
type Output: Send + Sync + Clone;
fn execute(
self,
listeners: Vec<Box<dyn Any + Send>>,
announcer: Box<dyn Any + Send>,
) -> TaskExecuteFuture;
fn create_announcement_erased() -> (Box<dyn Any + Send>, Box<dyn Any + Send>);
fn create_listener_erased(announcement: &dyn Any) -> Box<dyn Any + Send>;
}
#[cfg(test)]
mod tests;