Expand description
A minimal, durable, append-only log store for serializable records.
ministore is not a state manager. It is a Write-Ahead Log (WAL) engine that provides:
- Durability: every record is written to disk and
fsynced before the write returns. - Replay: the entire log can be read back as a sequence of strongly-typed records.
The caller is responsible for:
- Defining the record type (e.g., mutations, events, commands).
- Applying records to in-memory state.
- Managing concurrency (e.g., via
Arc<RwLock<MiniStore>>).
This design makes ministore ideal for building:
- Event-sourced systems
- State machines with durable logs
- Metadata stores (like Arcella’s component registry)
§Guarantees
- Atomicity: each
append()call writes exactly one record (as one JSON line). - Durability: after
append()returnsOk(()), the record is on stable storage. - Ordering: records are replayed in the exact order they were appended.
- Replay Safety: the journal format includes a magic header to prevent misuse.
§Journal Format
The on-disk journal is a text file in JSONL format:
// MINISTORE JOURNAL v0.1.4
{"Set":{"value":10}}
{"Inc":{"by":5}}- Line 1: magic header (for versioning and validation).
- Line N (N e 2): one JSON-serialized record per line.
The format is human-readable and easy to inspect/debug with standard tools (cat, jq, etc.).
§Segmented Rotation
To prevent unbounded growth, ministore supports segmented WAL rotation:
- When a segment reaches
max_bytes_per_segment, it is renamed tojournal.jsonl.001, etc. - Only up to
max_segmentsfiles are retained. Oldest are deleted automatically. replay()reads all segments in order:.001,.002, …, then activejournal.jsonl.
§Example: Simple Counter
use ministore::MiniStore;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
enum CounterMutation {
Set { value: u32 },
Inc { delta: u32 },
}
#[derive(Default)]
struct Counter {
value: u32,
}
impl Counter {
fn apply(&mut self, mutation: &CounterMutation) {
match mutation {
CounterMutation::Set { value } => self.value = *value,
CounterMutation::Inc { delta } => self.value += *delta,
}
}
}
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let tmp = tempfile::tempdir()?;
let path = tmp.path().join("counter.log");
// 1. Open the store
let mut store = MiniStore::open(&path).await?;
// 2. Append mutations
store.append(&CounterMutation::Set { value: 100 }).await?;
store.append(&CounterMutation::Inc { delta: 25 }).await?;
// 3. Rebuild state from log
let mut counter = Counter::default();
let records: Vec<CounterMutation> = MiniStore::replay(&path).await?;
for record in records {
counter.apply(&record);
}
assert_eq!(counter.value, 125);
Ok(())
}Structs§
- Journal
Stream - A stream over records in a single journal file.
- Mini
Store - A durable, append-only log store for serializable records.
- Mini
Store Options - Configuration for
MiniStorewith support for segmented WAL rotation.