use crate::ui_state::Clock;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::{Duration, Instant};
pub const DEBOUNCE: Duration = Duration::from_millis(100);
pub const CHEAP_SWEEP: Duration = Duration::from_secs(2);
pub const FULL_SWEEP: Duration = Duration::from_secs(15);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Sweep {
None,
Cheap,
Full,
}
pub struct Schedule {
clock: Arc<dyn Clock>,
pending: HashMap<PathBuf, Instant>,
last_cheap: Instant,
last_full: Instant,
}
impl Schedule {
pub fn new(clock: Arc<dyn Clock>) -> Self {
let now = clock.now();
Self {
clock,
pending: HashMap::new(),
last_cheap: now,
last_full: now,
}
}
pub(crate) fn mark<I: IntoIterator<Item = PathBuf>>(&mut self, roots: I) {
let deadline = self.clock.now() + DEBOUNCE;
for root in roots {
self.pending.entry(root).or_insert(deadline);
}
}
pub fn due(&mut self) -> Vec<PathBuf> {
let now = self.clock.now();
let mut ready = Vec::new();
for (root, &deadline) in &self.pending {
if now >= deadline {
ready.push(root.clone());
}
}
for root in &ready {
self.pending.remove(root);
}
ready
}
pub fn sweep(&mut self) -> Sweep {
let now = self.clock.now();
if elapsed(now, self.last_full, FULL_SWEEP) {
self.last_full = now;
self.last_cheap = now;
Sweep::Full
} else if elapsed(now, self.last_cheap, CHEAP_SWEEP) {
self.last_cheap = now;
Sweep::Cheap
} else {
Sweep::None
}
}
}
fn elapsed(now: Instant, last: Instant, period: Duration) -> bool {
now.saturating_duration_since(last) >= period
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::FakeClock;
fn root(s: &str) -> PathBuf {
PathBuf::from(s)
}
#[test]
fn debounce_holds_a_root_until_the_window_elapses() {
let clock = FakeClock::new();
let mut sched = Schedule::new(clock.arc());
sched.mark([root("/w")]);
assert!(sched.due().is_empty(), "held during the window");
clock.advance(DEBOUNCE);
assert_eq!(sched.due(), vec![root("/w")], "released after the window");
assert!(sched.due().is_empty(), "consumed — not re-emitted");
}
#[test]
fn debounce_coalesces_repeated_marks_to_one_release() {
let clock = FakeClock::new();
let mut sched = Schedule::new(clock.arc());
sched.mark([root("/w")]);
clock.advance(Duration::from_millis(40));
sched.mark([root("/w")]);
clock.advance(Duration::from_millis(60)); assert_eq!(sched.due(), vec![root("/w")]);
sched.mark([root("/w")]);
assert!(sched.due().is_empty());
}
#[test]
fn sweep_none_then_cheap_then_full() {
let clock = FakeClock::new();
let mut sched = Schedule::new(clock.arc());
assert_eq!(sched.sweep(), Sweep::None);
clock.advance(CHEAP_SWEEP);
assert_eq!(sched.sweep(), Sweep::Cheap);
assert_eq!(sched.sweep(), Sweep::None, "cheap deadline reset");
clock.advance(FULL_SWEEP);
assert_eq!(sched.sweep(), Sweep::Full);
assert_eq!(sched.sweep(), Sweep::None, "full reset both deadlines");
clock.advance(CHEAP_SWEEP);
assert_eq!(sched.sweep(), Sweep::Cheap, "cheap resumes after a full");
}
}