1use bytes::Bytes;
2use std::collections::HashMap;
3use std::sync::Arc;
4use tokio::sync::{broadcast, watch, RwLock};
5
6#[derive(Debug, Clone)]
8pub struct BusMessage {
9 pub key: String,
10 pub entity: String,
11 pub payload: Arc<Bytes>,
12}
13
14#[derive(Debug, Clone, Default)]
21pub struct StateUpdate {
22 pub published: u64,
23 pub replaced: u64,
28 pub payload: Arc<Bytes>,
29}
30
31fn replaces_entity(frame: &[u8]) -> bool {
34 #[derive(serde::Deserialize)]
35 struct Frame<'a> {
36 #[serde(borrow)]
37 op: std::borrow::Cow<'a, str>,
38 }
39 serde_json::from_slice::<Frame<'_>>(frame)
40 .is_ok_and(|frame| matches!(frame.op.as_ref(), "upsert" | "delete"))
41}
42
43#[derive(Clone)]
44#[allow(clippy::type_complexity)]
45pub struct BusManager {
46 state_buses: Arc<RwLock<HashMap<(String, String), watch::Sender<StateUpdate>>>>,
47 list_buses: Arc<RwLock<HashMap<String, broadcast::Sender<Arc<BusMessage>>>>>,
48 broadcast_capacity: usize,
49}
50
51impl BusManager {
52 pub fn new() -> Self {
53 Self::with_capacity(1000)
54 }
55
56 pub fn with_capacity(capacity: usize) -> Self {
57 Self {
58 state_buses: Arc::new(RwLock::new(HashMap::new())),
59 list_buses: Arc::new(RwLock::new(HashMap::new())),
60 broadcast_capacity: capacity,
61 }
62 }
63
64 pub async fn get_or_create_state_bus(
67 &self,
68 view_id: &str,
69 key: &str,
70 ) -> watch::Receiver<StateUpdate> {
71 let mut buses = self.state_buses.write().await;
72 let entry = (view_id.to_string(), key.to_string());
73
74 let tx = buses
75 .entry(entry)
76 .or_insert_with(|| watch::channel(StateUpdate::default()).0)
77 .clone();
78
79 tx.subscribe()
80 }
81
82 pub async fn get_or_create_list_bus(
83 &self,
84 view_id: &str,
85 ) -> broadcast::Receiver<Arc<BusMessage>> {
86 let mut buses = self.list_buses.write().await;
87
88 let tx = buses
89 .entry(view_id.to_string())
90 .or_insert_with(|| broadcast::channel(self.broadcast_capacity).0)
91 .clone();
92
93 tx.subscribe()
94 }
95
96 pub async fn publish_state(&self, view_id: &str, key: &str, frame: Arc<Bytes>) {
98 let buses = self.state_buses.read().await;
99 if let Some(tx) = buses.get(&(view_id.to_string(), key.to_string())) {
100 let replaces = replaces_entity(&frame);
101 tx.send_modify(|update| {
102 update.published += 1;
103 if replaces {
104 update.replaced = update.published;
105 }
106 update.payload = frame;
107 });
108 }
109 }
110
111 pub async fn publish_list(&self, view_id: &str, message: Arc<BusMessage>) {
112 let buses = self.list_buses.read().await;
113 if let Some(tx) = buses.get(view_id) {
114 let _ = tx.send(message);
115 }
116 }
117
118 pub async fn cleanup_stale_state_buses(&self) -> usize {
119 let mut buses = self.state_buses.write().await;
120 let before = buses.len();
121
122 buses.retain(|_, tx| tx.receiver_count() > 0);
123
124 before - buses.len()
125 }
126
127 pub async fn cleanup_stale_list_buses(&self) -> usize {
128 let mut buses = self.list_buses.write().await;
129 let before = buses.len();
130
131 buses.retain(|_, tx| tx.receiver_count() > 0);
132
133 before - buses.len()
134 }
135
136 pub async fn bus_counts(&self) -> (usize, usize) {
137 let state_count = self.state_buses.read().await.len();
138 let list_count = self.list_buses.read().await.len();
139 (state_count, list_count)
140 }
141}
142
143impl Default for BusManager {
144 fn default() -> Self {
145 Self::new()
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 fn frame(op: &str) -> Arc<Bytes> {
154 Arc::new(Bytes::from(format!(
155 r#"{{"mode":"state","entity":"Round/state","op":"{op}","key":"7","data":{{}}}}"#
156 )))
157 }
158
159 #[tokio::test]
163 async fn a_state_bus_records_its_latest_replacing_frame() {
164 let bus = BusManager::new();
165 let receiver = bus.get_or_create_state_bus("Round/state", "7").await;
166 bus.publish_state("Round/state", "7", frame("patch")).await;
167 assert_eq!(receiver.borrow().replaced, 0);
168 bus.publish_state("Round/state", "7", frame("upsert")).await;
169 bus.publish_state("Round/state", "7", frame("patch")).await;
170 assert_eq!(receiver.borrow().replaced, 2);
171 bus.publish_state("Round/state", "7", frame("delete")).await;
172 assert_eq!(receiver.borrow().replaced, 4);
173 assert_eq!(receiver.borrow().published, 4);
174 bus.publish_state(
175 "Round/state",
176 "7",
177 Arc::new(Bytes::from_static(b"not json")),
178 )
179 .await;
180 assert_eq!(receiver.borrow().replaced, 4, "unparsed frames are patches");
181 }
182}