macro_rules! idempotency_guard {
($events:expr,
$(already_applied: $pattern:pat $(if $guard:expr)? ,)+
resets_on: $break_pattern:pat $(if $break_guard:expr)? ,
snapshot: $snap_pattern:pat $(if $snap_guard:expr)? $(,)?) => { ... };
($events:expr,
$(already_applied: $pattern:pat $(if $guard:expr)? ,)+
resets_on: $break_pattern:pat $(if $break_guard:expr)? $(,)?) => { ... };
($events:expr,
$(already_applied: $pattern:pat $(if $guard:expr)? ,)+
snapshot: $snap_pattern:pat $(if $snap_guard:expr)? $(,)?) => { ... };
($events:expr,
$(already_applied: $pattern:pat $(if $guard:expr)?),+ $(,)?) => { ... };
}Expand description
Prevent duplicate event processing by checking for idempotent operations.
Guards against replaying the same mutation in event-sourced systems.
Returns AlreadyApplied early if matching events are found, allowing the caller
to skip redundant operations. Use resets_on to allow re-applying after an intervening event.
§Parameters
$events: Event collection to search (usually chronologically reversed)already_applied:One or more event patterns that indicate the operation was already applied. Multiple patterns are supported — each is checked independently.resets_on:Optional event pattern that resets the guard, allowing re-execution. Use Rust’s native or-pattern (P1 | P2) to match multiple reset events.
When iterating events in reverse, if a resets_on event is found before the
already_applied event, the guard allows re-execution. This is useful for
toggle-like operations (freeze/unfreeze) or when a state change should
invalidate a previous idempotency check.
§Examples
use es_entity::{idempotency_guard, Idempotent};
pub enum UserEvent{
Initialized {id: u64, name: String},
NameUpdated {name: String}
}
pub struct User{
events: Vec<UserEvent>
}
impl User{
pub fn update_name(&mut self, new_name: impl Into<String>) -> Idempotent<()>{
let name = new_name.into();
idempotency_guard!(
self.events.iter().rev(),
already_applied: UserEvent::NameUpdated { name: existing_name } if existing_name == &name
);
self.events.push(UserEvent::NameUpdated{name});
Idempotent::Executed(())
}
pub fn update_name_resettable(&mut self, new_name: impl Into<String>) -> Idempotent<()>{
let name = new_name.into();
idempotency_guard!(
self.events.iter().rev(),
already_applied: UserEvent::NameUpdated { name: existing_name } if existing_name == &name,
resets_on: UserEvent::NameUpdated {..}
// if any other NameUpdated happened more recently, allow re-applying
);
self.events.push(UserEvent::NameUpdated{name});
Idempotent::Executed(())
}
}
let mut user1 = User{ events: vec![] };
let mut user2 = User{ events: vec![] };
assert!(user1.update_name("Alice").did_execute());
// updating "Alice" again ignored because same event with same name exists
assert!(user1.update_name("Alice").was_already_applied());
assert!(user2.update_name_resettable("Alice").did_execute());
assert!(user2.update_name_resettable("Bob").did_execute());
// updating "Alice" again works because Bob's NameUpdated resets the guard
assert!(user2.update_name_resettable("Alice").did_execute());§Multiple already_applied patterns
use es_entity::{idempotency_guard, Idempotent};
pub enum ConfigEvent {
Initialized { id: u64 },
Updated { key: String, value: String },
KeyRotated { key: String },
}
pub struct Config {
events: Vec<ConfigEvent>,
}
impl Config {
pub fn apply_change(&mut self, key: String, value: String) -> Idempotent<()> {
idempotency_guard!(
self.events.iter().rev(),
already_applied: ConfigEvent::Updated { key: k, value: v } if k == &key && v == &value,
already_applied: ConfigEvent::KeyRotated { key: k } if k == &key,
resets_on: ConfigEvent::Initialized { .. }
);
self.events.push(ConfigEvent::Updated { key, value });
Idempotent::Executed(())
}
}
let mut config = Config { events: vec![] };
assert!(config.apply_change("k".into(), "v".into()).did_execute());
assert!(config.apply_change("k".into(), "v".into()).was_already_applied());§Snapshotted streams: the snapshot: clause
Iterating .replay() (or .replay_persisted()) on an EntityEvents<E, S>
with a real snapshot type yields Replay items instead
of plain events. already_applied: and resets_on: patterns still match
&E values (wrapped in Replay::Event under the hood via
IntoReplay), so existing call sites over
iter_all().rev() compile unchanged. A stream that can yield
Replay::Snapshot additionally needs a snapshot: clause — placed last,
after resets_on: if present — saying what the snapshot implies about
this operation. Omitting it on such a stream is a compile error: the
macro expands to a call the snapshot’s type does not implement, with a
diagnostic pointing at the missing clause. Use snapshot: _ if false to
say explicitly “the snapshot can never imply this was already applied”.
resets_on semantics fold into the guard the same way: if the reset
condition is itself something the snapshot could already reflect, encode
the resolved state in the snapshot: guard (e.g.
snapshot: s if s.last_threshold_update == Some((lower, upper))) rather
than in a separate clause — the snapshot has no “reset” of its own, it is
always the fold as of its sequence.
use es_entity::*;
use serde::{Serialize, Deserialize};
entity_id! { MeterId }
#[derive(EsSnapshot, Serialize, Deserialize, Debug)]
#[es_snapshot(version = 1)]
pub struct MeterSnapshot { id: MeterId, last_value: Option<i64> }
#[derive(EsEvent, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[es_event(id = "MeterId")]
pub enum MeterEvent {
Initialized { id: MeterId },
ReadingRecorded { value: i64 },
}
pub struct NewMeter { id: MeterId }
impl IntoEvents<MeterEvent> for NewMeter {
fn into_events(self) -> EntityEvents<MeterEvent> {
EntityEvents::init(self.id, [MeterEvent::Initialized { id: self.id }])
}
}
#[derive(EsEntity)]
pub struct Meter {
pub id: MeterId,
events: EntityEvents<MeterEvent, MeterSnapshot>,
}
impl TryFromEvents<MeterEvent, MeterSnapshot> for Meter {
fn try_from_events(
events: EntityEvents<MeterEvent, MeterSnapshot>,
) -> Result<Self, EntityHydrationError> {
let mut id = None;
for r in events.replay() {
match r {
Replay::Snapshot(s) => id = Some(s.id),
Replay::Event(MeterEvent::Initialized { id: i }) => id = Some(*i),
Replay::Event(_) => {}
}
}
Ok(Meter { id: id.expect("Initialized"), events })
}
}
impl Meter {
pub fn record(&mut self, value: i64) -> Idempotent<()> {
idempotency_guard!(
self.events.replay().rev(),
already_applied: MeterEvent::ReadingRecorded { value: v } if *v == value,
snapshot: s if s.last_value == Some(value),
);
self.events.push(MeterEvent::ReadingRecorded { value });
Idempotent::Executed(())
}
}
let id = MeterId::new();
let events = EntityEvents::init(id, [MeterEvent::Initialized { id }]).widen_snapshot();
let mut meter = <Meter as TryFromEvents<_, _>>::try_from_events(events).unwrap();
assert!(meter.record(10).did_execute());
assert!(meter.record(10).was_already_applied());Omitting the snapshot: clause on the same stream does not compile:
impl Meter {
pub fn record(&mut self, value: i64) -> Idempotent<()> {
// error: missing the required `snapshot:` clause.
idempotency_guard!(
self.events.replay().rev(),
already_applied: MeterEvent::ReadingRecorded { value: v } if *v == value,
);
self.events.push(MeterEvent::ReadingRecorded { value });
Idempotent::Executed(())
}
}