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    /// Effective scope at publication; never recomputed from later row state.
10    pub row_scope: Option<crate::row_scope::RowScope>,
11    pub key: String,
12    pub entity: String,
13    pub payload: Arc<Bytes>,
14}
15
16/// The latest frame published to one state key, and how many frames the key
17/// has published in total.
18///
19/// A state bus keeps only the latest frame, so frames published faster than a
20/// subscriber reads them are overwritten. The count lets the subscriber tell:
21/// if it moved by more than one since its last read, frames were skipped.
22#[derive(Debug, Clone, Default)]
23pub struct StateUpdate {
24    pub published: u64,
25    /// `published` as of the latest frame that replaced the entity instead
26    /// of patching it: an `upsert` or a `delete`. If it is past a
27    /// subscriber's last read, a client's copy may hold fields the entity no
28    /// longer has, which no patch removes. Zero until one is published.
29    pub replaced: u64,
30    pub payload: Arc<Bytes>,
31    /// Effective scope of `payload` at publication, like [`BusMessage::row_scope`].
32    pub row_scope: Option<crate::row_scope::RowScope>,
33}
34
35/// Whether a state frame replaces its entity (`upsert`, `delete`) rather than
36/// patching it. A frame that does not parse is taken for a patch.
37fn replaces_entity(frame: &[u8]) -> bool {
38    #[derive(serde::Deserialize)]
39    struct Frame<'a> {
40        #[serde(borrow)]
41        op: std::borrow::Cow<'a, str>,
42    }
43    serde_json::from_slice::<Frame<'_>>(frame)
44        .is_ok_and(|frame| matches!(frame.op.as_ref(), "upsert" | "delete"))
45}
46
47#[derive(Clone)]
48#[allow(clippy::type_complexity)]
49pub struct BusManager {
50    state_buses: Arc<RwLock<HashMap<(String, String), watch::Sender<StateUpdate>>>>,
51    list_buses: Arc<RwLock<HashMap<String, broadcast::Sender<Arc<BusMessage>>>>>,
52    broadcast_capacity: usize,
53}
54
55impl BusManager {
56    pub fn new() -> Self {
57        Self::with_capacity(1000)
58    }
59
60    pub fn with_capacity(capacity: usize) -> Self {
61        Self {
62            state_buses: Arc::new(RwLock::new(HashMap::new())),
63            list_buses: Arc::new(RwLock::new(HashMap::new())),
64            broadcast_capacity: capacity,
65        }
66    }
67
68    /// Get or create a state bus (latest-value semantics)
69    /// Each (view_id, key) pair gets its own watch channel
70    pub async fn get_or_create_state_bus(
71        &self,
72        view_id: &str,
73        key: &str,
74    ) -> watch::Receiver<StateUpdate> {
75        let mut buses = self.state_buses.write().await;
76        let entry = (view_id.to_string(), key.to_string());
77
78        let tx = buses
79            .entry(entry)
80            .or_insert_with(|| watch::channel(StateUpdate::default()).0)
81            .clone();
82
83        tx.subscribe()
84    }
85
86    pub async fn get_or_create_list_bus(
87        &self,
88        view_id: &str,
89    ) -> broadcast::Receiver<Arc<BusMessage>> {
90        let mut buses = self.list_buses.write().await;
91
92        let tx = buses
93            .entry(view_id.to_string())
94            .or_insert_with(|| broadcast::channel(self.broadcast_capacity).0)
95            .clone();
96
97        tx.subscribe()
98    }
99
100    /// Publish to a state bus (latest-value). `row_scope` is the frame's scope
101    /// at publication; subscribers authorise the frame against it, never
102    /// against later row state.
103    pub async fn publish_state(
104        &self,
105        view_id: &str,
106        key: &str,
107        frame: Arc<Bytes>,
108        row_scope: Option<crate::row_scope::RowScope>,
109    ) {
110        let buses = self.state_buses.read().await;
111        if let Some(tx) = buses.get(&(view_id.to_string(), key.to_string())) {
112            let replaces = replaces_entity(&frame);
113            tx.send_modify(|update| {
114                update.published += 1;
115                if replaces {
116                    update.replaced = update.published;
117                }
118                update.payload = frame;
119                update.row_scope = row_scope;
120            });
121        }
122    }
123
124    pub async fn publish_list(&self, view_id: &str, message: Arc<BusMessage>) {
125        let buses = self.list_buses.read().await;
126        if let Some(tx) = buses.get(view_id) {
127            let _ = tx.send(message);
128        }
129    }
130
131    pub async fn cleanup_stale_state_buses(&self) -> usize {
132        let mut buses = self.state_buses.write().await;
133        let before = buses.len();
134
135        buses.retain(|_, tx| tx.receiver_count() > 0);
136
137        before - buses.len()
138    }
139
140    pub async fn cleanup_stale_list_buses(&self) -> usize {
141        let mut buses = self.list_buses.write().await;
142        let before = buses.len();
143
144        buses.retain(|_, tx| tx.receiver_count() > 0);
145
146        before - buses.len()
147    }
148
149    pub async fn bus_counts(&self) -> (usize, usize) {
150        let state_count = self.state_buses.read().await.len();
151        let list_count = self.list_buses.read().await.len();
152        (state_count, list_count)
153    }
154}
155
156impl Default for BusManager {
157    fn default() -> Self {
158        Self::new()
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    fn frame(op: &str) -> Arc<Bytes> {
167        Arc::new(Bytes::from(format!(
168            r#"{{"mode":"state","entity":"Round/state","op":"{op}","key":"7","data":{{}}}}"#
169        )))
170    }
171
172    /// The bus remembers where the latest whole replacement (an upsert or a
173    /// delete) sits among the frames it published, so a subscriber that
174    /// skipped frames can tell whether a patch will do.
175    #[tokio::test]
176    async fn a_state_bus_records_its_latest_replacing_frame() {
177        let bus = BusManager::new();
178        let receiver = bus.get_or_create_state_bus("Round/state", "7").await;
179        bus.publish_state("Round/state", "7", frame("patch"), None)
180            .await;
181        assert_eq!(receiver.borrow().replaced, 0);
182        bus.publish_state("Round/state", "7", frame("upsert"), None)
183            .await;
184        bus.publish_state("Round/state", "7", frame("patch"), None)
185            .await;
186        assert_eq!(receiver.borrow().replaced, 2);
187        bus.publish_state("Round/state", "7", frame("delete"), None)
188            .await;
189        assert_eq!(receiver.borrow().replaced, 4);
190        assert_eq!(receiver.borrow().published, 4);
191        bus.publish_state(
192            "Round/state",
193            "7",
194            Arc::new(Bytes::from_static(b"not json")),
195            None,
196        )
197        .await;
198        assert_eq!(receiver.borrow().replaced, 4, "unparsed frames are patches");
199    }
200}