Quick Start
The shortest path from nothing to a running store, with a pointer at each step to the section that covers it properly.
Every import below is in the prelude, and an ordinary program takes it whole:
use amethystate::prelude::*;It carries what declaring, opening, reading and writing need — including
StoreExt and StoreBackend, which are traits, and without which a store
looks like it has no get and no save_now. The imports are spelled out in the
snippets that follow so each one says where its pieces come from.
Declare the state
Section titled “Declare the state”One attribute turns a struct’s fields into persisted reactive ones. prefix
says where in the store they live.
use amethystate::amethystate;
#[amethystate(prefix = "network")]pub struct NetworkState { #[amestate(default = "127.0.0.1".to_string())] pub host: String,
#[amestate(default = 8080u16)] pub port: u16,}Defaults, nested structs, volatile fields, read policies, and serde interaction: Defining structs.
Open the store
Section titled “Open the store”let store = StoreBuilder::new(settings) .disk(|d| d.debounce(Duration::from_millis(500))) .build()?;
let state = NetworkState::new_with(&store)?;new_with takes a store you hold. Opening one for the whole process instead,
letting the platform decide where the file goes, what the extension becomes,
and how closing reports a failure: Opening a store.
Timing, retries and what the store refuses to hold: Configuring a store. Which engine holds the file: Installation.
Read, write, subscribe
Section titled “Read, write, subscribe”println!("{}", state.host().get());
let _sub = state.port().subscribe(|port| { println!("port changed to {port}");});
state.port().set(9090)?;A write reaches memory at once and the disk on the debounce. Delivering callbacks on your own thread, filtering out your own writes, and what a subscription costs: Subscriptions.
To wait for the disk instead of the debounce: Durability.
Keys you do not know at compile time
Section titled “Keys you do not know at compile time”A map stores each entry at its own path, so entries can be added and observed one at a time.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]pub struct AlertThresholds { pub warning: u64, pub critical: u64,}
#[amethystate(prefix = "sys")]pub struct SystemSettings { #[amestate(default = { "cpu": AlertThresholds { warning: 70, critical: 90 }, "mem": AlertThresholds { warning: 80, critical: 95 } })] pub limits: ReactiveMap<String, AlertThresholds>,}state.limits().insert( "gpu".to_string(), &AlertThresholds { warning: 60, critical: 85, },)?;
let cpu = state.limits().get("cpu");
for (key, value) in state.limits().entries() { println!("{key}: {value:?}");}
let _sub = state.limits().subscribe_any(|change| { println!("{change:?}");});entries() walks in the store’s own order, which is the order of the names the
keys borrow. A key that is not a string goes through Id, and sorts by the
spelling Id holds - so Id<u16> comes back 10, 100, 9.
Paths decided entirely at run time need no struct: Kv holds them. And where one such path wants watching, ReactiveCell gives you over it what a field gives you over a declared one - a read, a write and a subscription.
When the struct changes
Section titled “When the struct changes”Bumping a struct’s version and declaring the steps between versions is how
data written by an older build is brought forward. A field renamed or retyped
without a bump is reported as drift and startup continues.
Open the store with migrate whenever #[migrate] is in the binary: build
runs no step at all.
Persistent-only mode
Section titled “Persistent-only mode”For frameworks that own their update loop - egui, iced, ratatui - mode = "persistent" makes the fields plain Rust values on a plain struct, saved when
you say so. Nothing is reactive, and the struct does not see changes made
elsewhere.
#[amethystate(prefix = "kept", mode = "persistent")]pub struct KeptSettings { #[amestate(default = "127.0.0.1".to_string())] pub host: String,
#[amestate(default = 8080u16)] pub port: u16,}let mut state = KeptSettings::load_with(&store)?;
state.port = 9090;state.save()?;
state.mutate(|d| { d.host = "0.0.0.0".to_string(); d.port = 443;})?;save_lazy and mutate_lazy are the same two writes with the flush left to
the debouncer.
What else there is
Section titled “What else there is”- Interceptors - a callback that sees a write before it lands and may rewrite or refuse it: Subscriptions.
- Tracing - structured events, each write tagged with the struct that made it: Observability.
- Framework integrations - Tauri with TypeScript bindings, Leptos, Dioxus, Yew, GPUI: Integrations.