Skip to main content

meerkat_runtime/
input_ledger.rs

1//! InputLedger — in-memory ledger of InputState entries.
2//!
3//! IndexMap<InputId, InputState>.
4
5use indexmap::IndexMap;
6use meerkat_core::lifecycle::InputId;
7
8use crate::input_state::InputState;
9
10/// In-memory ledger tracking InputState for all inputs.
11#[derive(Debug, Default, Clone)]
12pub struct InputLedger {
13    /// InputId → InputState (insertion order preserved).
14    states: IndexMap<InputId, InputState>,
15}
16
17impl InputLedger {
18    /// Create a new empty ledger.
19    pub fn new() -> Self {
20        Self::default()
21    }
22
23    /// Accept a new InputState into the ledger.
24    pub fn accept(&mut self, state: InputState) {
25        self.states.insert(state.input_id.clone(), state);
26    }
27
28    /// Recover an InputState after generated recovery authority retained it.
29    ///
30    /// Recovery retention and idempotency ownership live in the generated
31    /// MeerkatMachine recovery/admission path, not in the ledger.
32    /// Returns `true` if the state was inserted.
33    pub fn recover(&mut self, state: InputState) -> bool {
34        self.states.insert(state.input_id.clone(), state);
35        true
36    }
37
38    /// Get the state of a specific input.
39    pub fn get(&self, input_id: &InputId) -> Option<&InputState> {
40        self.states.get(input_id)
41    }
42
43    /// Remove an input from the ledger.
44    pub fn remove(&mut self, input_id: &InputId) -> Option<InputState> {
45        self.states.shift_remove(input_id)
46    }
47
48    /// Get mutable reference to the state of a specific input.
49    pub fn get_mut(&mut self, input_id: &InputId) -> Option<&mut InputState> {
50        self.states.get_mut(input_id)
51    }
52
53    /// Iterate over all input states. "Active" (non-terminal) filtering must
54    /// happen at the driver level, which has DSL access; the ledger by itself
55    /// carries only shell metadata.
56    pub fn iter(&self) -> impl Iterator<Item = (&InputId, &InputState)> {
57        self.states.iter()
58    }
59
60    /// Number of entries in the ledger.
61    pub fn len(&self) -> usize {
62        self.states.len()
63    }
64
65    /// Check if the ledger is empty.
66    pub fn is_empty(&self) -> bool {
67        self.states.is_empty()
68    }
69}
70
71#[cfg(test)]
72#[allow(clippy::unwrap_used)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn accept_and_retrieve() {
78        let mut ledger = InputLedger::new();
79        let id = InputId::new();
80        let state = InputState::new_accepted(id.clone());
81        ledger.accept(state);
82
83        assert_eq!(ledger.len(), 1);
84        assert!(!ledger.is_empty());
85        let retrieved = ledger.get(&id).unwrap();
86        assert_eq!(retrieved.input_id, id);
87    }
88
89    #[test]
90    fn accept_preserves_idempotency_key_as_metadata_only() {
91        let mut ledger = InputLedger::new();
92
93        let input_id = InputId::new();
94        let mut state = InputState::new_accepted(input_id.clone());
95        state.idempotency_key = Some(crate::identifiers::IdempotencyKey::new("req-123"));
96        ledger.accept(state);
97
98        assert_eq!(ledger.len(), 1);
99        assert_eq!(
100            ledger
101                .get(&input_id)
102                .and_then(|state| state.idempotency_key.as_ref()),
103            Some(&crate::identifiers::IdempotencyKey::new("req-123"))
104        );
105    }
106
107    #[test]
108    fn recover_does_not_interpret_durability() {
109        let mut ledger = InputLedger::new();
110
111        let mut state = InputState::new_accepted(InputId::new());
112        state.durability = Some(crate::input::InputDurability::Ephemeral);
113        assert!(
114            ledger.recover(state),
115            "the ledger inserts rows after machine authority has made the retention decision"
116        );
117        assert_eq!(ledger.len(), 1);
118    }
119}