use std::cell::RefCell; use std::collections::BTreeMap; use std::sync::mpsc::{self, Receiver, Sender}; use std::sync::{Arc, Mutex, OnceLock}; use crate::model::{DomainEntity, DomainEvent, NotificationAction}; #[derive(Default)] struct BusState { next_id: u64, subscribers: BTreeMap>, } #[derive(Default)] struct BusInner { state: Mutex, publish_lock: Mutex<()>, } /// Minimal deterministic in-process event bus. Delivery is synchronous and /// ordered, while every subscriber owns an independent unbounded queue. #[derive(Clone, Default)] pub struct EventBus { inner: Arc, } impl EventBus { pub fn subscribe(&self) -> EventSubscription { let (sender, receiver) = mpsc::channel(); let _publish = lock(&self.inner.publish_lock); let mut state = lock(&self.inner.state); let id = state.next_id; state.next_id = state.next_id.wrapping_add(1); state.subscribers.insert(id, sender); EventSubscription { id: Some(id), inner: Arc::clone(&self.inner), receiver, } } pub fn publish(&self, event: DomainEvent) { let _publish = lock(&self.inner.publish_lock); let subscribers = lock(&self.inner.state) .subscribers .iter() .map(|(id, sender)| (*id, sender.clone())) .collect::>(); let disconnected = subscribers .into_iter() .filter_map(|(id, sender)| sender.send(event.clone()).is_err().then_some(id)) .collect::>(); if !disconnected.is_empty() { let mut state = lock(&self.inner.state); for id in disconnected { state.subscribers.remove(&id); } } } } pub struct EventSubscription { id: Option, inner: Arc, receiver: Receiver, } impl EventSubscription { pub fn drain(&self) -> Vec { self.receiver.try_iter().collect() } pub fn unsubscribe(mut self) { self.detach(); } fn detach(&mut self) { if let Some(id) = self.id.take() { let _publish = lock(&self.inner.publish_lock); lock(&self.inner.state).subscribers.remove(&id); } } } impl Drop for EventSubscription { fn drop(&mut self) { self.detach(); } } fn lock(mutex: &Mutex) -> std::sync::MutexGuard<'_, T> { mutex .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()) } fn global_bus() -> &'static EventBus { static BUS: OnceLock = OnceLock::new(); BUS.get_or_init(EventBus::default) } thread_local! { static CAPTURED_EVENTS: RefCell>> = const { RefCell::new(None) }; } pub fn subscribe() -> EventSubscription { global_bus().subscribe() } pub fn publish(event: DomainEvent) { CAPTURED_EVENTS.with(|captured| { if let Some(events) = captured.borrow_mut().as_mut() { events.push(event.clone()); } }); global_bus().publish(event); } pub fn entity_changed( project_id: &str, entity: DomainEntity, entity_id: &str, action: NotificationAction, ) { publish(DomainEvent::EntityChanged { project_id: project_id.to_string(), entity, entity_id: entity_id.to_string(), action, }); } pub fn settings_changed(project_id: Option<&str>, key: &str) { publish(DomainEvent::SettingsChanged { project_id: project_id.map(str::to_string), key: key.to_string(), }); } pub(crate) fn capture_current_thread( operation: impl FnOnce() -> T, ) -> Result<(T, Vec), &'static str> { let started = CAPTURED_EVENTS.with(|captured| { let mut captured = captured.borrow_mut(); if captured.is_some() { false } else { *captured = Some(Vec::new()); true } }); if !started { return Err("nested CLI event capture is not supported"); } let mut reset = CaptureReset::default(); let result = operation(); let events = CAPTURED_EVENTS.with(|captured| captured.borrow_mut().take().unwrap_or_default()); reset.finished = true; Ok((result, events)) } #[derive(Default)] struct CaptureReset { finished: bool, } impl Drop for CaptureReset { fn drop(&mut self) { if !self.finished { CAPTURED_EVENTS.with(|captured| { captured.borrow_mut().take(); }); } } }