dbuff 0.1.0

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

#[derive(Debug, Clone, Default)]
struct AppData {
    total: i32,
    _side_effects: Vec<String>,
}

#[derive(Debug, Clone, wherror::Error)]
#[error(debug)]
struct CmdError;

// Tracks how many discard commands actually executed
static DISCARD_COUNT: AtomicUsize = AtomicUsize::new(0);

struct Add(i32);

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

    async fn execute(self, _: ()) -> Result<Self::Output, Self::Error> {
        Ok(self.0)
    }
}

// Fire-and-forget logging command — output will be discarded by the chain
struct Log(String);

#[async_trait::async_trait]
impl Command<()> for Log {
    type Output = String;
    type Error = CmdError;

    async fn execute(self, _: ()) -> Result<Self::Output, Self::Error> {
        DISCARD_COUNT.fetch_add(1, Ordering::SeqCst);
        Ok(self.0)
    }
}

// Fire-and-forget notification command — output will be discarded by the chain
struct Notify(String);

#[async_trait::async_trait]
impl Command<()> for Notify {
    type Output = String;
    type Error = CmdError;

    async fn execute(self, _: ()) -> Result<Self::Output, Self::Error> {
        DISCARD_COUNT.fetch_add(1, Ordering::SeqCst);
        Ok(self.0)
    }
}

#[tokio::main]
async fn main() {
    let rt = tokio::runtime::Handle::current();

    let (domain, write_handle) = SharedDomainData::with_coalesce(
        AppData::default(),
        Duration::from_millis(1),
    );
    tokio::spawn(write_handle.run());

    // Mixed chain: exec → exec_discard → exec_discard → exec
    // The discard commands still execute; their output is simply thrown away
    let handle = domain.bind((), rt.clone())
        .exec(Add(10), |d, v: &i32| d.total += *v)
        .exec_discard(Log("checkpoint reached".into()))
        .exec_discard(Notify("user@host".into()))
        .exec(Add(20), |d, v: &i32| d.total += *v)
        .go();

    let flow = handle.await.unwrap();
    assert_eq!(flow, ControlFlow::Continue);

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

    let guard = domain.read();
    assert_eq!(guard.total, 30);
    assert_eq!(DISCARD_COUNT.load(Ordering::SeqCst), 2);
    println!("total = {}, discard_commands_executed = {}", guard.total, DISCARD_COUNT.load(Ordering::SeqCst));
}