cratestack_core/events/
bus.rs1use std::collections::BTreeMap;
8use std::sync::atomic::{AtomicU64, Ordering};
9use std::sync::{Arc, RwLock};
10
11use super::{CoolEventEnvelope, CoolEventFuture, ModelEventKind, event_topic};
12use crate::error::CoolError;
13
14type EventHandler = Arc<dyn Fn(CoolEventEnvelope) -> CoolEventFuture + Send + Sync>;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct SubscriptionHandle {
22 topic: String,
23 id: u64,
24}
25
26#[derive(Clone, Default)]
27pub struct CoolEventBus {
28 handlers: Arc<RwLock<BTreeMap<String, Vec<(u64, EventHandler)>>>>,
29 next_id: Arc<AtomicU64>,
30}
31
32impl std::fmt::Debug for CoolEventBus {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 let handler_count = self
35 .handlers
36 .read()
37 .map(|handlers| handlers.values().map(Vec::len).sum::<usize>())
38 .unwrap_or_default();
39 f.debug_struct("CoolEventBus")
40 .field("handler_count", &handler_count)
41 .finish()
42 }
43}
44
45impl CoolEventBus {
46 pub fn subscribe<F>(
47 &self,
48 model: &'static str,
49 operation: ModelEventKind,
50 handler: F,
51 ) -> SubscriptionHandle
52 where
53 F: Fn(CoolEventEnvelope) -> CoolEventFuture + Send + Sync + 'static,
54 {
55 let topic = event_topic(model, operation);
56 let id = self.next_id.fetch_add(1, Ordering::Relaxed);
57 let mut handlers = self
58 .handlers
59 .write()
60 .expect("event bus handler registry should not be poisoned");
61 handlers
62 .entry(topic.clone())
63 .or_default()
64 .push((id, Arc::new(handler)));
65 SubscriptionHandle { topic, id }
66 }
67
68 pub fn unsubscribe(&self, handle: SubscriptionHandle) {
73 let mut handlers = self
74 .handlers
75 .write()
76 .expect("event bus handler registry should not be poisoned");
77 if let Some(topic_handlers) = handlers.get_mut(&handle.topic) {
78 topic_handlers.retain(|(id, _)| *id != handle.id);
79 }
80 }
81
82 pub async fn emit(&self, envelope: CoolEventEnvelope) -> Result<(), CoolError> {
83 let handlers: Vec<EventHandler> = self
84 .handlers
85 .read()
86 .expect("event bus handler registry should not be poisoned")
87 .get(&event_topic(&envelope.model, envelope.operation))
88 .map(|entries| entries.iter().map(|(_, handler)| handler.clone()).collect())
89 .unwrap_or_default();
90
91 for handler in handlers {
92 handler(envelope.clone()).await?;
93 }
94
95 Ok(())
96 }
97}
98
99#[derive(Default)]
113pub struct SubscriptionGuard {
114 bus: Option<CoolEventBus>,
115 handles: Vec<SubscriptionHandle>,
116}
117
118impl SubscriptionGuard {
119 pub fn new(bus: CoolEventBus) -> Self {
120 Self {
121 bus: Some(bus),
122 handles: Vec::new(),
123 }
124 }
125
126 pub fn track(&mut self, handle: SubscriptionHandle) {
128 self.handles.push(handle);
129 }
130}
131
132impl Drop for SubscriptionGuard {
133 fn drop(&mut self) {
134 let Some(bus) = &self.bus else {
135 return;
136 };
137 for handle in self.handles.drain(..) {
138 bus.unsubscribe(handle);
139 }
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use std::sync::Mutex;
146
147 use super::*;
148 use crate::events::ModelEvent;
149
150 fn envelope(model: &str, operation: ModelEventKind) -> CoolEventEnvelope {
151 CoolEventEnvelope {
152 event_id: uuid::Uuid::new_v4(),
153 model: model.to_owned(),
154 operation,
155 occurred_at: chrono::Utc::now(),
156 data: serde_json::json!({"id": 1}),
157 }
158 }
159
160 #[tokio::test]
161 async fn unsubscribe_stops_further_delivery() {
162 let bus = CoolEventBus::default();
163 let received = Arc::new(Mutex::new(0u32));
164 let received_clone = Arc::clone(&received);
165 let handle = bus.subscribe("Widget", ModelEventKind::Created, move |_event| {
166 let received = Arc::clone(&received_clone);
167 Box::pin(async move {
168 *received.lock().unwrap() += 1;
169 Ok(())
170 })
171 });
172
173 bus.emit(envelope("Widget", ModelEventKind::Created))
174 .await
175 .unwrap();
176 assert_eq!(*received.lock().unwrap(), 1);
177
178 bus.unsubscribe(handle);
179
180 bus.emit(envelope("Widget", ModelEventKind::Created))
181 .await
182 .unwrap();
183 assert_eq!(
184 *received.lock().unwrap(),
185 1,
186 "no further delivery after unsubscribe"
187 );
188 }
189
190 #[tokio::test]
191 async fn unsubscribe_does_not_affect_other_handlers_on_the_same_topic() {
192 let bus = CoolEventBus::default();
193 let count_a = Arc::new(Mutex::new(0u32));
194 let count_b = Arc::new(Mutex::new(0u32));
195
196 let handle_a = {
197 let count_a = Arc::clone(&count_a);
198 bus.subscribe("Widget", ModelEventKind::Created, move |_event| {
199 let count_a = Arc::clone(&count_a);
200 Box::pin(async move {
201 *count_a.lock().unwrap() += 1;
202 Ok(())
203 })
204 })
205 };
206 {
207 let count_b = Arc::clone(&count_b);
208 bus.subscribe("Widget", ModelEventKind::Created, move |_event| {
209 let count_b = Arc::clone(&count_b);
210 Box::pin(async move {
211 *count_b.lock().unwrap() += 1;
212 Ok(())
213 })
214 });
215 }
216
217 bus.unsubscribe(handle_a);
218 bus.emit(envelope("Widget", ModelEventKind::Created))
219 .await
220 .unwrap();
221
222 assert_eq!(*count_a.lock().unwrap(), 0);
223 assert_eq!(*count_b.lock().unwrap(), 1);
224 }
225
226 #[tokio::test]
227 async fn unsubscribe_is_a_no_op_for_an_unknown_handle() {
228 let bus = CoolEventBus::default();
229 bus.unsubscribe(SubscriptionHandle {
231 topic: "Widget.created".to_owned(),
232 id: 42,
233 });
234 }
235
236 #[tokio::test]
237 async fn subscription_guard_unsubscribes_every_tracked_handle_on_drop() {
238 let bus = CoolEventBus::default();
239 let received = Arc::new(Mutex::new(0u32));
240
241 let mut guard = SubscriptionGuard::new(bus.clone());
242 for _ in 0..2 {
243 let received = Arc::clone(&received);
244 let handle = bus.subscribe("Widget", ModelEventKind::Created, move |_event| {
245 let received = Arc::clone(&received);
246 Box::pin(async move {
247 *received.lock().unwrap() += 1;
248 Ok(())
249 })
250 });
251 guard.track(handle);
252 }
253
254 bus.emit(envelope("Widget", ModelEventKind::Created))
255 .await
256 .unwrap();
257 assert_eq!(*received.lock().unwrap(), 2, "both handlers fired");
258
259 drop(guard);
260
261 bus.emit(envelope("Widget", ModelEventKind::Created))
262 .await
263 .unwrap();
264 assert_eq!(
265 *received.lock().unwrap(),
266 2,
267 "dropping the guard unsubscribed both handlers"
268 );
269 }
270
271 #[test]
272 fn model_event_try_from_envelope_still_works_alongside_the_bus_types() {
273 #[derive(serde::Deserialize)]
276 struct Widget {
277 id: i64,
278 }
279 let event =
280 ModelEvent::<Widget>::try_from(envelope("Widget", ModelEventKind::Created)).unwrap();
281 assert_eq!(event.data.id, 1);
282 }
283}