use std::time::{Duration, Instant};
use rustc_hash::FxHashSet;
use crate::app_state::with_app_state;
use crate::reactive::owner::{OwnerId, current_owner, on_cleanup, under_scope};
const MIN_PERIOD: Duration = Duration::from_millis(1);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TimerHandle(u64);
impl TimerHandle {
pub fn cancel(self) {
with_app_state(|app| {
app.timers
.borrow_mut()
.entries
.retain(|timer| timer.id != self.0);
});
}
pub fn is_pending(self) -> bool {
with_app_state(|app| {
app.timers
.borrow()
.entries
.iter()
.any(|timer| timer.id == self.0)
})
}
}
pub fn set_timeout(delay: Duration, f: impl FnOnce() + 'static) -> TimerHandle {
schedule(delay, Run::Once(Box::new(f)))
}
pub fn set_interval(period: Duration, f: impl FnMut() + 'static) -> TimerHandle {
schedule(period.max(MIN_PERIOD), Run::Every(Box::new(f)))
}
#[derive(Default)]
pub(crate) struct Timers {
next_id: u64,
entries: Vec<Timer>,
scopes: FxHashSet<OwnerId>,
}
struct Timer {
id: u64,
owner: Option<OwnerId>,
after: Duration,
deadline: Option<Instant>,
run: Option<Run>,
}
enum Run {
Once(Box<dyn FnOnce()>),
Every(Box<dyn FnMut()>),
}
fn schedule(after: Duration, run: Run) -> TimerHandle {
let owner = current_owner();
let (id, first_of_its_scope) = with_app_state(|app| {
let mut timers = app.timers.borrow_mut();
timers.next_id += 1;
let id = timers.next_id;
timers.entries.push(Timer {
id,
owner,
after,
deadline: None,
run: Some(run),
});
let first = owner.is_some_and(|owner| timers.scopes.insert(owner));
(id, first)
});
if let Some(owner) = owner.filter(|_| first_of_its_scope) {
on_cleanup(move || {
with_app_state(|app| {
let mut timers = app.timers.borrow_mut();
timers.scopes.remove(&owner);
timers.entries.retain(|timer| timer.owner != Some(owner));
});
});
}
TimerHandle(id)
}
pub(crate) fn next_deadline() -> Option<Instant> {
with_app_state(|app| {
let timers = app.timers.borrow();
let unarmed = timers.entries.iter().any(|timer| timer.deadline.is_none());
if unarmed {
return Some(Instant::now());
}
timers
.entries
.iter()
.filter_map(|timer| timer.deadline)
.min()
})
}
pub(crate) fn run_due_timers(now: Instant) {
let due: Vec<u64> = with_app_state(|app| {
let mut timers = app.timers.borrow_mut();
for timer in &mut timers.entries {
timer.deadline.get_or_insert(now + timer.after);
}
let mut due: Vec<(Instant, u64)> = timers
.entries
.iter()
.filter_map(|timer| {
timer
.deadline
.filter(|at| *at <= now)
.map(|at| (at, timer.id))
})
.collect();
due.sort_unstable();
due.into_iter().map(|(_, id)| id).collect()
});
for id in due {
let Some((owner, run)) = with_app_state(|app| {
let mut timers = app.timers.borrow_mut();
let at = timers.entries.iter().position(|timer| timer.id == id)?;
let owner = timers.entries[at].owner;
let run = timers.entries[at].run.take()?;
if matches!(run, Run::Once(_)) {
timers.entries.remove(at);
}
Some((owner, run))
}) else {
continue;
};
match run {
Run::Once(f) => under_scope(owner, f),
Run::Every(mut f) => {
under_scope(owner, &mut f);
with_app_state(|app| {
let mut timers = app.timers.borrow_mut();
if let Some(timer) = timers.entries.iter_mut().find(|timer| timer.id == id) {
let last = timer.deadline.expect("a timer that ran was armed");
let missed = (now - last).as_nanos() / timer.after.as_nanos();
let beats = u32::try_from(missed + 1).unwrap_or(u32::MAX);
timer.deadline = Some(last + timer.after * beats);
timer.run = Some(Run::Every(f));
}
});
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_loop_sleeps_until_the_next_timer() {
let now = Instant::now();
let timer = set_timeout(Duration::from_millis(250), || {});
assert!(
next_deadline().is_some_and(|at| at <= Instant::now()),
"an unarmed timer asks for a pass now, to be armed"
);
run_due_timers(now);
assert_eq!(next_deadline(), Some(now + Duration::from_millis(250)));
timer.cancel();
assert_eq!(next_deadline(), None, "a cancelled timer is no deadline");
}
}