use std::collections::HashMap;
use std::path::PathBuf;
use std::time::{Duration, Instant};
#[derive(Debug)]
pub struct Debouncer {
pending: HashMap<PathBuf, Instant>,
duration: Duration,
}
impl Debouncer {
pub fn new(debounce_ms: u64) -> Self {
Self {
pending: HashMap::new(),
duration: Duration::from_millis(debounce_ms),
}
}
pub fn record(&mut self, path: PathBuf) {
self.pending.insert(path, Instant::now());
}
pub fn remove(&mut self, path: &PathBuf) {
self.pending.remove(path);
}
pub fn take_ready(&mut self) -> Vec<PathBuf> {
let now = Instant::now();
let mut ready = Vec::new();
self.pending.retain(|path, last_change| {
if now.duration_since(*last_change) >= self.duration {
ready.push(path.clone());
false } else {
true }
});
ready
}
pub fn has_pending(&self) -> bool {
!self.pending.is_empty()
}
#[allow(dead_code)]
pub fn pending_count(&self) -> usize {
self.pending.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread::sleep;
#[test]
fn test_debouncer_basic() {
let mut debouncer = Debouncer::new(50);
let path = PathBuf::from("/test/file.rs");
debouncer.record(path.clone());
assert!(debouncer.take_ready().is_empty());
assert!(debouncer.has_pending());
sleep(Duration::from_millis(60));
let ready = debouncer.take_ready();
assert_eq!(ready.len(), 1);
assert_eq!(ready[0], path);
assert!(!debouncer.has_pending());
}
#[test]
fn test_debouncer_resets_on_new_change() {
let mut debouncer = Debouncer::new(50);
let path = PathBuf::from("/test/file.rs");
debouncer.record(path.clone());
sleep(Duration::from_millis(30));
debouncer.record(path.clone());
sleep(Duration::from_millis(30));
assert!(debouncer.take_ready().is_empty());
sleep(Duration::from_millis(30));
let ready = debouncer.take_ready();
assert_eq!(ready.len(), 1);
}
#[test]
fn test_debouncer_multiple_files() {
let mut debouncer = Debouncer::new(50);
let path1 = PathBuf::from("/test/file1.rs");
let path2 = PathBuf::from("/test/file2.rs");
debouncer.record(path1.clone());
sleep(Duration::from_millis(30));
debouncer.record(path2.clone());
sleep(Duration::from_millis(25));
let ready = debouncer.take_ready();
assert_eq!(ready.len(), 1);
assert_eq!(ready[0], path1);
assert!(debouncer.has_pending());
sleep(Duration::from_millis(30));
let ready = debouncer.take_ready();
assert_eq!(ready.len(), 1);
assert_eq!(ready[0], path2);
}
#[test]
fn test_debouncer_remove() {
let mut debouncer = Debouncer::new(50);
let path = PathBuf::from("/test/file.rs");
debouncer.record(path.clone());
assert!(debouncer.has_pending());
debouncer.remove(&path);
assert!(!debouncer.has_pending());
}
}