Skip to main content

ready_active_safe/
journal.rs

1//! Transition recording and deterministic replay.
2//!
3//! The journal is an observer: it records what happened, but it does not
4//! influence decisions or modify modes.
5//!
6//! This module is intentionally in-memory only. Persistence is an adapter
7//! concern and belongs outside the core crate.
8
9use alloc::vec::Vec;
10
11use crate::Machine;
12
13/// A record of one processed event.
14///
15/// A record captures the before/after modes and the emitted commands so a system
16/// can be audited or replayed deterministically.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct TransitionRecord<M, E, C> {
19    /// Sequential step index (0-based) within the journal.
20    pub step: usize,
21    /// Mode before processing the event.
22    pub from: M,
23    /// Mode after applying the decision.
24    pub to: M,
25    /// The processed event.
26    pub event: E,
27    /// Commands emitted by the machine, in order.
28    pub commands: Vec<C>,
29    /// Timestamp recorded by the journal.
30    pub timestamp: std::time::SystemTime,
31}
32
33impl<M, E, C> TransitionRecord<M, E, C> {
34    /// Creates a new transition record.
35    #[must_use]
36    pub fn new(step: usize, from: M, to: M, event: E, commands: Vec<C>) -> Self {
37        Self {
38            step,
39            from,
40            to,
41            event,
42            commands,
43            timestamp: std::time::SystemTime::now(),
44        }
45    }
46}
47
48/// An in-memory journal of [`TransitionRecord`] values.
49#[derive(Debug, Default)]
50pub struct InMemoryJournal<M, E, C> {
51    records: Vec<TransitionRecord<M, E, C>>,
52}
53
54impl<M, E, C> InMemoryJournal<M, E, C> {
55    /// Creates an empty journal.
56    #[must_use]
57    pub const fn new() -> Self {
58        Self {
59            records: Vec::new(),
60        }
61    }
62
63    /// Returns the number of records in the journal.
64    #[must_use]
65    pub fn len(&self) -> usize {
66        self.records.len()
67    }
68
69    /// Returns `true` if the journal is empty.
70    #[must_use]
71    pub fn is_empty(&self) -> bool {
72        self.records.is_empty()
73    }
74
75    /// Returns a slice of all records.
76    #[must_use]
77    pub fn records(&self) -> &[TransitionRecord<M, E, C>] {
78        &self.records
79    }
80
81    /// Appends a record.
82    pub fn append(&mut self, record: TransitionRecord<M, E, C>) {
83        self.records.push(record);
84    }
85
86    /// Returns the most recent record, if any.
87    #[must_use]
88    pub fn last(&self) -> Option<&TransitionRecord<M, E, C>> {
89        self.records.last()
90    }
91
92    /// Records a step by cloning the provided values.
93    ///
94    /// This is a convenience for runtimes that keep `Mode` and `Event` by
95    /// reference and want to capture an owned record.
96    pub fn record_step(&mut self, from: &M, to: &M, event: &E, commands: &[C])
97    where
98        M: Clone,
99        E: Clone,
100        C: Clone,
101    {
102        let step = self.records.len();
103        self.append(TransitionRecord::new(
104            step,
105            from.clone(),
106            to.clone(),
107            event.clone(),
108            commands.to_vec(),
109        ));
110    }
111
112    /// Returns an iterator over records whose `from` mode matches `mode`.
113    pub fn transitions_from<'a>(
114        &'a self,
115        mode: &'a M,
116    ) -> impl Iterator<Item = &'a TransitionRecord<M, E, C>> + 'a
117    where
118        M: PartialEq,
119    {
120        self.records.iter().filter(move |r| &r.from == mode)
121    }
122
123    /// Returns an iterator over records whose `to` mode matches `mode`.
124    pub fn transitions_to<'a>(
125        &'a self,
126        mode: &'a M,
127    ) -> impl Iterator<Item = &'a TransitionRecord<M, E, C>> + 'a
128    where
129        M: PartialEq,
130    {
131        self.records.iter().filter(move |r| &r.to == mode)
132    }
133
134    /// Replays all records through `machine`, starting from `initial_mode`.
135    ///
136    /// On success, returns the final mode after applying all decisions.
137    ///
138    /// # Errors
139    ///
140    /// Returns a [`ReplayError`] if replay diverges from the recorded history.
141    pub fn replay<Mac>(&self, machine: &Mac, initial_mode: M) -> Result<M, ReplayError>
142    where
143        Mac: Machine<Mode = M, Event = E, Command = C>,
144        M: Clone + PartialEq,
145        C: PartialEq,
146    {
147        self.replay_with_error::<core::convert::Infallible, _>(machine, initial_mode)
148    }
149
150    /// Replays all records through `machine` when the machine uses a non-default error type.
151    ///
152    /// This is identical to [`InMemoryJournal::replay`], but works with machines that are
153    /// implemented as `Machine<Err>` rather than `Machine` (where `Err` is the default
154    /// `core::convert::Infallible`).
155    ///
156    /// # Errors
157    ///
158    /// Returns a [`ReplayError`] if replay diverges from the recorded history.
159    pub fn replay_with_error<Err, Mac>(
160        &self,
161        machine: &Mac,
162        initial_mode: M,
163    ) -> Result<M, ReplayError>
164    where
165        Mac: Machine<Err, Mode = M, Event = E, Command = C>,
166        M: Clone + PartialEq,
167        C: PartialEq,
168    {
169        let mut mode = initial_mode;
170
171        for record in &self.records {
172            if mode != record.from {
173                return Err(ReplayError::new(record.step, ReplayMismatch::FromMode));
174            }
175
176            let decision = machine.decide(&mode, &record.event);
177            let (next_mode, commands) = decision.apply(mode.clone());
178
179            if next_mode != record.to {
180                return Err(ReplayError::new(record.step, ReplayMismatch::ToMode));
181            }
182
183            if commands.as_slice() != record.commands.as_slice() {
184                return Err(ReplayError::new(record.step, ReplayMismatch::Commands));
185            }
186
187            mode = next_mode;
188        }
189
190        Ok(mode)
191    }
192}
193
194/// The type of mismatch encountered during replay.
195///
196/// # Examples
197///
198/// ```
199/// use ready_active_safe::journal::ReplayMismatch;
200///
201/// let m = ReplayMismatch::FromMode;
202/// assert_eq!(m.to_string(), "current mode differs from recorded starting mode");
203/// ```
204#[derive(Debug, Clone, Copy, PartialEq, Eq)]
205#[non_exhaustive]
206pub enum ReplayMismatch {
207    /// The current mode at a step differs from the recorded `from` mode.
208    FromMode,
209    /// The computed next mode differs from the recorded `to` mode.
210    ToMode,
211    /// The computed commands differ from the recorded `commands`.
212    Commands,
213}
214
215impl core::fmt::Display for ReplayMismatch {
216    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
217        match self {
218            Self::FromMode => {
219                write!(f, "current mode differs from recorded starting mode")
220            }
221            Self::ToMode => {
222                write!(f, "computed next mode differs from recorded mode")
223            }
224            Self::Commands => {
225                write!(f, "computed commands differ from recorded commands")
226            }
227        }
228    }
229}
230
231/// An error returned by [`InMemoryJournal::replay`].
232///
233/// # Examples
234///
235/// ```
236/// use ready_active_safe::journal::{ReplayError, ReplayMismatch};
237///
238/// let err = ReplayError::new(3, ReplayMismatch::ToMode);
239/// assert_eq!(err.step(), 3);
240/// assert_eq!(err.mismatch(), ReplayMismatch::ToMode);
241/// assert_eq!(
242///     err.to_string(),
243///     "replay diverged at step 3: computed next mode differs from recorded mode",
244/// );
245/// ```
246#[derive(Debug, Clone, PartialEq, Eq)]
247pub struct ReplayError {
248    step: usize,
249    mismatch: ReplayMismatch,
250}
251
252impl core::fmt::Display for ReplayError {
253    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
254        write!(
255            f,
256            "replay diverged at step {}: {}",
257            self.step, self.mismatch,
258        )
259    }
260}
261
262impl std::error::Error for ReplayError {}
263
264impl ReplayError {
265    /// Creates a new replay error.
266    #[must_use]
267    pub const fn new(step: usize, mismatch: ReplayMismatch) -> Self {
268        Self { step, mismatch }
269    }
270
271    /// Returns the step at which replay diverged.
272    #[must_use]
273    pub const fn step(&self) -> usize {
274        self.step
275    }
276
277    /// Returns the kind of mismatch encountered.
278    #[must_use]
279    pub const fn mismatch(&self) -> ReplayMismatch {
280        self.mismatch
281    }
282}