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 REGISTRY.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 IN_POLL.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
412pub fn scoped_delay_with_key<K: PartialEq + Clone + 'static>(
416 key: K,
417 duration: Duration,
418 cb: impl FnOnce() + 'static,
419) {
420 let cell: Rc<RefCell<ScopedSlot<K>>> = crate::remember(|| {
421 RefCell::new(ScopedSlot {
422 key: None,
423 alive: Rc::new(RefCell::new(true)),
424 installed: false,
425 })
426 });
427 let mut slot = cell.borrow_mut();
428 if !slot.installed {
429 slot.installed = true;
430 let cell_c = cell.clone();
432 crate::scoped_effect(move || {
433 crate::on_unmount(move || {
434 *cell_c.borrow().alive.borrow_mut() = false;
435 })
436 });
437 }
438 if slot.key.as_ref() != Some(&key) {
439 *slot.alive.borrow_mut() = false;
440 let alive = Rc::new(RefCell::new(true));
441 slot.alive = alive.clone();
442 slot.key = Some(key);
443 delay(duration, move || {
444 if *alive.borrow() {
445 cb();
446 }
447 })
448 .detach();
449 }
450}
451
452#[cfg(test)]
453mod tests {
454 use super::*;
455 use std::time::Duration as StdDuration;
456
457 fn sleep_ms(ms: u64) {
458 std::thread::sleep(StdDuration::from_millis(ms));
459 }
460
461 fn reset() {
463 REGISTRY.with(|r| r.borrow_mut().clear());
464 }
465
466 #[test]
467 fn delay_fires_after_duration() {
468 reset();
469 let fired = Rc::new(RefCell::new(false));
470 let fired_c = fired.clone();
471 let _h = delay(Duration::from_millis(5), move || {
472 *fired_c.borrow_mut() = true;
473 });
474 poll();
475 assert!(!*fired.borrow(), "must not fire before the deadline");
476 sleep_ms(30);
477 poll();
478 assert!(*fired.borrow(), "must fire once the deadline passes");
479 sleep_ms(30);
480 poll();
481 assert!(next_deadline().is_none(), "one-shot must not reschedule");
482 }
483
484 #[test]
485 fn drop_cancels_delay() {
486 reset();
487 let fired = Rc::new(RefCell::new(false));
488 let fired_c = fired.clone();
489 let h = delay(Duration::from_millis(5), move || {
490 *fired_c.borrow_mut() = true;
491 });
492 drop(h);
493 sleep_ms(30);
494 poll();
495 assert!(!*fired.borrow(), "cancelled timer must not fire");
496 }
497
498 #[test]
499 fn interval_repeats_and_drop_stops() {
500 reset();
501 let count = Rc::new(RefCell::new(0u32));
502 let count_c = count.clone();
503 let h = interval(Duration::from_millis(5), move || {
504 *count_c.borrow_mut() += 1;
505 });
506 sleep_ms(30);
507 poll();
508 assert!(
509 *count.borrow() >= 1,
510 "must fire at least once, got {}",
511 *count.borrow()
512 );
513 let after_first = *count.borrow();
514 drop(h);
515 sleep_ms(30);
516 poll();
517 assert_eq!(*count.borrow(), after_first, "dropped interval must stop");
518 }
519
520 #[test]
521 fn interval_n_fires_exactly_n_times() {
522 reset();
523 let count = Rc::new(RefCell::new(0u32));
524 let count_c = count.clone();
525 let _h = interval_n(Duration::from_millis(5), 3, move || {
526 *count_c.borrow_mut() += 1;
527 });
528 for _ in 0..10 {
529 sleep_ms(15);
530 poll();
531 }
532 assert_eq!(*count.borrow(), 3);
533 assert!(next_deadline().is_none());
534 }
535
536 #[test]
537 fn delay_frames_counts_polls() {
538 reset();
539 let fired = Rc::new(RefCell::new(false));
540 let fired_c = fired.clone();
541 let start = frame_count();
542 let _h = delay_frames(3, move || {
543 *fired_c.borrow_mut() = true;
544 });
545 poll();
546 poll();
547 assert!(!*fired.borrow(), "must not fire before 3 polls");
548 poll();
549 assert!(*fired.borrow(), "must fire on the 3rd poll");
550 assert_eq!(frame_count(), start + 3);
551 }
552
553 #[test]
554 fn debouncer_coalesces_rapid_calls() {
555 reset();
556 let count = Rc::new(RefCell::new(0u32));
557 let deb = Debouncer::new(Duration::from_millis(10));
558 for _ in 0..5 {
559 let count_c = count.clone();
560 deb.call(move || {
561 *count_c.borrow_mut() += 1;
562 });
563 }
564 sleep_ms(40);
565 poll();
566 assert_eq!(
567 *count.borrow(),
568 1,
569 "rapid calls must coalesce into one firing"
570 );
571 }
572
573 #[test]
574 fn throttler_leads_and_trails_once() {
575 reset();
576 let count = Rc::new(RefCell::new(0u32));
577 let thro = Throttler::new(Duration::from_millis(50));
578 for _ in 0..5 {
579 let count_c = count.clone();
580 thro.call(move || {
581 *count_c.borrow_mut() += 1;
582 });
583 }
584 assert_eq!(*count.borrow(), 1, "first call fires immediately");
585 sleep_ms(80);
586 poll();
587 assert_eq!(
588 *count.borrow(),
589 2,
590 "the rest collapse into one trailing firing"
591 );
592 }
593
594 #[test]
595 fn same_batch_cancel_suppresses() {
596 for _ in 0..32 {
600 reset();
601 let events: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
602 let slot: Rc<RefCell<Option<TimerHandle>>> = Rc::new(RefCell::new(None));
603 let ev_c = events.clone();
604 let slot_c = slot.clone();
605 let _first = delay(Duration::from_millis(1), move || {
606 ev_c.borrow_mut().push("cancel");
607 *slot_c.borrow_mut() = None;
608 });
609 let ev_c = events.clone();
610 *slot.borrow_mut() = Some(delay(Duration::from_millis(1), move || {
611 ev_c.borrow_mut().push("second");
612 }));
613 sleep_ms(20);
614 poll();
615 let ev = events.borrow();
616 if *ev == ["cancel"] {
617 return;
618 }
619 assert_eq!(
620 *ev,
621 ["second", "cancel"],
622 "unexpected event sequence: {ev:?}"
623 );
624 }
625 panic!("canceller never ran first in 32 trials");
626 }
627
628 #[test]
629 fn reentrant_poll_is_ignored() {
630 reset();
631 let count = Rc::new(RefCell::new(0u32));
632 let count_c = count.clone();
633 let _h = delay(Duration::from_millis(1), move || {
634 *count_c.borrow_mut() += 1;
635 poll();
636 });
637 sleep_ms(20);
638 poll();
639 poll();
640 assert_eq!(*count.borrow(), 1, "nested poll must not refire");
641
642 reset();
643 let ticks = Rc::new(RefCell::new(0u32));
644 let ticks_c = ticks.clone();
645 let _i = interval(Duration::from_millis(5), move || {
646 *ticks_c.borrow_mut() += 1;
647 poll();
648 });
649 sleep_ms(30);
650 poll();
651 assert_eq!(
652 *ticks.borrow(),
653 1,
654 "nested poll must not double-fire intervals"
655 );
656 }
657
658 #[test]
659 fn interval_holds_phase_when_poll_late() {
660 reset();
661 let stamps = Rc::new(RefCell::new(Vec::new()));
662 let stamps_c = stamps.clone();
663 let period = Duration::from_millis(20);
664 let _h = interval(period, move || {
665 stamps_c.borrow_mut().push(Instant::now());
666 });
667 sleep_ms(70);
670 poll();
671 assert_eq!(stamps.borrow().len(), 1, "one poll fires once at most");
672 let first = stamps.borrow()[0];
673 let next = next_deadline().expect("interval must reschedule");
674 let gap = next.saturating_duration_since(first);
675 assert!(
676 gap < period + Duration::from_millis(15),
677 "reschedule must not drift by the full lateness, gap was {gap:?}"
678 );
679 }
680}