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