Skip to content

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.

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.

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.

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.

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.

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.

Migrations.

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.

  • 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.