dbuff 0.1.0

Double-buffered state with async command chains, streaming, and keyed task pools for ratatui applications
Documentation
use dbuff::*;
use std::time::Duration;

#[derive(Debug, Clone, Default)]
struct AppData {
    step1_status: TaskStatus<i32>,
    step2_status: TaskStatus<()>,
    error_log: Vec<String>,
}

#[derive(Debug, Clone, wherror::Error)]
#[error("step failed: {reason}")]
struct AppError {
    reason: String,
}

struct Add(i32);

#[async_trait::async_trait]
impl Command<()> for Add {
    type Output = i32;
    type Error = AppError;

    async fn execute(self, _: ()) -> Result<Self::Output, Self::Error> {
        tokio::time::sleep(Duration::from_millis(50)).await;
        Ok(self.0)
    }
}

struct Fail;

#[async_trait::async_trait]
impl Command<()> for Fail {
    type Output = ();
    type Error = AppError;

    async fn execute(self, _: ()) -> Result<Self::Output, Self::Error> {
        tokio::time::sleep(Duration::from_millis(50)).await;
        Err(AppError { reason: "something went wrong".into() })
    }
}

#[tokio::main]
async fn main() {
    let rt = tokio::runtime::Handle::current();
    let (domain, write_handle) =
        SharedDomainData::with_coalesce(AppData::default(), Duration::from_micros(500));
    tokio::spawn(write_handle.run());

    // Show initial Idle state
    println!("step1_status: {:?}", domain.read().step1_status);
    println!("step2_status: {:?}", domain.read().step2_status);
    println!("---");

    // Chain: Add(10) succeeds, Fail errors → step2 tracked shows Error.
    // No separate total field — TaskStatus<i32> holds the value.
    let handle = domain
        .bind((), rt.clone())
        .on_error(|err, d| {
            d.error_log.push(format!("{err}"));
        })
        .exec(Add(10), |_, _: &i32| {})
        .tracked(|d: &mut AppData, status: TaskStatus<i32>| d.step1_status = status)
        .exec_discard(Fail)
        .tracked(|d: &mut AppData, status: TaskStatus<()>| d.step2_status = status)
        .go();

    // Poll step1_status: Pending → Resolved
    loop {
        let state = domain.read();
        match &state.step1_status {
            TaskStatus::Idle => {
                println!("step1_status: idle");
            }
            TaskStatus::Pending => {
                println!("step1_status: pending...");
            }
            TaskStatus::Resolved(v) => {
                println!("step1_status: resolved ({v})");
                break;
            }
            TaskStatus::Error(e) => {
                println!("step1_status: error ({e})");
                panic!("step1 should succeed");
            }
            TaskStatus::Aborted => {
                println!("step1_status: aborted");
                panic!("step1 should succeed");
            }
        }
        tokio::time::sleep(Duration::from_millis(5)).await;
    }

    // Poll step2_status: Pending → Error.
    // The error propagated via .tracked() is the actual AppError from Fail,
    // not a generic "chain error" string.
    loop {
        let state = domain.read();
        match &state.step2_status {
            TaskStatus::Idle => {
                println!("step2_status: idle");
            }
            TaskStatus::Pending => {
                println!("step2_status: pending...");
            }
            TaskStatus::Resolved(()) => {
                println!("step2_status: resolved");
                panic!("step2 should fail");
            }
            TaskStatus::Error(e) => {
                println!("step2_status: error ({e})");
                break;
            }
            TaskStatus::Aborted => {
                println!("step2_status: aborted");
                panic!("step2 should fail with error, not abort");
            }
        }
        tokio::time::sleep(Duration::from_millis(5)).await;
    }

    // Wait for chain to finish
    let flow = handle.await.unwrap();
    assert_eq!(flow, ControlFlow::Break);

    tokio::time::sleep(Duration::from_millis(10)).await;

    // Verify final state — read value directly from TaskStatus
    let state = domain.read();
    println!("---");
    println!("step1_status = {:?}", state.step1_status);
    println!("step2_status = {:?}", state.step2_status);
    println!("error_log = {:?}", state.error_log);

    assert!(state.step1_status.is_resolved());
    assert_eq!(*state.step1_status.resolved().unwrap(), 10);
    assert!(state.step2_status.is_error());
    let err_msg = state.step2_status.error().unwrap().to_string();
    assert!(err_msg.contains("step failed"), "expected actual error, got: {err_msg}");
    assert!(!err_msg.contains("chain error"), "should not be generic 'chain error'");
    assert!(!state.error_log.is_empty());
}