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}
26
27/// Scope of configuration change
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum ConfigScope {
30    Global,
31    Project(String),
32    Local(String),
33    Mcp,
34}
35
36/// Event bus for broadcasting data events
37///
38/// Uses tokio::broadcast for multi-consumer support.
39/// TUI subscribes for redraw triggers, Web uses for SSE push.
40pub struct EventBus {
41    sender: broadcast::Sender<DataEvent>,
42}
43
44impl EventBus {
45    /// Create a new event bus with specified channel capacity
46    pub fn new(capacity: usize) -> Self {
47        let (sender, _) = broadcast::channel(capacity);
48        Self { sender }
49    }
50
51    /// Create with default capacity (256 events)
52    pub fn default_capacity() -> Self {
53        Self::new(256)
54    }
55
56    /// Publish an event to all subscribers
57    pub fn publish(&self, event: DataEvent) {
58        // Ignore send errors (no subscribers)
59        let _ = self.sender.send(event);
60    }
61
62    /// Subscribe to receive events
63    pub fn subscribe(&self) -> broadcast::Receiver<DataEvent> {
64        self.sender.subscribe()
65    }
66
67    /// Get current number of active subscribers
68    pub fn subscriber_count(&self) -> usize {
69        self.sender.receiver_count()
70    }
71}
72
73impl Default for EventBus {
74    fn default() -> Self {
75        Self::default_capacity()
76    }
77}
78
79impl Clone for EventBus {
80    fn clone(&self) -> Self {
81        Self {
82            sender: self.sender.clone(),
83        }
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[tokio::test]
92    async fn test_event_bus_publish_subscribe() {
93        let bus = EventBus::default_capacity();
94        let mut rx = bus.subscribe();
95
96        bus.publish(DataEvent::StatsUpdated);
97        bus.publish(DataEvent::SessionCreated(SessionId::from("test-session")));
98
99        let event1 = rx.recv().await.unwrap();
100        assert!(matches!(event1, DataEvent::StatsUpdated));
101
102        let event2 = rx.recv().await.unwrap();
103        assert!(
104            matches!(event2, DataEvent::SessionCreated(ref id) if id.as_str() == "test-session")
105        );
106    }
107
108    #[tokio::test]
109    async fn test_event_bus_multiple_subscribers() {
110        let bus = EventBus::default_capacity();
111        let mut rx1 = bus.subscribe();
112        let mut rx2 = bus.subscribe();
113
114        assert_eq!(bus.subscriber_count(), 2);
115
116        bus.publish(DataEvent::LoadCompleted);
117
118        let e1 = rx1.recv().await.unwrap();
119        let e2 = rx2.recv().await.unwrap();
120
121        assert!(matches!(e1, DataEvent::LoadCompleted));
122        assert!(matches!(e2, DataEvent::LoadCompleted));
123    }
124
125    #[test]
126    fn test_event_bus_no_subscribers_ok() {
127        let bus = EventBus::default_capacity();
128        // Should not panic even with no subscribers
129        bus.publish(DataEvent::StatsUpdated);
130    }
131}