1use std::cell::{Cell, RefCell};
31use std::rc::Rc;
32
33use rustc_hash::FxHashMap;
34
35use web_time::{Duration, Instant};
36
37use crate::{request_frame, unique_component_id};
38
39const MIN_PERIOD: Duration = Duration::from_millis(1);
42
43thread_local! {
44 static REGISTRY: RefCell<FxHashMap<u64, Entry>> = RefCell::new(FxHashMap::default());
45 static FRAME: Cell<u64> = const { Cell::new(0) };
47 static IN_POLL: Cell<bool> = const { Cell::new(false) };
50}
51
52type Callback = Rc<RefCell<Box<dyn FnMut()>>>;
53
54#[derive(Clone, Copy)]
55enum Due {
56 At(Instant),
58 Frame(u64),
60}
61
62#[derive(Clone, Copy)]
63enum Repeat {
64 Once,
65 Every { period: Duration },
66 Times { period: Duration, left: u32 },
67}
68
69struct Entry {
70 due: Due,
71 repeat: Repeat,
72 callback: Callback,
73}
74
75#[must_use = "dropping the handle cancels the timer"]
77pub struct TimerHandle {
78 id: Option<u64>,
79}
80
81impl TimerHandle {
82 pub fn cancel(mut self) {
84 self.cancel_now();
85 }
86
87 pub fn detach(self) {
92 std::mem::forget(self);
93 }
94
95 fn cancel_now(&mut self) {
96 if let Some(id) = self.id.take() {
97 cancel(id);
98 }
99 }
100}
101
102impl Drop for TimerHandle {
103 fn drop(&mut self) {
104 self.cancel_now();
105 }
106}
107
108fn insert(due: Due, repeat: Repeat, callback: Callback) -> TimerHandle {
109 let id = unique_component_id();
110 let old = REGISTRY.with(|r| {
111 r.borrow_mut().insert(
112 id,
113 Entry {
114 due,
115 repeat,
116 callback,
117 },
118 )
119 });
120 drop(old);
121 request_frame();
122 TimerHandle { id: Some(id) }
123}
124
125fn cancel(id: u64) {
126 let removed = REGISTRY
127 .try_with(|r| {
128 r.try_borrow_mut()
129 .ok()
130 .and_then(|mut registry| registry.remove(&id))
131 })
132 .ok()
133 .flatten();
134 drop(removed);
135}
136
137fn wrap_once(cb: impl FnOnce() + 'static) -> Callback {
138 let mut cb = Some(cb);
139 Rc::new(RefCell::new(Box::new(move || {
140 if let Some(f) = cb.take() {
141 f();
142 }
143 }) as Box<dyn FnMut()>))
144}
145
146pub fn delay(duration: Duration, cb: impl FnOnce() + 'static) -> TimerHandle {
151 insert(
152 Due::At(saturating_add(Instant::now(), duration)),
153 Repeat::Once,
154 wrap_once(cb),
155 )
156}
157
158pub fn timeout(duration: Duration, cb: impl FnOnce() + 'static) -> TimerHandle {
162 delay(duration, cb)
163}
164
165pub fn delay_frames(frames: u32, cb: impl FnOnce() + 'static) -> TimerHandle {
170 let at = FRAME.with(|f| f.get().wrapping_add(frames as u64));
171 insert(Due::Frame(at), Repeat::Once, wrap_once(cb))
172}
173
174pub fn interval(period: Duration, cb: impl FnMut() + 'static) -> TimerHandle {
178 let period = period.max(MIN_PERIOD);
179 insert(
180 Due::At(saturating_add(Instant::now(), period)),
181 Repeat::Every { period },
182 Rc::new(RefCell::new(Box::new(cb) as Box<dyn FnMut()>)),
183 )
184}
185
186pub fn interval_n(period: Duration, times: u32, cb: impl FnMut() + 'static) -> TimerHandle {
189 if times == 0 {
190 return TimerHandle { id: None };
191 }
192 let period = period.max(MIN_PERIOD);
193 insert(
194 Due::At(saturating_add(Instant::now(), period)),
195 Repeat::Times {
196 period,
197 left: times,
198 },
199 Rc::new(RefCell::new(Box::new(cb) as Box<dyn FnMut()>)),
200 )
201}
202
203pub fn frame_count() -> u64 {
205 FRAME.with(Cell::get)
206}
207
208pub fn next_deadline() -> Option<Instant> {
212 REGISTRY.with(|r| {
213 r.borrow()
214 .values()
215 .filter_map(|e| match e.due {
216 Due::At(t) => Some(t),
217 Due::Frame(_) => None,
218 })
219 .min()
220 })
221}
222
223fn saturating_add(t: Instant, d: Duration) -> Instant {
226 t.checked_add(d).unwrap_or(t)
227}
228
229pub fn poll() {
234 if IN_POLL.with(|f| f.replace(true)) {
235 return;
236 }
237 struct Guard;
238 impl Drop for Guard {
239 fn drop(&mut self) {
240 let _ = IN_POLL.try_with(|f| f.set(false));
241 }
242 }
243 let _guard = Guard;
244 let frame = FRAME.with(|f| {
245 let frame = f.get().wrapping_add(1);
246 f.set(frame);
247 frame
248 });
249 let now = Instant::now();
250 let mut due: Vec<(u64, Callback)> = Vec::new();
253 let mut remove: Vec<u64> = Vec::new();
254 let mut need_frames = false;
255 REGISTRY.with(|r| {
256 let mut reg = r.borrow_mut();
257 for (id, entry) in reg.iter_mut() {
258 let is_due = match entry.due {
259 Due::At(t) => t <= now,
260 Due::Frame(f) => {
261 if frame >= f {
262 true
263 } else {
264 need_frames = true;
265 false
266 }
267 }
268 };
269 if !is_due {
270 continue;
271 }
272 due.push((*id, entry.callback.clone()));
273 let base = match entry.due {
275 Due::At(t) => t,
276 Due::Frame(_) => now,
277 };
278 match entry.repeat {
279 Repeat::Once => remove.push(*id),
280 Repeat::Every { period } => {
281 entry.due = Due::At(skip_ahead(base, period, now));
282 }
283 Repeat::Times { period, left } => {
284 if left <= 1 {
285 remove.push(*id);
286 } else {
287 entry.repeat = Repeat::Times {
288 period,
289 left: left - 1,
290 };
291 entry.due = Due::At(skip_ahead(base, period, now));
292 }
293 }
294 }
295 }
296 });
297 for (id, cb) in due.iter() {
298 let live = REGISTRY.with(|r| r.borrow().contains_key(id));
299 if live {
300 cb.borrow_mut()();
301 }
302 }
303 if !remove.is_empty() {
304 let removed_entries = REGISTRY.with(|r| {
305 let mut reg = r.borrow_mut();
306 let removed = remove
307 .into_iter()
308 .filter_map(|id| reg.remove(&id))
309 .collect::<Vec<_>>();
310 need_frames = need_frames || reg.values().any(|e| matches!(e.due, Due::Frame(_)));
311 removed
312 });
313 drop(removed_entries);
314 }
315 if need_frames {
316 request_frame();
317 }
318}
319
320fn skip_ahead(mut next: Instant, period: Duration, now: Instant) -> Instant {
322 let mut guard = 0u32;
323 while next <= now && guard < 1024 {
324 next = saturating_add(next, period);
325 guard += 1;
326 }
327 if next <= now {
328 saturating_add(now, period)
329 } else {
330 next
331 }
332}
333
334fn replace_timer_slot(slot: &Rc<RefCell<Option<TimerHandle>>>, value: Option<TimerHandle>) {
335 let old = slot
336 .try_borrow_mut()
337 .ok()
338 .map(|mut current| std::mem::replace(&mut *current, value));
339 drop(old);
340}
341
342#[derive(Clone)]
345pub struct Debouncer {
346 delay: Duration,
347 pending: Rc<RefCell<Option<TimerHandle>>>,
348}
349
350impl Default for Debouncer {
351 fn default() -> Self {
353 Self::new(Duration::from_millis(300))
354 }
355}
356
357impl Debouncer {
358 pub fn new(delay: Duration) -> Self {
360 Self {
361 delay: delay.max(MIN_PERIOD),
362 pending: Rc::new(RefCell::new(None)),
363 }
364 }
365
366 pub fn call(&self, cb: impl FnOnce() + 'static) {
368 replace_timer_slot(&self.pending, Some(delay(self.delay, cb)));
369 }
370
371 pub fn cancel_pending(&self) {
373 replace_timer_slot(&self.pending, None);
374 }
375}
376
377#[derive(Clone)]
379pub struct Throttler {
380 period: Duration,
381 last_fire: Rc<RefCell<Option<Instant>>>,
382 pending: Rc<RefCell<Option<TimerHandle>>>,
383}
384
385impl Throttler {
386 pub fn new(period: Duration) -> Self {
388 Self {
389 period: period.max(MIN_PERIOD),
390 last_fire: Rc::new(RefCell::new(None)),
391 pending: Rc::new(RefCell::new(None)),
392 }
393 }
394
395 pub fn call(&self, cb: impl FnOnce() + 'static) {
399 let now = Instant::now();
400 let edge = self
401 .last_fire
402 .borrow()
403 .map(|t| saturating_add(t, self.period))
404 .unwrap_or(now);
405 if now >= edge {
406 *self.last_fire.borrow_mut() = Some(now);
407 cb();
408 } else {
409 let last_fire = self.last_fire.clone();
410 replace_timer_slot(
411 &self.pending,
412 Some(delay(edge - now, move || {
413 *last_fire.borrow_mut() = Some(Instant::now());
414 cb();
415 })),
416 );
417 }
418 }
419}
420
421pub fn scoped_delay(duration: Duration, cb: impl FnOnce() + 'static) {
425 scoped_delay_with_key((), duration, cb);
426}
427
428struct ScopedSlot<K> {
429 key: Option<K>,
430 alive: Rc<RefCell<bool>>,
431 installed: bool,
432}
433
434#[track_caller]
438pub fn scoped_delay_with_key<K: PartialEq + Clone + 'static>(
439 key: K,
440 duration: Duration,
441 cb: impl FnOnce() + 'static,
442) {
443 let loc = std::panic::Location::caller();
444 let callsite = format!(
445 "scoped_delay:{}:{}:{}",
446 loc.file(),
447 loc.line(),
448 loc.column()
449 );
450 let cell: Rc<RefCell<ScopedSlot<K>>> = crate::remember_with_key(callsite, || {
451 RefCell::new(ScopedSlot {
452 key: None,
453 alive: Rc::new(RefCell::new(true)),
454 installed: false,
455 })
456 });
457 let install = {
458 let mut slot = cell.borrow_mut();
459 if !slot.installed {
460 slot.installed = true;
461 true
462 } else {
463 false
464 }
465 };
466 if install {
467 let cell_c = cell.clone();
468 crate::scoped_effect(move || {
469 crate::on_unmount(move || {
470 *cell_c.borrow().alive.borrow_mut() = false;
471 })
472 });
473 }
474 let alive = Rc::new(RefCell::new(true));
475 let old_values = {
476 let mut slot = cell.borrow_mut();
477 if slot.key.as_ref() != Some(&key) {
478 *slot.alive.borrow_mut() = false;
479 let old_alive = std::mem::replace(&mut slot.alive, alive.clone());
480 let old_key = slot.key.replace(key.clone());
481 Some((old_alive, old_key))
482 } else {
483 None
484 }
485 };
486 if let Some((old_alive, old_key)) = old_values {
487 drop(old_alive);
488 drop(old_key);
489 delay(duration, move || {
490 if *alive.borrow() {
491 cb();
492 }
493 })
494 .detach();
495 }
496}
497
498#[cfg(test)]
499mod tests {
500 use super::*;
501 use std::time::Duration as StdDuration;
502
503 fn sleep_ms(ms: u64) {
504 std::thread::sleep(StdDuration::from_millis(ms));
505 }
506
507 fn reset() {
509 let entries = REGISTRY.with(|r| std::mem::take(&mut *r.borrow_mut()));
510 drop(entries);
511 }
512
513 #[test]
514 fn delay_fires_after_duration() {
515 reset();
516 let fired = Rc::new(RefCell::new(false));
517 let fired_c = fired.clone();
518 let _h = delay(Duration::from_millis(5), move || {
519 *fired_c.borrow_mut() = true;
520 });
521 poll();
522 assert!(!*fired.borrow(), "must not fire before the deadline");
523 sleep_ms(30);
524 poll();
525 assert!(*fired.borrow(), "must fire once the deadline passes");
526 sleep_ms(30);
527 poll();
528 assert!(next_deadline().is_none(), "one-shot must not reschedule");
529 }
530
531 #[test]
532 fn drop_cancels_delay() {
533 reset();
534 let fired = Rc::new(RefCell::new(false));
535 let fired_c = fired.clone();
536 let h = delay(Duration::from_millis(5), move || {
537 *fired_c.borrow_mut() = true;
538 });
539 drop(h);
540 sleep_ms(30);
541 poll();
542 assert!(!*fired.borrow(), "cancelled timer must not fire");
543 }
544
545 #[test]
546 fn interval_repeats_and_drop_stops() {
547 reset();
548 let count = Rc::new(RefCell::new(0u32));
549 let count_c = count.clone();
550 let h = interval(Duration::from_millis(5), move || {
551 *count_c.borrow_mut() += 1;
552 });
553 sleep_ms(30);
554 poll();
555 assert!(
556 *count.borrow() >= 1,
557 "must fire at least once, got {}",
558 *count.borrow()
559 );
560 let after_first = *count.borrow();
561 drop(h);
562 sleep_ms(30);
563 poll();
564 assert_eq!(*count.borrow(), after_first, "dropped interval must stop");
565 }
566
567 #[test]
568 fn interval_n_fires_exactly_n_times() {
569 reset();
570 let count = Rc::new(RefCell::new(0u32));
571 let count_c = count.clone();
572 let _h = interval_n(Duration::from_millis(5), 3, move || {
573 *count_c.borrow_mut() += 1;
574 });
575 for _ in 0..10 {
576 sleep_ms(15);
577 poll();
578 }
579 assert_eq!(*count.borrow(), 3);
580 assert!(next_deadline().is_none());
581 }
582
583 #[test]
584 fn delay_frames_counts_polls() {
585 reset();
586 let fired = Rc::new(RefCell::new(false));
587 let fired_c = fired.clone();
588 let start = frame_count();
589 let _h = delay_frames(3, move || {
590 *fired_c.borrow_mut() = true;
591 });
592 poll();
593 poll();
594 assert!(!*fired.borrow(), "must not fire before 3 polls");
595 poll();
596 assert!(*fired.borrow(), "must fire on the 3rd poll");
597 assert_eq!(frame_count(), start + 3);
598 }
599
600 #[test]
601 fn debouncer_coalesces_rapid_calls() {
602 reset();
603 let count = Rc::new(RefCell::new(0u32));
604 let deb = Debouncer::new(Duration::from_millis(10));
605 for _ in 0..5 {
606 let count_c = count.clone();
607 deb.call(move || {
608 *count_c.borrow_mut() += 1;
609 });
610 }
611 sleep_ms(40);
612 poll();
613 assert_eq!(
614 *count.borrow(),
615 1,
616 "rapid calls must coalesce into one firing"
617 );
618 }
619
620 #[test]
621 fn throttler_leads_and_trails_once() {
622 reset();
623 let count = Rc::new(RefCell::new(0u32));
624 let thro = Throttler::new(Duration::from_millis(50));
625 for _ in 0..5 {
626 let count_c = count.clone();
627 thro.call(move || {
628 *count_c.borrow_mut() += 1;
629 });
630 }
631 assert_eq!(*count.borrow(), 1, "first call fires immediately");
632 sleep_ms(80);
633 poll();
634 assert_eq!(
635 *count.borrow(),
636 2,
637 "the rest collapse into one trailing firing"
638 );
639 }
640
641 #[test]
642 fn same_batch_cancel_suppresses() {
643 for _ in 0..32 {
647 reset();
648 let events: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
649 let slot: Rc<RefCell<Option<TimerHandle>>> = Rc::new(RefCell::new(None));
650 let ev_c = events.clone();
651 let slot_c = slot.clone();
652 let _first = delay(Duration::from_millis(1), move || {
653 ev_c.borrow_mut().push("cancel");
654 *slot_c.borrow_mut() = None;
655 });
656 let ev_c = events.clone();
657 *slot.borrow_mut() = Some(delay(Duration::from_millis(1), move || {
658 ev_c.borrow_mut().push("second");
659 }));
660 sleep_ms(20);
661 poll();
662 let ev = events.borrow();
663 if *ev == ["cancel"] {
664 return;
665 }
666 assert_eq!(
667 *ev,
668 ["second", "cancel"],
669 "unexpected event sequence: {ev:?}"
670 );
671 }
672 panic!("canceller never ran first in 32 trials");
673 }
674
675 #[test]
676 fn reentrant_poll_is_ignored() {
677 reset();
678 let count = Rc::new(RefCell::new(0u32));
679 let count_c = count.clone();
680 let _h = delay(Duration::from_millis(1), move || {
681 *count_c.borrow_mut() += 1;
682 poll();
683 });
684 sleep_ms(20);
685 poll();
686 poll();
687 assert_eq!(*count.borrow(), 1, "nested poll must not refire");
688
689 reset();
690 let ticks = Rc::new(RefCell::new(0u32));
691 let ticks_c = ticks.clone();
692 let _i = interval(Duration::from_millis(5), move || {
693 *ticks_c.borrow_mut() += 1;
694 poll();
695 });
696 sleep_ms(30);
697 poll();
698 assert_eq!(
699 *ticks.borrow(),
700 1,
701 "nested poll must not double-fire intervals"
702 );
703 }
704
705 #[test]
706 fn interval_holds_phase_when_poll_late() {
707 reset();
708 let stamps = Rc::new(RefCell::new(Vec::new()));
709 let stamps_c = stamps.clone();
710 let period = Duration::from_millis(20);
711 let _h = interval(period, move || {
712 stamps_c.borrow_mut().push(Instant::now());
713 });
714 sleep_ms(70);
717 poll();
718 assert_eq!(stamps.borrow().len(), 1, "one poll fires once at most");
719 let first = stamps.borrow()[0];
720 let next = next_deadline().expect("interval must reschedule");
721 let gap = next.saturating_duration_since(first);
722 assert!(
723 gap < period + Duration::from_millis(15),
724 "reschedule must not drift by the full lateness, gap was {gap:?}"
725 );
726 }
727}