Skip to main content

ccboard_core/
event.rs

1//! Event bus for ccboard using tokio::broadcast
2//!
3//! Provides a publish-subscribe mechanism for data updates.
4
5use crate::models::SessionId;
6use tokio::sync::broadcast;
7
8/// Events emitted by the data layer
9#[derive(Debug, Clone)]
10pub enum DataEvent {
11    /// Stats cache was updated
12    StatsUpdated,
13    /// A new session was created
14    SessionCreated(SessionId),
15    /// An existing session was updated
16    SessionUpdated(SessionId),
17    /// Configuration changed
18    ConfigChanged(ConfigScope),
19    /// Analytics data was computed and cached
20    AnalyticsUpdated,
21    /// Initial load completed
22    LoadCompleted,
23    /// Watcher encountered an error
24    WatcherError(String),
25    /// Hook-based live session status changed (live-sessions.json updated)
26    LiveSessionStatusChanged,
27}
28
29/// Scope of configuration change
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub enum ConfigScope {
32    Global,
33    Project(String),
34    Local(String),
35    Mcp,
36}
37
38/// Event bus for broadcasting data events
39///
40/// Uses tokio::broadcast for multi-consumer support.
41/// TUI subscribes for redraw triggers, Web uses for SSE push.
42pub struct EventBus {
43    sender: broadcast::Sender<DataEvent>,
44}
45
46impl EventBus {
47    /// Create a new event bus with specified channel capacity
48    pub fn new(capacity: usize) -> Self {
49        let (sender, _) = broadcast::channel(capacity);
50        Self { sender }
51    }
52
53    /// Create with default capacity (256 events)
54    pub fn default_capacity() -> Self {
55        Self::new(256)
56    }
57
58    /// Publish an event to all subscribers
59    pub fn publish(&self, event: DataEvent) {
60        // Ignore send errors (no subscribers)
61        let _ = self.sender.send(event);
62    }
63
64    /// Subscribe to receive events
65    pub fn subscribe(&self) -> broadcast::Receiver<DataEvent> {
66        self.sender.subscribe()
67    }
68
69    /// Get current number of active subscribers
70    pub fn subscriber_count(&self) -> usize {
71        self.sender.receiver_count()
72    }
73}
74
75impl Default for EventBus {
76    fn default() -> Self {
77        Self::default_capacity()
78    }
79}
80
81impl Clone for EventBus {
82    fn clone(&self) -> Self {
83        Self {
84            sender: self.sender.clone(),
85        }
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    #[tokio::test]
94    async fn test_event_bus_publish_subscribe() {
95        let bus = EventBus::default_capacity();
96        let mut rx = bus.subscribe();
97
98        bus.publish(DataEvent::StatsUpdated);
99        bus.publish(DataEvent::SessionCreated(SessionId::from("test-session")));
100
101        let event1 = rx.recv().await.unwrap();
102        assert!(matches!(event1, DataEvent::StatsUpdated));
103
104        let event2 = rx.recv().await.unwrap();
105        assert!(
106            matches!(event2, DataEvent::SessionCreated(ref id) if id.as_str() == "test-session")
107        );
108    }
109
110    #[tokio::test]
111    async fn test_event_bus_multiple_subscribers() {
112        let bus = EventBus::default_capacity();
113        let mut rx1 = bus.subscribe();
114        let mut rx2 = bus.subscribe();
115
116        assert_eq!(bus.subscriber_count(), 2);
117
118        bus.publish(DataEvent::LoadCompleted);
119
120        let e1 = rx1.recv().await.unwrap();
121        let e2 = rx2.recv().await.unwrap();
122
123        assert!(matches!(e1, DataEvent::LoadCompleted));
124        assert!(matches!(e2, DataEvent::LoadCompleted));
125    }
126
127    #[test]
128    fn test_event_bus_no_subscribers_ok() {
129        let bus = EventBus::default_capacity();
130        // Should not panic even with no subscribers
131        bus.publish(DataEvent::StatsUpdated);
132    }
133}