Skip to main content

xs/processor/lifecycle/
slots.rs

1use scru128::Scru128Id;
2
3/// Two-slot compaction state for a single `<kind>.<name>` lifecycle.
4///
5/// Tracks the latest known-good `create` (`confirmed`) and the latest
6/// untested `create` (`pending`). See ADR 0005 for the state transition
7/// table this implements.
8#[derive(Debug, Clone, Default, PartialEq, Eq)]
9pub struct Slots {
10    confirmed: Option<Scru128Id>,
11    pending: Option<Scru128Id>,
12}
13
14/// One lifecycle event the state machine consumes. Maps 1:1 to the
15/// topic vocabulary in ADR 0005.
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum Event {
18    /// User-appended `create` for this kind/name.
19    Create { id: Scru128Id },
20    /// Runtime emitted `active`; `source` is the `create`'s id.
21    Active { source: Scru128Id },
22    /// Runtime emitted `invalid` (failed to init); `source` is the
23    /// `create`'s id.
24    Invalid { source: Scru128Id },
25    /// User-appended `term`. Clears both slots.
26    Term,
27    /// Runtime emitted any of `fin.error` / `fin.ok` / `fin.term`.
28    /// Clears both slots.
29    Fin,
30    /// Runtime emitted `replaced`. No effect on slots (the replacement's
31    /// `active` will overwrite `confirmed` once it arrives).
32    Replaced,
33    /// Runtime emitted `stopped` (xs.stopping ack). No effect on slots.
34    Stopped,
35}
36
37/// The dispatcher's decision at threshold.
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub enum ThresholdPick {
40    /// Nothing to start.
41    None,
42    /// Try `id`. On parse-fail, fall back to `fallback` if `Some`.
43    Start {
44        id: Scru128Id,
45        fallback: Option<Scru128Id>,
46    },
47}
48
49impl Slots {
50    pub fn new() -> Self {
51        Self::default()
52    }
53
54    /// Apply one event to the state.
55    pub fn apply(&mut self, event: Event) {
56        match event {
57            Event::Create { id } => {
58                self.pending = Some(id);
59            }
60            Event::Active { source } => {
61                self.confirmed = Some(source);
62                if self.pending == Some(source) {
63                    self.pending = None;
64                }
65            }
66            Event::Invalid { source } => {
67                if self.pending == Some(source) {
68                    self.pending = None;
69                }
70            }
71            Event::Term | Event::Fin => {
72                self.confirmed = None;
73                self.pending = None;
74            }
75            Event::Replaced | Event::Stopped => {}
76        }
77    }
78
79    /// Replay a sequence of events from an empty state.
80    pub fn replay<I: IntoIterator<Item = Event>>(events: I) -> Self {
81        let mut s = Self::new();
82        for e in events {
83            s.apply(e);
84        }
85        s
86    }
87
88    /// The dispatcher's pick at threshold:
89    ///
90    /// * `pending` set: try it; if it parse-fails, fall back to `confirmed`.
91    /// * no `pending`, `confirmed` set: start `confirmed` (no fallback).
92    /// * both empty: nothing to start.
93    pub fn threshold(&self) -> ThresholdPick {
94        match (self.pending, self.confirmed) {
95            (Some(p), c) => ThresholdPick::Start { id: p, fallback: c },
96            (None, Some(c)) => ThresholdPick::Start {
97                id: c,
98                fallback: None,
99            },
100            (None, None) => ThresholdPick::None,
101        }
102    }
103
104    pub fn confirmed(&self) -> Option<Scru128Id> {
105        self.confirmed
106    }
107
108    pub fn pending(&self) -> Option<Scru128Id> {
109        self.pending
110    }
111}