rusty_files/watcher/
debouncer.rs1use 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}