curfew 0.0.5

A deadline-aware fork-join model where multiple tasks run in parallel, but only those that finish before a specified absolute clock time are joined and their results collected.
Documentation
use std::any::Any;
use std::fmt;
use std::time::Instant;

/// Identifier assigned to each task managed by `curfew`.
pub type TaskId = u64;

/// Outcome of executing a task.
#[derive(Debug)]
pub enum TaskOutcome<T> {
    /// Task completed successfully and produced a value.
    Completed(T),
    /// Task panicked during execution.
    Panicked(PanicInfo),
}

impl<T> TaskOutcome<T> {
    /// Maps the success value using the provided function, keeping panic data as-is.
    pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> TaskOutcome<U> {
        match self {
            TaskOutcome::Completed(value) => TaskOutcome::Completed(f(value)),
            TaskOutcome::Panicked(info) => TaskOutcome::Panicked(info),
        }
    }
}

/// Metadata about a task that completed before the deadline.
#[derive(Debug)]
pub struct TaskResult<T> {
    /// Identifier assigned at spawn time.
    pub task_id: TaskId,
    /// Instant when the task was started (when known).
    pub started_at: Option<Instant>,
    /// Instant when the task finished.
    pub finished_at: Instant,
    /// Execution outcome (`Ok` or panic information).
    pub outcome: TaskOutcome<T>,
}

impl<T> TaskResult<T> {
    /// Returns the wall-clock duration the task ran for when both timestamps are known.
    pub fn duration(&self) -> Option<std::time::Duration> {
        self.started_at
            .map(|start| self.finished_at.saturating_duration_since(start))
    }
}

/// Metadata describing a task that did not complete before the deadline.
#[derive(Debug)]
pub struct TimedOutTask {
    /// Identifier assigned at spawn time.
    pub task_id: TaskId,
    /// Instant when the task was started.
    pub started_at: Option<Instant>,
}

impl TimedOutTask {
    /// Returns the elapsed wall-clock time since the task started.
    pub fn elapsed(&self) -> Option<std::time::Duration> {
        self.started_at
            .map(|start| Instant::now().saturating_duration_since(start))
    }
}

/// Captured information about a panic.
#[derive(Clone)]
pub struct PanicInfo {
    message: String,
}

impl PanicInfo {
    /// Creates panic metadata from the original payload.
    pub fn from_payload(payload: Box<dyn Any + Send + 'static>) -> Self {
        if let Some(s) = payload.downcast_ref::<&str>() {
            Self { message: s.to_string() }
        } else if let Some(s) = payload.downcast_ref::<String>() {
            Self { message: s.clone() }
        } else {
            Self { message: "<non-string panic payload>".to_string() }
        }
    }

    /// Human-readable panic message.
    pub fn message(&self) -> &str {
        &self.message
    }
}

impl fmt::Debug for PanicInfo {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("PanicInfo").field("message", &self.message).finish()
    }
}

impl fmt::Display for PanicInfo {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.message)
    }
}

impl std::error::Error for PanicInfo {}