Skip to main content

hd_watch/
debounce.rs

1use std::collections::HashMap;
2use std::path::PathBuf;
3
4#[derive(Debug, Clone, PartialEq)]
5pub enum ChangeKind {
6    Created,
7    Modified,
8    Deleted,
9    Renamed,
10}
11
12#[derive(Debug, Clone)]
13pub struct RawChange {
14    pub path: PathBuf,
15    pub kind: ChangeKind,
16}
17
18/// Collects raw filesystem events and coalesces them by path.
19/// When drained, emits one event per unique path (last event wins).
20pub struct Debouncer {
21    pending: HashMap<PathBuf, ChangeKind>,
22}
23
24impl Debouncer {
25    pub fn new() -> Self {
26        Debouncer {
27            pending: HashMap::new(),
28        }
29    }
30
31    /// Push a raw change event. If the same path was already pending,
32    /// the new kind overwrites the old one.
33    pub fn push(&mut self, change: RawChange) {
34        self.pending.insert(change.path, change.kind);
35    }
36
37    /// Drain all pending changes, returning a batch of coalesced events.
38    pub fn drain(&mut self) -> Vec<RawChange> {
39        let batch: Vec<RawChange> = self.pending
40            .drain()
41            .map(|(path, kind)| RawChange { path, kind })
42            .collect();
43        batch
44    }
45
46    /// Check if there are pending changes.
47    pub fn has_pending(&self) -> bool {
48        !self.pending.is_empty()
49    }
50}
51
52impl Default for Debouncer {
53    fn default() -> Self {
54        Self::new()
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    use std::path::PathBuf;
62
63    #[test]
64    fn single_event_emitted_after_drain() {
65        let mut debouncer = Debouncer::new();
66        debouncer.push(RawChange {
67            path: PathBuf::from("src/main.rs"),
68            kind: ChangeKind::Modified,
69        });
70        let batch = debouncer.drain();
71        assert_eq!(batch.len(), 1);
72        assert_eq!(batch[0].path, PathBuf::from("src/main.rs"));
73    }
74
75    #[test]
76    fn duplicate_paths_coalesced() {
77        let mut debouncer = Debouncer::new();
78        debouncer.push(RawChange {
79            path: PathBuf::from("src/main.rs"),
80            kind: ChangeKind::Modified,
81        });
82        debouncer.push(RawChange {
83            path: PathBuf::from("src/main.rs"),
84            kind: ChangeKind::Modified,
85        });
86        let batch = debouncer.drain();
87        assert_eq!(batch.len(), 1);
88    }
89
90    #[test]
91    fn different_paths_preserved() {
92        let mut debouncer = Debouncer::new();
93        debouncer.push(RawChange {
94            path: PathBuf::from("a.rs"),
95            kind: ChangeKind::Modified,
96        });
97        debouncer.push(RawChange {
98            path: PathBuf::from("b.rs"),
99            kind: ChangeKind::Created,
100        });
101        let batch = debouncer.drain();
102        assert_eq!(batch.len(), 2);
103    }
104
105    #[test]
106    fn drain_clears_buffer() {
107        let mut debouncer = Debouncer::new();
108        debouncer.push(RawChange {
109            path: PathBuf::from("file.rs"),
110            kind: ChangeKind::Modified,
111        });
112        let _ = debouncer.drain();
113        let batch = debouncer.drain();
114        assert!(batch.is_empty());
115    }
116
117    #[test]
118    fn last_kind_wins_for_same_path() {
119        let mut debouncer = Debouncer::new();
120        debouncer.push(RawChange {
121            path: PathBuf::from("file.rs"),
122            kind: ChangeKind::Created,
123        });
124        debouncer.push(RawChange {
125            path: PathBuf::from("file.rs"),
126            kind: ChangeKind::Deleted,
127        });
128        let batch = debouncer.drain();
129        assert_eq!(batch.len(), 1);
130        assert_eq!(batch[0].kind, ChangeKind::Deleted);
131    }
132}