use std::collections::HashMap;
use std::path::PathBuf;
use std::time::{Duration, Instant};
use super::{WatchEvent, WatchEventKind};
#[derive(Debug)]
pub struct Debouncer {
delay: Duration,
pending: HashMap<PathBuf, (WatchEvent, Instant)>,
}
impl Debouncer {
pub fn new(delay_ms: u64) -> Self {
Self {
delay: Duration::from_millis(delay_ms),
pending: HashMap::new(),
}
}
pub fn add_event(&mut self, event: WatchEvent) {
if event.kind == WatchEventKind::Removed {
self.pending.remove(&event.path);
return;
}
self.pending
.insert(event.path.clone(), (event, Instant::now()));
}
pub fn get_ready_events(&mut self) -> Vec<WatchEvent> {
let now = Instant::now();
let mut ready = Vec::new();
let ready_paths: Vec<PathBuf> = self
.pending
.iter()
.filter(|(_, (_, timestamp))| now.duration_since(*timestamp) >= self.delay)
.map(|(path, _)| path.clone())
.collect();
for path in ready_paths {
if let Some((event, _)) = self.pending.remove(&path) {
ready.push(event);
}
}
ready
}
pub fn pending_count(&self) -> usize {
self.pending.len()
}
pub fn has_pending(&self) -> bool {
!self.pending.is_empty()
}
pub fn clear(&mut self) {
self.pending.clear();
}
pub fn pending_paths(&self) -> Vec<&PathBuf> {
self.pending.keys().collect()
}
pub fn time_until_ready(&self) -> Option<Duration> {
let now = Instant::now();
self.pending
.values()
.map(|(_, timestamp)| {
let elapsed = now.duration_since(*timestamp);
if elapsed >= self.delay {
Duration::ZERO
} else {
self.delay - elapsed
}
})
.min()
}
pub fn flush(&mut self) -> Vec<WatchEvent> {
self.pending.drain().map(|(_, (event, _))| event).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
#[test]
fn test_debouncer_basic() {
let mut debouncer = Debouncer::new(100);
let event = WatchEvent::new(PathBuf::from("test.rs"), WatchEventKind::Modified);
debouncer.add_event(event);
let ready = debouncer.get_ready_events();
assert!(ready.is_empty());
assert_eq!(debouncer.pending_count(), 1);
thread::sleep(Duration::from_millis(150));
let ready = debouncer.get_ready_events();
assert_eq!(ready.len(), 1);
assert_eq!(debouncer.pending_count(), 0);
}
#[test]
fn test_debouncer_coalesce() {
let mut debouncer = Debouncer::new(100);
for _ in 0..5 {
let event = WatchEvent::new(PathBuf::from("test.rs"), WatchEventKind::Modified);
debouncer.add_event(event);
thread::sleep(Duration::from_millis(20));
}
assert_eq!(debouncer.pending_count(), 1);
thread::sleep(Duration::from_millis(150));
let ready = debouncer.get_ready_events();
assert_eq!(ready.len(), 1);
}
#[test]
fn test_debouncer_multiple_files() {
let mut debouncer = Debouncer::new(50);
debouncer.add_event(WatchEvent::new(
PathBuf::from("a.rs"),
WatchEventKind::Modified,
));
debouncer.add_event(WatchEvent::new(
PathBuf::from("b.rs"),
WatchEventKind::Modified,
));
debouncer.add_event(WatchEvent::new(
PathBuf::from("c.rs"),
WatchEventKind::Modified,
));
assert_eq!(debouncer.pending_count(), 3);
thread::sleep(Duration::from_millis(100));
let ready = debouncer.get_ready_events();
assert_eq!(ready.len(), 3);
}
#[test]
fn test_debouncer_remove_not_debounced() {
let mut debouncer = Debouncer::new(100);
debouncer.add_event(WatchEvent::new(
PathBuf::from("test.rs"),
WatchEventKind::Modified,
));
debouncer.add_event(WatchEvent::new(
PathBuf::from("test.rs"),
WatchEventKind::Removed,
));
assert_eq!(debouncer.pending_count(), 0);
}
#[test]
fn test_debouncer_flush() {
let mut debouncer = Debouncer::new(1000);
debouncer.add_event(WatchEvent::new(
PathBuf::from("a.rs"),
WatchEventKind::Modified,
));
debouncer.add_event(WatchEvent::new(
PathBuf::from("b.rs"),
WatchEventKind::Modified,
));
let events = debouncer.flush();
assert_eq!(events.len(), 2);
assert_eq!(debouncer.pending_count(), 0);
}
#[test]
fn test_time_until_ready() {
let mut debouncer = Debouncer::new(100);
assert!(debouncer.time_until_ready().is_none());
debouncer.add_event(WatchEvent::new(
PathBuf::from("test.rs"),
WatchEventKind::Modified,
));
let time = debouncer.time_until_ready().unwrap();
assert!(time > Duration::ZERO);
assert!(time <= Duration::from_millis(100));
thread::sleep(Duration::from_millis(150));
let time = debouncer.time_until_ready().unwrap();
assert!(time <= Duration::from_millis(1));
}
}