facett_core/
action_bus.rs1use std::collections::VecDeque;
16
17use serde::{Deserialize, Serialize};
18
19#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
23pub struct BusAction {
24 pub seq: u64,
25 pub source: String,
26 pub kind: String,
27 pub payload: serde_json::Value,
28}
29
30#[derive(Clone, Debug, PartialEq)]
33pub enum BusMsg {
34 Dispatch { source: String, kind: String, payload: serde_json::Value },
36 Clear,
38}
39
40#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
43pub struct ActionBus {
44 queue: VecDeque<BusAction>,
45 next_seq: u64,
47}
48
49impl ActionBus {
50 pub fn new() -> Self {
51 Self::default()
52 }
53
54 pub fn dispatch(
57 &mut self,
58 source: impl Into<String>,
59 kind: impl Into<String>,
60 payload: serde_json::Value,
61 ) -> u64 {
62 let seq = self.next_seq;
63 self.next_seq += 1;
64 self.queue.push_back(BusAction { seq, source: source.into(), kind: kind.into(), payload });
65 seq
66 }
67
68 pub fn len(&self) -> usize {
70 self.queue.len()
71 }
72
73 pub fn is_empty(&self) -> bool {
75 self.queue.is_empty()
76 }
77
78 pub fn peek(&self) -> Option<&BusAction> {
80 self.queue.front()
81 }
82
83 pub fn drain(&mut self) -> Vec<BusAction> {
87 self.queue.drain(..).collect()
88 }
89
90 pub fn update(&mut self, msg: BusMsg) -> Option<u64> {
94 match msg {
95 BusMsg::Dispatch { source, kind, payload } => Some(self.dispatch(source, kind, payload)),
96 BusMsg::Clear => {
97 self.queue.clear();
98 None
99 }
100 }
101 }
102
103 pub fn state_json(&self) -> serde_json::Value {
106 serde_json::json!({
107 "pending": self.queue.iter().collect::<Vec<_>>(),
108 "len": self.queue.len(),
109 "next_seq": self.next_seq,
110 })
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 #[test]
119 fn dispatch_stamps_monotonic_seqs_and_preserves_order() {
120 let mut bus = ActionBus::new();
121 let a = bus.dispatch("kanban", "move", serde_json::json!({ "card": "SUPP-1" }));
122 let b = bus.dispatch("gantt", "scrub", serde_json::json!({ "t": 12.0 }));
123 assert_eq!((a, b), (0, 1));
124 assert_eq!(bus.len(), 2);
125 let drained = bus.drain();
126 assert_eq!(drained.iter().map(|x| x.seq).collect::<Vec<_>>(), vec![0, 1], "FIFO order");
127 assert_eq!(drained[0].kind, "move");
128 assert!(bus.is_empty(), "drain empties the queue");
129 }
130
131 #[test]
132 fn seq_keeps_climbing_across_drains() {
133 let mut bus = ActionBus::new();
134 bus.dispatch("a", "x", serde_json::Value::Null);
135 bus.drain();
136 let seq = bus.dispatch("b", "y", serde_json::Value::Null);
137 assert_eq!(seq, 1, "seq is not reset by a drain — ids stay unique");
138 }
139
140 #[test]
141 fn update_dispatch_and_clear() {
142 let mut bus = ActionBus::new();
143 let seq = bus.update(BusMsg::Dispatch {
144 source: "toast".into(),
145 kind: "open".into(),
146 payload: serde_json::json!({ "id": 7 }),
147 });
148 assert_eq!(seq, Some(0));
149 assert_eq!(bus.peek().unwrap().kind, "open");
150 assert_eq!(bus.update(BusMsg::Clear), None);
151 assert!(bus.is_empty(), "clear drops everything");
152 }
153
154 #[test]
155 fn state_json_and_serde_round_trip() {
156 let mut bus = ActionBus::new();
157 bus.dispatch("kanban", "move", serde_json::json!({ "to": "done" }));
158 let s = bus.state_json();
159 assert_eq!(s["len"], 1);
160 assert_eq!(s["pending"][0]["kind"], "move");
161 let json = serde_json::to_string(&bus).unwrap();
162 let back: ActionBus = serde_json::from_str(&json).unwrap();
163 assert_eq!(bus, back);
164 }
165}