a3s-flow 0.10.1

Durable workflow engine and Rust SDK for A3S
Documentation
use std::fmt;

use thiserror::Error;

/// Crate-local result type.
pub type Result<T> = std::result::Result<T, FlowError>;

/// Errors surfaced by the workflow engine and runtime adapters.
#[derive(Error)]
pub enum FlowError {
    #[error("workflow run not found: {0}")]
    RunNotFound(String),

    #[error("workflow run {0} is already terminal")]
    RunTerminal(String),

    #[error("workflow run id is invalid: {0}")]
    InvalidRunId(String),

    #[error("workflow run {run_id} conflicts with existing run: {reason}")]
    RunConflict { run_id: String, reason: String },

    #[error("non-deterministic workflow replay for run {run_id}: {reason}")]
    NonDeterministic { run_id: String, reason: String },

    #[error(
        "event sequence conflict for run {run_id}: expected {expected_sequence}, actual {actual_sequence}"
    )]
    EventConflict {
        run_id: String,
        expected_sequence: u64,
        actual_sequence: u64,
    },

    /// The original token remains available for programmatic routing, while
    /// `Display` and `Debug` deliberately redact it.
    #[error("active hook token not found (value redacted)")]
    HookTokenNotFound(String),

    #[error("workflow task lease is no longer active: {0}")]
    LeaseLost(String),

    /// The conflicting token remains available for programmatic handling,
    /// while `Display` and `Debug` deliberately redact it.
    #[error(
        "active hook token is already used by run {existing_run_id} hook {existing_hook_id} (value redacted)"
    )]
    HookTokenConflict {
        token: String,
        existing_run_id: String,
        existing_hook_id: String,
    },

    #[error("invalid workflow definition: {0}")]
    InvalidWorkflow(String),

    #[error("invalid state transition: {0}")]
    InvalidTransition(String),

    #[error("invalid worker configuration: {0}")]
    InvalidWorkerConfiguration(String),

    #[error("task manager error: {0}")]
    TaskManagement(String),

    #[error("event store error: {0}")]
    Store(String),

    #[error("runtime error: {0}")]
    Runtime(String),

    #[error("serialization error: {0}")]
    Serialization(#[from] serde_json::Error),

    #[error("io error: {0}")]
    Io(#[from] std::io::Error),

    #[error("workflow replay exceeded {0} iterations")]
    ReplayLimitExceeded(usize),
}

// Error values can retain callback tokens for programmatic recovery, but
// diagnostics must never reveal those bearer credentials. Keep ordinary
// variants structurally useful while replacing token fields in Debug output.
impl fmt::Debug for FlowError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::RunNotFound(run_id) => {
                formatter.debug_tuple("RunNotFound").field(run_id).finish()
            }
            Self::RunTerminal(run_id) => {
                formatter.debug_tuple("RunTerminal").field(run_id).finish()
            }
            Self::InvalidRunId(run_id) => {
                formatter.debug_tuple("InvalidRunId").field(run_id).finish()
            }
            Self::RunConflict { run_id, reason } => formatter
                .debug_struct("RunConflict")
                .field("run_id", run_id)
                .field("reason", reason)
                .finish(),
            Self::NonDeterministic { run_id, reason } => formatter
                .debug_struct("NonDeterministic")
                .field("run_id", run_id)
                .field("reason", reason)
                .finish(),
            Self::EventConflict {
                run_id,
                expected_sequence,
                actual_sequence,
            } => formatter
                .debug_struct("EventConflict")
                .field("run_id", run_id)
                .field("expected_sequence", expected_sequence)
                .field("actual_sequence", actual_sequence)
                .finish(),
            Self::HookTokenNotFound(_) => formatter
                .debug_tuple("HookTokenNotFound")
                .field(&"<redacted>")
                .finish(),
            Self::LeaseLost(lease_id) => {
                formatter.debug_tuple("LeaseLost").field(lease_id).finish()
            }
            Self::HookTokenConflict {
                existing_run_id,
                existing_hook_id,
                ..
            } => formatter
                .debug_struct("HookTokenConflict")
                .field("token", &"<redacted>")
                .field("existing_run_id", existing_run_id)
                .field("existing_hook_id", existing_hook_id)
                .finish(),
            Self::InvalidWorkflow(message) => formatter
                .debug_tuple("InvalidWorkflow")
                .field(message)
                .finish(),
            Self::InvalidTransition(message) => formatter
                .debug_tuple("InvalidTransition")
                .field(message)
                .finish(),
            Self::InvalidWorkerConfiguration(message) => formatter
                .debug_tuple("InvalidWorkerConfiguration")
                .field(message)
                .finish(),
            Self::TaskManagement(message) => formatter
                .debug_tuple("TaskManagement")
                .field(message)
                .finish(),
            Self::Store(message) => formatter.debug_tuple("Store").field(message).finish(),
            Self::Runtime(message) => formatter.debug_tuple("Runtime").field(message).finish(),
            Self::Serialization(error) => {
                formatter.debug_tuple("Serialization").field(error).finish()
            }
            Self::Io(error) => formatter.debug_tuple("Io").field(error).finish(),
            Self::ReplayLimitExceeded(limit) => formatter
                .debug_tuple("ReplayLimitExceeded")
                .field(limit)
                .finish(),
        }
    }
}