Skip to main content

idempotency_guard

Macro idempotency_guard 

Source
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(())
    }
}