1use std::{
2 cell::Cell,
3 future::Future,
4 pin::Pin,
5 rc::Rc,
6 task::{Context, Poll},
7 time::Duration,
8};
9
10use wasm_bindgen::{prelude::Closure, JsCast};
11
12fn timeout_to_clear(awoken: bool, timeout_id: Option<i32>) -> Option<i32> {
13 if awoken {
14 None
15 } else {
16 timeout_id
17 }
18}
19
20#[derive(Debug)]
33#[pin_project::pin_project(PinnedDrop)]
34pub struct Delay {
35 inner: Duration,
36 closure: Option<Closure<dyn FnMut()>>,
37 timeout_id: Option<i32>,
38 awoken: Rc<Cell<bool>>,
39}
40
41impl Future for Delay {
42 type Output = ();
43
44 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
45 let this = self.project();
46
47 if !this.awoken.get() {
48 if this.closure.is_none() {
49 let awoken = this.awoken.clone();
50 let callback_ref = this.closure.get_or_insert_with(move || {
51 let waker = cx.waker().clone();
52 let wake = Box::new(move || {
53 waker.wake_by_ref();
54 awoken.set(true);
55 });
56
57 Closure::wrap_assert_unwind_safe(wake as _)
58 });
59
60 let global: web_sys::WorkerGlobalScope = js_sys::global().unchecked_into();
62 let timeout_id = global
63 .set_timeout_with_callback_and_timeout_and_arguments_0(
64 callback_ref.as_ref().unchecked_ref::<js_sys::Function>(),
65 this.inner.as_millis() as i32,
66 )
67 .unwrap();
68 *this.timeout_id = Some(timeout_id);
69 }
70
71 Poll::Pending
72 } else {
73 Poll::Ready(())
74 }
75 }
76}
77
78impl From<Duration> for Delay {
79 fn from(inner: Duration) -> Self {
80 Self {
81 inner,
82 closure: None,
83 timeout_id: None,
84 awoken: Rc::new(Cell::default()),
85 }
86 }
87}
88
89#[pin_project::pinned_drop]
93impl PinnedDrop for Delay {
94 fn drop(self: Pin<&'_ mut Self>) {
95 let this = self.project();
96
97 if let Some(id) = timeout_to_clear(this.awoken.get(), *this.timeout_id) {
98 let global: web_sys::WorkerGlobalScope = js_sys::global().unchecked_into();
99 global.clear_timeout_with_handle(id);
100 }
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::timeout_to_clear;
107
108 #[test]
109 fn clears_timeout_when_not_awoken() {
110 assert_eq!(timeout_to_clear(false, Some(42)), Some(42));
111 }
112
113 #[test]
114 fn does_not_clear_timeout_when_awoken() {
115 assert_eq!(timeout_to_clear(true, Some(42)), None);
116 }
117
118 #[test]
119 fn does_not_clear_without_timeout_id() {
120 assert_eq!(timeout_to_clear(false, None), None);
121 }
122}