dbuff 0.1.0

Double-buffered state with async command chains, streaming, and keyed task pools for ratatui applications
Documentation
# dbuff

[![Crates.io](https://img.shields.io/crates/v/dbuff.svg)](https://crates.io/crates/dbuff)
[![License: LGPL-3.0-or-later](https://img.shields.io/badge/License-LGPL--3.0--or--later-blue.svg)](https://opensource.org/license/lgpl-3-0)
[![Repository](https://img.shields.io/badge/repository-GitHub-black)](https://github.com/jayson-lennon/dbuff)

Lock-free shared state for [ratatui](https://github.com/ratatui/ratatui) applications. UI threads read snapshots instantly while background workers apply updates asynchronously.

`dbuff` provides `SharedDomainData<D>` — an `ArcSwap`-backed double buffer. Reads via `.read()` return a guard that dereferences to `&D` without any lock. Writes go through a channel and are batched by a coalescing worker, so the UI never blocks. On top of that, `dbuff` ships composable async command chains, stream consumers, and a keyed task pool — all wired to write results back into your domain data.

## Concepts

The core idea: you run an async _command_ and provide a closure that says what to do with the _result_. The crate handles the lifecycle including spawning the task, awaiting the output, and calling your closure when it's done.

That closure is where you decide how to signal the result to readers. The most common approach is to write it into shared state via `SharedDomainData`, but you could also send it through a channel or anything else. The crate is generic over signalling mechanism via closure.

A _command_ is any type that implements the `Command` trait. The generic parameter is a services type — config, API clients, or anything the command needs. Use `()` if you don't need any:

```rust
struct FetchUser(i32);

#[async_trait::async_trait]
impl Command<ApiClient> for FetchUser {
    type Output = User;
    type Error = AppError;

    async fn execute(self, client: ApiClient) -> Result<User, AppError> {
        client.get_user(self.0).await
    }
}
```

You then run the command and provide a closure for what to do with the result:

```text
.exec(FetchUser(42), |shared, user| { /* do something with user */ })
```

`FetchUser(42)` runs asynchronously with access to the `ApiClient`. When it finishes, the closure receives the output and a mutable reference to your shared state. You can then update the shared state with the user information which can then be immediately displayed in a TUI. Composing multiple commands into chains, tracking their status, or running them in parallel all use the same pattern.

## Setup

Define your app state. Create a `SharedDomainData` and spawn its write handle as a background task:

```rust
#[derive(Clone, Default)]
struct AppData {
    counter: i32,
}

let (domain, write_handle) =
    SharedDomainData::with_coalesce(AppData::default(), Duration::from_micros(500));
tokio::spawn(write_handle.run());
```

Read state instantly from anywhere — no locks:

```rust
let guard = domain.read();
println!("counter = {}", guard.counter);
```

Write state without blocking — closures are sent through a channel and batched:

```rust
domain.modify(|shared| shared.counter += 1);
```

## Commands

Implement the `Command` trait to define async operations. Each command declares its own `Output` and `Error` types:

```rust
struct Add(i32);

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

    async fn execute(self, _: ()) -> Result<i32, CmdError> {
        Ok(self.0)
    }
}
```

The second argument to `Command` is a services type. You can pass config, API clients, or any shared dependencies. Use `()` if you don't need any:

```rust
struct Greet(String);

#[async_trait::async_trait]
impl Command<AppServices> for Greet {
    type Output = String;
    type Error = CmdError;

    async fn execute(self, services: AppServices) -> Result<String, CmdError> {
        Ok(format!("{} {}", services.prefix, self.0))
    }
}
```

Execute a command with `.bind(services, rt).exec(...)`:

```rust
domain.bind((), rt)
    .exec(Add(10), |shared, v: &i32| shared.counter += *v)
    .go_detach();
```

## Command Chains

Chain multiple commands with `.exec()` for sequential execution:

```rust
domain.bind((), rt)
    .exec(Add(10), |shared, v: &i32| shared.total += *v)
    .exec(Add(20), |shared, v: &i32| shared.total += *v)
    .exec(Add(5),  |shared, v: &i32| shared.total += *v)
    .go();
```

### Pipelines with `.then()`

Use `.then()` to pass the previous command's output into the next. Build multi-step pipelines where each step feeds the one after it:

```rust
// ResolvePath -> ReadFile -> ParseConfig
domain.bind((), rt)
    .exec(ResolvePath("config.toml".into()), |shared, path: &PathBuf| shared.config_path = path.clone())
    .then(
        |path: &PathBuf| ReadFile(path.clone()),
        |shared, contents: &String| shared.raw_config = contents.clone(),
    )
    .then(
        |contents: &String| ParseConfig(contents.clone()),
        |shared, config: &Config| shared.config = config.clone(),
    )
    .go();
```

### Error handling

Chains short-circuit on the first error. Use `.on_error()` to capture it. The returned `ControlFlow` tells you whether the chain completed or was interrupted:

```rust
// A command that always fails
struct Fail;

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

    async fn execute(self, _: ()) -> Result<(), CmdError> {
        Err(CmdError)
    }
}

let handle = domain.bind((), rt)
    .on_error(|err, shared| shared.error_log.push(format!("{err}")))
    .exec(Add(10), |shared, v: &i32| shared.total += *v)
    .exec_discard(Fail)          // returns Err, causes short-circuit
    .exec(Add(5), |shared, v: &i32| shared.total += *v)  // skipped
    .go();

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

## Status Tracking

Use `.tracked()` to store a command's `TaskStatus` directly in domain state. The resolved value lives inside the `TaskStatus`, so you don't need a separate field to hold the result:

```rust
#[derive(Clone, Default)]
struct AppData {
    search_status: TaskStatus<Vec<String>>,
}

domain.bind((), rt)
    .exec(SearchFiles("query".into()), |_, _| {})
    .tracked(|shared, status| shared.search_status = status)
    .go();
```

Then in your render loop, a single `domain.read()` covers loading, results, and errors:

```rust
match &domain.read().search_status {
    TaskStatus::Idle => {},                      // not started
    TaskStatus::Pending => {},                   // show spinner
    TaskStatus::Resolved(results) => {},         // render results
    TaskStatus::Error(e) => {},                  // show error
    TaskStatus::Aborted => {},                   // show cancelled
}
```

## Examples

Only some of the features are mentioned here. The [`examples/`](./examples) directory contains detailed, runnable examples covering every feature.

## License

[LGPL-3.0-or-later](https://www.gnu.org/licenses/lgpl-3.0.en.html)