12. Fold Reference
umari::prelude ships a handful of generic folds for the most common state-derivation shapes — “did this happen?”, “what’s the latest value?”, “how many times?”, “which of these two won?”. Reach for these first; only write a custom fold when none of them fit.
| Fold | State | Pick when you ask… |
|---|---|---|
EventFold<E> | EventState<E> | “Give me every occurrence of E” |
LatestEvent<E> | Option<StoredEvent<E>> | “What’s the most recent E?” |
EventCounter<E> | u64 | “How many E have happened?” |
EventToggle<A, B> | ToggleState<A, B> | “Was the last event A or B?” |
SingleEvent<E> | — (an EventSet) | Custom fold that only reads one event type |
EventFold
Collects every occurrence of event type E into a Vec.
State: EventState<E>
#![allow(unused)]
fn main() {
.fold::<EventFold<ShopConnected>>()
.execute(|input, connected| {
if connected.exists() {
let first = &connected.events[0];
// ...
}
Ok(emit![])
})
}
#![allow(unused)]
fn main() {
impl<E: Event> EventState<E> {
pub events: Vec<StoredEvent<E>>;
pub fn exists(&self) -> bool;
}
}
Use when: you need to inspect or aggregate over the full history.
Avoid when: you only need the most recent value — LatestEvent is cheaper.
LatestEvent
Keeps only the most recent E. Each new event replaces the previous.
State: Option<StoredEvent<E>>
#![allow(unused)]
fn main() {
.fold::<LatestEvent<WarrantyPlanUpdated>>()
.execute(|input, latest| {
if let Some(event) = latest {
// event.data is the most recent WarrantyPlanUpdated
}
Ok(emit![])
})
}
Use when: you care about the current value — last Updated, last Connected, etc.
Avoid when: you need the full history.
EventCounter
Counts occurrences of E without storing event data.
State: u64
#![allow(unused)]
fn main() {
.fold::<EventCounter<WarrantySold>>()
.execute(|input, sale_count| {
anyhow::ensure!(
sale_count < MAX_WARRANTIES,
"shop has reached the maximum number of warranties"
);
Ok(emit![/* ... */])
})
}
Use when: you only need a count. Avoid when: you need to inspect individual events.
EventToggle
Tracks which of two opposing events occurred last. Designed for created/deleted, activated/deactivated, archived/unarchived pairs.
State: ToggleState<A, B>
#![allow(unused)]
fn main() {
.fold::<EventToggle<WarrantyPlanArchived, WarrantyPlanUnarchived>>()
.execute(|input, toggle| {
if toggle.is_a() {
// currently archived
} else if toggle.is_b() {
// currently unarchived
} else {
// neither has happened
}
Ok(emit![])
})
}
#![allow(unused)]
fn main() {
impl<A: Event, B: Event> ToggleState<A, B> {
pub last: Option<ToggleSide<A, B>>;
pub fn is_a(&self) -> bool;
pub fn is_b(&self) -> bool;
pub fn as_a(&self) -> Option<&StoredEvent<A>>;
pub fn as_b(&self) -> Option<&StoredEvent<B>>;
}
}
Use when: paired opposing events — archived/unarchived, activated/deactivated, locked/unlocked.
Constraint: A and B must share the same domain ID fields.
SingleEvent
Not a fold — an EventSet shorthand for custom folds that only read a single event type. Use it as type Events = SingleEvent<MyEvent>;.
#![allow(unused)]
fn main() {
#[derive(DomainIds, FromDomainIds)]
pub struct ShopExistsFold {
#[domain_id]
pub shop_id: u64,
}
impl Fold for ShopExistsFold {
type Events = SingleEvent<ShopConnected>;
type State = bool;
fn apply(&self, exists: &mut bool, _event: StoredEvent<ShopConnected>) {
*exists = true;
}
}
}
Custom folds
When none of the built-ins fit, implement Fold yourself:
#![allow(unused)]
fn main() {
#[derive(DomainIds, FromDomainIds)]
pub struct MyFold {
#[domain_id] pub shop_id: u64,
#[from_domain_id(default)]
pub custom_field: String,
}
#[derive(EventSet)]
pub enum MyFoldEvents {
#[scope(shop_id)]
EventA(EventA),
EventB(EventB),
}
#[derive(Default)]
pub struct MyState {
pub a_count: u64,
pub b_count: u64,
pub latest_value: Option<String>,
}
impl Fold for MyFold {
type Events = MyFoldEvents;
type State = MyState;
fn apply(&self, state: &mut MyState, event: StoredEvent<MyFoldEvents>) {
match event.data {
MyFoldEvents::EventA(_) => state.a_count += 1,
MyFoldEvents::EventB(ev) => {
state.b_count += 1;
state.latest_value = Some(ev.some_field);
}
}
}
}
}
#[from_domain_id(default)]
Fields not annotated with #[domain_id] can use #[from_domain_id(default)] to get their default value during fold construction. The fold won’t try to bind these from domain IDs.
Fold composition limit
Commands support up to 12 folds in a single tuple. If you need more, compose multiple folds into a single fold by nesting state types, or split the command into multiple commands.