use std::time::{Duration, Instant};
pub struct Debouncer {
window: Duration,
pending_since: Option<Instant>,
}
impl Debouncer {
pub fn new(window: Duration) -> Self {
Self {
window,
pending_since: None,
}
}
pub fn record(&mut self, now: Instant) {
self.pending_since = Some(now);
}
pub fn deadline(&self) -> Option<Instant> {
self.pending_since.map(|t| t + self.window)
}
#[allow(dead_code)] pub fn is_pending(&self) -> bool {
self.pending_since.is_some()
}
pub fn fire_if_due(&mut self, now: Instant) -> bool {
let Some(deadline) = self.deadline() else {
return false;
};
if now >= deadline {
self.pending_since = None;
true
} else {
false
}
}
pub fn clear(&mut self) {
self.pending_since = None;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn window() -> Duration {
Duration::from_millis(200)
}
#[test]
fn fresh_debouncer_is_idle() {
let d = Debouncer::new(window());
assert!(!d.is_pending());
assert!(d.deadline().is_none());
}
#[test]
fn record_arms_the_deadline() {
let mut d = Debouncer::new(window());
let t0 = Instant::now();
d.record(t0);
assert!(d.is_pending());
assert_eq!(d.deadline(), Some(t0 + window()));
}
#[test]
fn fire_if_due_returns_false_before_deadline() {
let mut d = Debouncer::new(window());
let t0 = Instant::now();
d.record(t0);
assert!(!d.fire_if_due(t0 + Duration::from_millis(50)));
assert!(d.is_pending(), "still pending below deadline");
}
#[test]
fn fire_if_due_clears_state_at_deadline() {
let mut d = Debouncer::new(window());
let t0 = Instant::now();
d.record(t0);
assert!(d.fire_if_due(t0 + window()));
assert!(!d.is_pending());
assert!(d.deadline().is_none());
}
#[test]
fn record_during_pending_extends_window() {
let mut d = Debouncer::new(window());
let t0 = Instant::now();
d.record(t0);
let t1 = t0 + Duration::from_millis(100);
d.record(t1);
assert_eq!(d.deadline(), Some(t1 + window()));
assert!(!d.fire_if_due(t0 + window()));
}
#[test]
fn clear_discards_pending_state() {
let mut d = Debouncer::new(window());
d.record(Instant::now());
d.clear();
assert!(!d.is_pending());
assert!(d.deadline().is_none());
}
#[test]
fn fire_if_due_on_idle_is_a_noop() {
let mut d = Debouncer::new(window());
assert!(!d.fire_if_due(Instant::now()));
assert!(!d.is_pending());
}
}