greppy/daemon/
events.rs

1//! Event broadcasting for daemon
2//!
3//! Provides a broadcast channel for daemon events that can be subscribed to
4//! by clients (like the web server) for real-time updates.
5
6use serde::{Deserialize, Serialize};
7use std::path::PathBuf;
8use tokio::sync::broadcast;
9
10// =============================================================================
11// EVENT TYPES
12// =============================================================================
13
14/// Events emitted by the daemon
15#[derive(Debug, Clone, Serialize, Deserialize)]
16#[serde(tag = "event", content = "data")]
17pub enum DaemonEvent {
18    /// File changed in a watched project
19    FileChanged {
20        project: String,
21        path: String,
22        action: FileAction,
23    },
24    /// Reindexing has started
25    ReindexStart {
26        project: String,
27        files: usize,
28        reason: String,
29    },
30    /// Progress update during reindexing
31    ReindexProgress {
32        project: String,
33        processed: usize,
34        total: usize,
35    },
36    /// Reindexing completed
37    ReindexComplete {
38        project: String,
39        files: usize,
40        symbols: usize,
41        dead: usize,
42        duration_ms: f64,
43    },
44    /// Daemon status update
45    StatusUpdate { projects: usize, watching: usize },
46}
47
48/// File change action type
49#[derive(Debug, Clone, Serialize, Deserialize)]
50#[serde(rename_all = "lowercase")]
51pub enum FileAction {
52    Created,
53    Modified,
54    Deleted,
55}
56
57// =============================================================================
58// EVENT BROADCASTER
59// =============================================================================
60
61/// Broadcasts events to all subscribers
62#[derive(Clone)]
63pub struct EventBroadcaster {
64    sender: broadcast::Sender<DaemonEvent>,
65}
66
67impl EventBroadcaster {
68    /// Create a new event broadcaster with the specified capacity
69    pub fn new(capacity: usize) -> Self {
70        let (sender, _) = broadcast::channel(capacity);
71        Self { sender }
72    }
73
74    /// Broadcast an event to all subscribers
75    /// Returns the number of receivers that received the event
76    pub fn broadcast(&self, event: DaemonEvent) -> usize {
77        // send() returns Err if there are no receivers, which is fine
78        self.sender.send(event).unwrap_or(0)
79    }
80
81    /// Subscribe to events
82    pub fn subscribe(&self) -> broadcast::Receiver<DaemonEvent> {
83        self.sender.subscribe()
84    }
85
86    /// Get the number of active subscribers
87    pub fn subscriber_count(&self) -> usize {
88        self.sender.receiver_count()
89    }
90
91    // ==========================================================================
92    // CONVENIENCE METHODS
93    // ==========================================================================
94
95    /// Emit a file changed event
96    pub fn file_changed(&self, project: &PathBuf, path: &PathBuf, action: FileAction) {
97        self.broadcast(DaemonEvent::FileChanged {
98            project: project.to_string_lossy().to_string(),
99            path: path.to_string_lossy().to_string(),
100            action,
101        });
102    }
103
104    /// Emit a reindex start event
105    pub fn reindex_start(&self, project: &PathBuf, files: usize, reason: &str) {
106        self.broadcast(DaemonEvent::ReindexStart {
107            project: project.to_string_lossy().to_string(),
108            files,
109            reason: reason.to_string(),
110        });
111    }
112
113    /// Emit a reindex progress event
114    pub fn reindex_progress(&self, project: &PathBuf, processed: usize, total: usize) {
115        self.broadcast(DaemonEvent::ReindexProgress {
116            project: project.to_string_lossy().to_string(),
117            processed,
118            total,
119        });
120    }
121
122    /// Emit a reindex complete event
123    pub fn reindex_complete(
124        &self,
125        project: &PathBuf,
126        files: usize,
127        symbols: usize,
128        dead: usize,
129        duration_ms: f64,
130    ) {
131        self.broadcast(DaemonEvent::ReindexComplete {
132            project: project.to_string_lossy().to_string(),
133            files,
134            symbols,
135            dead,
136            duration_ms,
137        });
138    }
139}
140
141impl Default for EventBroadcaster {
142    fn default() -> Self {
143        Self::new(256) // Default capacity of 256 events
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[tokio::test]
152    async fn test_event_broadcaster() {
153        let broadcaster = EventBroadcaster::new(16);
154        let mut rx = broadcaster.subscribe();
155
156        broadcaster.broadcast(DaemonEvent::StatusUpdate {
157            projects: 1,
158            watching: 1,
159        });
160
161        let event = rx.recv().await.unwrap();
162        match event {
163            DaemonEvent::StatusUpdate { projects, watching } => {
164                assert_eq!(projects, 1);
165                assert_eq!(watching, 1);
166            }
167            _ => panic!("Unexpected event type"),
168        }
169    }
170
171    #[test]
172    fn test_no_subscribers() {
173        let broadcaster = EventBroadcaster::new(16);
174        // Should not panic even with no subscribers
175        let count = broadcaster.broadcast(DaemonEvent::StatusUpdate {
176            projects: 0,
177            watching: 0,
178        });
179        assert_eq!(count, 0);
180    }
181}