use std::collections::VecDeque;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct BusAction {
pub seq: u64,
pub source: String,
pub kind: String,
pub payload: serde_json::Value,
}
#[derive(Clone, Debug, PartialEq)]
pub enum BusMsg {
Dispatch { source: String, kind: String, payload: serde_json::Value },
Clear,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct ActionBus {
queue: VecDeque<BusAction>,
next_seq: u64,
}
impl ActionBus {
pub fn new() -> Self {
Self::default()
}
pub fn dispatch(
&mut self,
source: impl Into<String>,
kind: impl Into<String>,
payload: serde_json::Value,
) -> u64 {
let seq = self.next_seq;
self.next_seq += 1;
self.queue.push_back(BusAction { seq, source: source.into(), kind: kind.into(), payload });
seq
}
pub fn len(&self) -> usize {
self.queue.len()
}
pub fn is_empty(&self) -> bool {
self.queue.is_empty()
}
pub fn peek(&self) -> Option<&BusAction> {
self.queue.front()
}
pub fn drain(&mut self) -> Vec<BusAction> {
self.queue.drain(..).collect()
}
pub fn update(&mut self, msg: BusMsg) -> Option<u64> {
match msg {
BusMsg::Dispatch { source, kind, payload } => Some(self.dispatch(source, kind, payload)),
BusMsg::Clear => {
self.queue.clear();
None
}
}
}
pub fn state_json(&self) -> serde_json::Value {
serde_json::json!({
"pending": self.queue.iter().collect::<Vec<_>>(),
"len": self.queue.len(),
"next_seq": self.next_seq,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dispatch_stamps_monotonic_seqs_and_preserves_order() {
let mut bus = ActionBus::new();
let a = bus.dispatch("kanban", "move", serde_json::json!({ "card": "SUPP-1" }));
let b = bus.dispatch("gantt", "scrub", serde_json::json!({ "t": 12.0 }));
assert_eq!((a, b), (0, 1));
assert_eq!(bus.len(), 2);
let drained = bus.drain();
assert_eq!(drained.iter().map(|x| x.seq).collect::<Vec<_>>(), vec![0, 1], "FIFO order");
assert_eq!(drained[0].kind, "move");
assert!(bus.is_empty(), "drain empties the queue");
}
#[test]
fn seq_keeps_climbing_across_drains() {
let mut bus = ActionBus::new();
bus.dispatch("a", "x", serde_json::Value::Null);
bus.drain();
let seq = bus.dispatch("b", "y", serde_json::Value::Null);
assert_eq!(seq, 1, "seq is not reset by a drain — ids stay unique");
}
#[test]
fn update_dispatch_and_clear() {
let mut bus = ActionBus::new();
let seq = bus.update(BusMsg::Dispatch {
source: "toast".into(),
kind: "open".into(),
payload: serde_json::json!({ "id": 7 }),
});
assert_eq!(seq, Some(0));
assert_eq!(bus.peek().unwrap().kind, "open");
assert_eq!(bus.update(BusMsg::Clear), None);
assert!(bus.is_empty(), "clear drops everything");
}
#[test]
fn state_json_and_serde_round_trip() {
let mut bus = ActionBus::new();
bus.dispatch("kanban", "move", serde_json::json!({ "to": "done" }));
let s = bus.state_json();
assert_eq!(s["len"], 1);
assert_eq!(s["pending"][0]["kind"], "move");
let json = serde_json::to_string(&bus).unwrap();
let back: ActionBus = serde_json::from_str(&json).unwrap();
assert_eq!(bus, back);
}
}