use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::rc::Rc;
use web_time::{Duration, Instant};
use crate::{request_frame, unique_component_id};
const MIN_PERIOD: Duration = Duration::from_millis(1);
thread_local! {
static REGISTRY: RefCell<HashMap<u64, Entry>> = RefCell::new(HashMap::new());
static FRAME: RefCell<u64> = const { RefCell::new(0) };
static IN_POLL: Cell<bool> = const { Cell::new(false) };
}
type Callback = Rc<RefCell<Box<dyn FnMut()>>>;
#[derive(Clone, Copy)]
enum Due {
At(Instant),
Frame(u64),
}
#[derive(Clone, Copy)]
enum Repeat {
Once,
Every { period: Duration },
Times { period: Duration, left: u32 },
}
struct Entry {
due: Due,
repeat: Repeat,
callback: Callback,
}
#[must_use = "dropping the handle cancels the timer"]
pub struct TimerHandle {
id: Option<u64>,
}
impl TimerHandle {
pub fn cancel(mut self) {
self.cancel_now();
}
pub fn detach(self) {
std::mem::forget(self);
}
fn cancel_now(&mut self) {
if let Some(id) = self.id.take() {
cancel(id);
}
}
}
impl Drop for TimerHandle {
fn drop(&mut self) {
self.cancel_now();
}
}
fn insert(due: Due, repeat: Repeat, callback: Callback) -> TimerHandle {
let id = unique_component_id();
REGISTRY.with(|r| {
r.borrow_mut().insert(
id,
Entry {
due,
repeat,
callback,
},
);
});
request_frame();
TimerHandle { id: Some(id) }
}
fn cancel(id: u64) {
REGISTRY.with(|r| {
r.borrow_mut().remove(&id);
});
}
fn wrap_once(cb: impl FnOnce() + 'static) -> Callback {
let mut cb = Some(cb);
Rc::new(RefCell::new(Box::new(move || {
if let Some(f) = cb.take() {
f();
}
}) as Box<dyn FnMut()>))
}
pub fn delay(duration: Duration, cb: impl FnOnce() + 'static) -> TimerHandle {
insert(
Due::At(saturating_add(Instant::now(), duration)),
Repeat::Once,
wrap_once(cb),
)
}
pub fn timeout(duration: Duration, cb: impl FnOnce() + 'static) -> TimerHandle {
delay(duration, cb)
}
pub fn delay_frames(frames: u32, cb: impl FnOnce() + 'static) -> TimerHandle {
let at = FRAME.with(|f| f.borrow().wrapping_add(frames as u64));
insert(Due::Frame(at), Repeat::Once, wrap_once(cb))
}
pub fn interval(period: Duration, cb: impl FnMut() + 'static) -> TimerHandle {
let period = period.max(MIN_PERIOD);
insert(
Due::At(saturating_add(Instant::now(), period)),
Repeat::Every { period },
Rc::new(RefCell::new(Box::new(cb) as Box<dyn FnMut()>)),
)
}
pub fn interval_n(period: Duration, times: u32, cb: impl FnMut() + 'static) -> TimerHandle {
if times == 0 {
return TimerHandle { id: None };
}
let period = period.max(MIN_PERIOD);
insert(
Due::At(saturating_add(Instant::now(), period)),
Repeat::Times {
period,
left: times,
},
Rc::new(RefCell::new(Box::new(cb) as Box<dyn FnMut()>)),
)
}
pub fn frame_count() -> u64 {
FRAME.with(|f| *f.borrow())
}
pub fn next_deadline() -> Option<Instant> {
REGISTRY.with(|r| {
r.borrow()
.values()
.filter_map(|e| match e.due {
Due::At(t) => Some(t),
Due::Frame(_) => None,
})
.min()
})
}
fn saturating_add(t: Instant, d: Duration) -> Instant {
t.checked_add(d).unwrap_or(t)
}
pub fn poll() {
if IN_POLL.with(|f| f.replace(true)) {
return;
}
struct Guard;
impl Drop for Guard {
fn drop(&mut self) {
IN_POLL.with(|f| f.set(false));
}
}
let _guard = Guard;
let frame = FRAME.with(|f| {
let mut f = f.borrow_mut();
*f = f.wrapping_add(1);
*f
});
let now = Instant::now();
let mut due: Vec<(u64, Callback)> = Vec::new();
let mut remove: Vec<u64> = Vec::new();
let mut need_frames = false;
REGISTRY.with(|r| {
let mut reg = r.borrow_mut();
for (id, entry) in reg.iter_mut() {
let is_due = match entry.due {
Due::At(t) => t <= now,
Due::Frame(f) => {
if frame >= f {
true
} else {
need_frames = true;
false
}
}
};
if !is_due {
continue;
}
due.push((*id, entry.callback.clone()));
let base = match entry.due {
Due::At(t) => t,
Due::Frame(_) => now,
};
match entry.repeat {
Repeat::Once => remove.push(*id),
Repeat::Every { period } => {
entry.due = Due::At(skip_ahead(base, period, now));
}
Repeat::Times { period, left } => {
if left <= 1 {
remove.push(*id);
} else {
entry.repeat = Repeat::Times {
period,
left: left - 1,
};
entry.due = Due::At(skip_ahead(base, period, now));
}
}
}
}
});
for (id, cb) in due.iter() {
let live = REGISTRY.with(|r| r.borrow().contains_key(id));
if live {
cb.borrow_mut()();
}
}
if !remove.is_empty() {
REGISTRY.with(|r| {
let mut reg = r.borrow_mut();
for id in remove {
reg.remove(&id);
}
need_frames = need_frames || reg.values().any(|e| matches!(e.due, Due::Frame(_)));
});
}
if need_frames {
request_frame();
}
}
fn skip_ahead(mut next: Instant, period: Duration, now: Instant) -> Instant {
let mut guard = 0u32;
while next <= now && guard < 1024 {
next = saturating_add(next, period);
guard += 1;
}
if next <= now {
saturating_add(now, period)
} else {
next
}
}
#[derive(Clone)]
pub struct Debouncer {
delay: Duration,
pending: Rc<RefCell<Option<TimerHandle>>>,
}
impl Default for Debouncer {
fn default() -> Self {
Self::new(Duration::from_millis(300))
}
}
impl Debouncer {
pub fn new(delay: Duration) -> Self {
Self {
delay: delay.max(MIN_PERIOD),
pending: Rc::new(RefCell::new(None)),
}
}
pub fn call(&self, cb: impl FnOnce() + 'static) {
*self.pending.borrow_mut() = Some(delay(self.delay, cb));
}
pub fn cancel_pending(&self) {
*self.pending.borrow_mut() = None;
}
}
#[derive(Clone)]
pub struct Throttler {
period: Duration,
last_fire: Rc<RefCell<Option<Instant>>>,
pending: Rc<RefCell<Option<TimerHandle>>>,
}
impl Throttler {
pub fn new(period: Duration) -> Self {
Self {
period: period.max(MIN_PERIOD),
last_fire: Rc::new(RefCell::new(None)),
pending: Rc::new(RefCell::new(None)),
}
}
pub fn call(&self, cb: impl FnOnce() + 'static) {
let now = Instant::now();
let edge = self
.last_fire
.borrow()
.map(|t| saturating_add(t, self.period))
.unwrap_or(now);
if now >= edge {
*self.last_fire.borrow_mut() = Some(now);
cb();
} else {
let last_fire = self.last_fire.clone();
*self.pending.borrow_mut() = Some(delay(edge - now, move || {
*last_fire.borrow_mut() = Some(Instant::now());
cb();
}));
}
}
}
pub fn scoped_delay(duration: Duration, cb: impl FnOnce() + 'static) {
scoped_delay_with_key((), duration, cb);
}
struct ScopedSlot<K> {
key: Option<K>,
alive: Rc<RefCell<bool>>,
installed: bool,
}
pub fn scoped_delay_with_key<K: PartialEq + Clone + 'static>(
key: K,
duration: Duration,
cb: impl FnOnce() + 'static,
) {
let cell: Rc<RefCell<ScopedSlot<K>>> = crate::remember(|| {
RefCell::new(ScopedSlot {
key: None,
alive: Rc::new(RefCell::new(true)),
installed: false,
})
});
let mut slot = cell.borrow_mut();
if !slot.installed {
slot.installed = true;
let cell_c = cell.clone();
crate::scoped_effect(move || {
crate::on_unmount(move || {
*cell_c.borrow().alive.borrow_mut() = false;
})
});
}
if slot.key.as_ref() != Some(&key) {
*slot.alive.borrow_mut() = false;
let alive = Rc::new(RefCell::new(true));
slot.alive = alive.clone();
slot.key = Some(key);
delay(duration, move || {
if *alive.borrow() {
cb();
}
})
.detach();
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration as StdDuration;
fn sleep_ms(ms: u64) {
std::thread::sleep(StdDuration::from_millis(ms));
}
fn reset() {
REGISTRY.with(|r| r.borrow_mut().clear());
}
#[test]
fn delay_fires_after_duration() {
reset();
let fired = Rc::new(RefCell::new(false));
let fired_c = fired.clone();
let _h = delay(Duration::from_millis(5), move || {
*fired_c.borrow_mut() = true;
});
poll();
assert!(!*fired.borrow(), "must not fire before the deadline");
sleep_ms(30);
poll();
assert!(*fired.borrow(), "must fire once the deadline passes");
sleep_ms(30);
poll();
assert!(next_deadline().is_none(), "one-shot must not reschedule");
}
#[test]
fn drop_cancels_delay() {
reset();
let fired = Rc::new(RefCell::new(false));
let fired_c = fired.clone();
let h = delay(Duration::from_millis(5), move || {
*fired_c.borrow_mut() = true;
});
drop(h);
sleep_ms(30);
poll();
assert!(!*fired.borrow(), "cancelled timer must not fire");
}
#[test]
fn interval_repeats_and_drop_stops() {
reset();
let count = Rc::new(RefCell::new(0u32));
let count_c = count.clone();
let h = interval(Duration::from_millis(5), move || {
*count_c.borrow_mut() += 1;
});
sleep_ms(30);
poll();
assert!(
*count.borrow() >= 1,
"must fire at least once, got {}",
*count.borrow()
);
let after_first = *count.borrow();
drop(h);
sleep_ms(30);
poll();
assert_eq!(*count.borrow(), after_first, "dropped interval must stop");
}
#[test]
fn interval_n_fires_exactly_n_times() {
reset();
let count = Rc::new(RefCell::new(0u32));
let count_c = count.clone();
let _h = interval_n(Duration::from_millis(5), 3, move || {
*count_c.borrow_mut() += 1;
});
for _ in 0..10 {
sleep_ms(15);
poll();
}
assert_eq!(*count.borrow(), 3);
assert!(next_deadline().is_none());
}
#[test]
fn delay_frames_counts_polls() {
reset();
let fired = Rc::new(RefCell::new(false));
let fired_c = fired.clone();
let start = frame_count();
let _h = delay_frames(3, move || {
*fired_c.borrow_mut() = true;
});
poll();
poll();
assert!(!*fired.borrow(), "must not fire before 3 polls");
poll();
assert!(*fired.borrow(), "must fire on the 3rd poll");
assert_eq!(frame_count(), start + 3);
}
#[test]
fn debouncer_coalesces_rapid_calls() {
reset();
let count = Rc::new(RefCell::new(0u32));
let deb = Debouncer::new(Duration::from_millis(10));
for _ in 0..5 {
let count_c = count.clone();
deb.call(move || {
*count_c.borrow_mut() += 1;
});
}
sleep_ms(40);
poll();
assert_eq!(
*count.borrow(),
1,
"rapid calls must coalesce into one firing"
);
}
#[test]
fn throttler_leads_and_trails_once() {
reset();
let count = Rc::new(RefCell::new(0u32));
let thro = Throttler::new(Duration::from_millis(50));
for _ in 0..5 {
let count_c = count.clone();
thro.call(move || {
*count_c.borrow_mut() += 1;
});
}
assert_eq!(*count.borrow(), 1, "first call fires immediately");
sleep_ms(80);
poll();
assert_eq!(
*count.borrow(),
2,
"the rest collapse into one trailing firing"
);
}
#[test]
fn same_batch_cancel_suppresses() {
for _ in 0..32 {
reset();
let events: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
let slot: Rc<RefCell<Option<TimerHandle>>> = Rc::new(RefCell::new(None));
let ev_c = events.clone();
let slot_c = slot.clone();
let _first = delay(Duration::from_millis(1), move || {
ev_c.borrow_mut().push("cancel");
*slot_c.borrow_mut() = None;
});
let ev_c = events.clone();
*slot.borrow_mut() = Some(delay(Duration::from_millis(1), move || {
ev_c.borrow_mut().push("second");
}));
sleep_ms(20);
poll();
let ev = events.borrow();
if *ev == ["cancel"] {
return;
}
assert_eq!(
*ev,
["second", "cancel"],
"unexpected event sequence: {ev:?}"
);
}
panic!("canceller never ran first in 32 trials");
}
#[test]
fn reentrant_poll_is_ignored() {
reset();
let count = Rc::new(RefCell::new(0u32));
let count_c = count.clone();
let _h = delay(Duration::from_millis(1), move || {
*count_c.borrow_mut() += 1;
poll();
});
sleep_ms(20);
poll();
poll();
assert_eq!(*count.borrow(), 1, "nested poll must not refire");
reset();
let ticks = Rc::new(RefCell::new(0u32));
let ticks_c = ticks.clone();
let _i = interval(Duration::from_millis(5), move || {
*ticks_c.borrow_mut() += 1;
poll();
});
sleep_ms(30);
poll();
assert_eq!(
*ticks.borrow(),
1,
"nested poll must not double-fire intervals"
);
}
#[test]
fn interval_holds_phase_when_poll_late() {
reset();
let stamps = Rc::new(RefCell::new(Vec::new()));
let stamps_c = stamps.clone();
let period = Duration::from_millis(20);
let _h = interval(period, move || {
stamps_c.borrow_mut().push(Instant::now());
});
sleep_ms(70);
poll();
assert_eq!(stamps.borrow().len(), 1, "one poll fires once at most");
let first = stamps.borrow()[0];
let next = next_deadline().expect("interval must reschedule");
let gap = next.saturating_duration_since(first);
assert!(
gap < period + Duration::from_millis(15),
"reschedule must not drift by the full lateness, gap was {gap:?}"
);
}
}