rusty_files/watcher/
debouncer.rs

1use dashmap::DashMap;
2use std::path::PathBuf;
3use std::sync::Arc;
4use std::time::{Duration, Instant};
5
6pub struct EventDebouncer {
7    events: Arc<DashMap<PathBuf, DebouncedEvent>>,
8    debounce_duration: Duration,
9}
10
11#[derive(Clone)]
12struct DebouncedEvent {
13    last_event_time: Instant,
14    event_type: FileEventType,
15}
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub enum FileEventType {
19    Created,
20    Modified,
21    Deleted,
22    Renamed,
23}
24
25impl EventDebouncer {
26    pub fn new(debounce_ms: u64) -> Self {
27        Self {
28            events: Arc::new(DashMap::new()),
29            debounce_duration: Duration::from_millis(debounce_ms),
30        }
31    }
32
33    pub fn should_process(&self, path: PathBuf, event_type: FileEventType) -> bool {
34        let now = Instant::now();
35
36        if let Some(mut entry) = self.events.get_mut(&path) {
37            let elapsed = now.duration_since(entry.last_event_time);
38
39            if elapsed < self.debounce_duration {
40                entry.last_event_time = now;
41                entry.event_type = event_type;
42                return false;
43            }
44
45            entry.last_event_time = now;
46            entry.event_type = event_type;
47            true
48        } else {
49            self.events.insert(
50                path.clone(),
51                DebouncedEvent {
52                    last_event_time: now,
53                    event_type,
54                },
55            );
56            true
57        }
58    }
59
60    pub fn cleanup_old_events(&self, max_age: Duration) {
61        let now = Instant::now();
62        self.events.retain(|_, event| {
63            now.duration_since(event.last_event_time) < max_age
64        });
65    }
66
67    pub fn clear(&self) {
68        self.events.clear();
69    }
70
71    pub fn len(&self) -> usize {
72        self.events.len()
73    }
74
75    pub fn is_empty(&self) -> bool {
76        self.events.is_empty()
77    }
78}
79
80impl Default for EventDebouncer {
81    fn default() -> Self {
82        Self::new(500)
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use std::thread;
90
91    #[test]
92    fn test_debouncer_basic() {
93        let debouncer = EventDebouncer::new(100);
94        let path = PathBuf::from("/test/file.txt");
95
96        assert!(debouncer.should_process(path.clone(), FileEventType::Modified));
97
98        assert!(!debouncer.should_process(path.clone(), FileEventType::Modified));
99    }
100
101    #[test]
102    fn test_debouncer_after_delay() {
103        let debouncer = EventDebouncer::new(50);
104        let path = PathBuf::from("/test/file.txt");
105
106        assert!(debouncer.should_process(path.clone(), FileEventType::Modified));
107
108        thread::sleep(Duration::from_millis(100));
109
110        assert!(debouncer.should_process(path.clone(), FileEventType::Modified));
111    }
112
113    #[test]
114    fn test_cleanup_old_events() {
115        let debouncer = EventDebouncer::new(100);
116        let path = PathBuf::from("/test/file.txt");
117
118        debouncer.should_process(path.clone(), FileEventType::Modified);
119        assert_eq!(debouncer.len(), 1);
120
121        thread::sleep(Duration::from_millis(200));
122        debouncer.cleanup_old_events(Duration::from_millis(100));
123
124        assert_eq!(debouncer.len(), 0);
125    }
126}