dbuff
Lock-free shared state for 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:
;
You then run the command and provide a closure for what to do with the result:
.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:
let =
with_coalesce;
spawn;
Read state instantly from anywhere — no locks:
let guard = domain.read;
println!;
Write state without blocking — closures are sent through a channel and batched:
domain.modify;
Commands
Implement the Command trait to define async operations. Each command declares its own Output and Error types:
;
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:
;
Execute a command with .bind(services, rt).exec(...):
domain.bind
.exec
.go_detach;
Command Chains
Chain multiple commands with .exec() for sequential execution:
domain.bind
.exec
.exec
.exec
.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:
// ResolvePath -> ReadFile -> ParseConfig
domain.bind
.exec
.then
.then
.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:
// A command that always fails
;
let handle = domain.bind
.on_error
.exec
.exec_discard // returns Err, causes short-circuit
.exec // skipped
.go;
let flow = handle.await.unwrap;
assert_eq!;
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:
domain.bind
.exec
.tracked
.go;
Then in your render loop, a single domain.read() covers loading, results, and errors:
match &domain.read.search_status
Examples
Only some of the features are mentioned here. The examples/ directory contains detailed, runnable examples covering every feature.