use crate::ui_state::Clock;
use crate::watch::Mark;
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>,
cadence: super::Cadence,
pending: HashMap<PathBuf, (Instant, Mark)>,
last_cheap: Instant,
last_full: Instant,
}
impl Schedule {
pub fn new(clock: Arc<dyn Clock>, cadence: super::Cadence) -> Self {
let now = clock.now();
Self {
clock,
cadence,
pending: HashMap::new(),
last_cheap: now,
last_full: now,
}
}
pub fn set_cadence(&mut self, cadence: super::Cadence) {
self.cadence = cadence;
}
pub(crate) fn mark<I: IntoIterator<Item = (PathBuf, Mark)>>(&mut self, roots: I) {
let deadline = self.clock.now() + self.cadence.debounce;
for (root, mark) in roots {
let slot = self.pending.entry(root).or_insert((deadline, mark));
slot.1 = slot.1.max(mark);
}
}
pub fn due(&mut self) -> Vec<(PathBuf, Mark)> {
let mut ready = Vec::new();
let now = self.clock.now();
for (root, &(deadline, mark)) in &self.pending {
if now >= deadline {
ready.push((root.clone(), mark));
}
}
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, self.cadence.full_sweep) {
self.last_full = now;
self.last_cheap = now;
Sweep::Full
} else if elapsed(now, self.last_cheap, self.cadence.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(), super::super::Cadence::default());
sched.mark([(root("/w"), Mark::Watch)]);
assert!(sched.due().is_empty(), "held during the window");
clock.advance(DEBOUNCE);
assert_eq!(
sched.due(),
vec![(root("/w"), Mark::Watch)],
"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(), super::super::Cadence::default());
sched.mark([(root("/w"), Mark::Watch)]);
clock.advance(Duration::from_millis(40));
sched.mark([(root("/w"), Mark::Watch)]);
clock.advance(Duration::from_millis(60)); assert_eq!(sched.due(), vec![(root("/w"), Mark::Watch)]);
sched.mark([(root("/w"), Mark::Watch)]);
assert!(sched.due().is_empty());
}
#[test]
fn the_strongest_mark_wins_inside_one_window() {
let clock = FakeClock::new();
let mut sched = Schedule::new(clock.arc(), super::super::Cadence::default());
sched.mark([(root("/w"), Mark::Sweep)]);
sched.mark([(root("/w"), Mark::Watch)]);
clock.advance(DEBOUNCE);
assert_eq!(sched.due(), vec![(root("/w"), Mark::Watch)]);
sched.mark([(root("/w"), Mark::Desync)]);
sched.mark([(root("/w"), Mark::Sweep)]);
clock.advance(DEBOUNCE);
assert_eq!(sched.due(), vec![(root("/w"), Mark::Desync)]);
}
#[test]
fn a_re_tuned_cadence_drives_the_next_windows_and_sweeps() {
let clock = FakeClock::new();
let mut sched = Schedule::new(clock.arc(), super::super::Cadence::default());
sched.set_cadence(super::super::Cadence {
debounce: Duration::from_millis(500),
cheap_sweep: Duration::from_secs(10),
full_sweep: Duration::from_mins(1),
});
sched.mark([(root("/w"), Mark::Watch)]);
clock.advance(DEBOUNCE);
assert!(
sched.due().is_empty(),
"the shipped 100 ms is not the window"
);
clock.advance(Duration::from_millis(400));
assert_eq!(sched.due(), vec![(root("/w"), Mark::Watch)]);
clock.advance(CHEAP_SWEEP);
assert_eq!(sched.sweep(), Sweep::None, "2 s is no longer a cheap tick");
clock.advance(Duration::from_secs(10));
assert_eq!(sched.sweep(), Sweep::Cheap);
clock.advance(Duration::from_mins(1));
assert_eq!(sched.sweep(), Sweep::Full);
}
#[test]
fn sweep_none_then_cheap_then_full() {
let clock = FakeClock::new();
let mut sched = Schedule::new(clock.arc(), super::super::Cadence::default());
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");
}
}