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}