1use serde::{Deserialize, Serialize};
7use std::path::PathBuf;
8use tokio::sync::broadcast;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
16#[serde(tag = "event", content = "data")]
17pub enum DaemonEvent {
18 FileChanged {
20 project: String,
21 path: String,
22 action: FileAction,
23 },
24 ReindexStart {
26 project: String,
27 files: usize,
28 reason: String,
29 },
30 ReindexProgress {
32 project: String,
33 processed: usize,
34 total: usize,
35 },
36 ReindexComplete {
38 project: String,
39 files: usize,
40 symbols: usize,
41 dead: usize,
42 duration_ms: f64,
43 },
44 StatusUpdate { projects: usize, watching: usize },
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50#[serde(rename_all = "lowercase")]
51pub enum FileAction {
52 Created,
53 Modified,
54 Deleted,
55}
56
57#[derive(Clone)]
63pub struct EventBroadcaster {
64 sender: broadcast::Sender<DaemonEvent>,
65}
66
67impl EventBroadcaster {
68 pub fn new(capacity: usize) -> Self {
70 let (sender, _) = broadcast::channel(capacity);
71 Self { sender }
72 }
73
74 pub fn broadcast(&self, event: DaemonEvent) -> usize {
77 self.sender.send(event).unwrap_or(0)
79 }
80
81 pub fn subscribe(&self) -> broadcast::Receiver<DaemonEvent> {
83 self.sender.subscribe()
84 }
85
86 pub fn subscriber_count(&self) -> usize {
88 self.sender.receiver_count()
89 }
90
91 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 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 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 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) }
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 let count = broadcaster.broadcast(DaemonEvent::StatusUpdate {
176 projects: 0,
177 watching: 0,
178 });
179 assert_eq!(count, 0);
180 }
181}