Skip to main content

arete_server/
bus.rs

1use bytes::Bytes;
2use std::collections::HashMap;
3use std::sync::Arc;
4use tokio::sync::{broadcast, watch, RwLock};
5
6/// Message sent through the event bus
7#[derive(Debug, Clone)]
8pub struct BusMessage {
9    pub key: String,
10    pub entity: String,
11    pub payload: Arc<Bytes>,
12}
13
14/// The latest frame published to one state key, and how many frames the key
15/// has published in total.
16///
17/// A state bus keeps only the latest frame, so frames published faster than a
18/// subscriber reads them are overwritten. The count lets the subscriber tell:
19/// if it moved by more than one since its last read, frames were skipped.
20#[derive(Debug, Clone, Default)]
21pub struct StateUpdate {
22    pub published: u64,
23    /// `published` as of the latest frame that replaced the entity instead
24    /// of patching it: an `upsert` or a `delete`. If it is past a
25    /// subscriber's last read, a client's copy may hold fields the entity no
26    /// longer has, which no patch removes. Zero until one is published.
27    pub replaced: u64,
28    pub payload: Arc<Bytes>,
29}
30
31/// Whether a state frame replaces its entity (`upsert`, `delete`) rather than
32/// patching it. A frame that does not parse is taken for a patch.
33fn 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    /// Get or create a state bus (latest-value semantics)
65    /// Each (view_id, key) pair gets its own watch channel
66    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    /// Publish to a state bus (latest-value)
97    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    /// The bus remembers where the latest whole replacement (an upsert or a
160    /// delete) sits among the frames it published, so a subscriber that
161    /// skipped frames can tell whether a patch will do.
162    #[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}