rquickjs_extra_timers/
lib.rs

1use std::sync::Arc;
2use std::time::Duration;
3
4use rquickjs::{
5    class::Trace,
6    function::Opt,
7    prelude::Func,
8    JsLifetime, {Class, Ctx, Function, Result},
9};
10use tokio::sync::Notify;
11
12const TARGET: &str = "timers";
13
14#[derive(Trace, JsLifetime)]
15#[rquickjs::class]
16struct Timeout {
17    #[qjs(skip_trace)]
18    abort: Arc<Notify>,
19}
20
21fn clear_timeout(_ctx: Ctx<'_>, timeout: Class<Timeout>) -> Result<()> {
22    timeout.borrow().abort.notify_one();
23    Ok(())
24}
25
26fn set_timeout_interval<'js>(
27    ctx: Ctx<'js>,
28    cb: Function<'js>,
29    msec: Option<u64>,
30    is_interval: bool,
31) -> Result<Class<'js, Timeout>> {
32    let mut msecs = msec.unwrap_or(0);
33    if msecs < 4 {
34        msecs = 4;
35    }
36    let duration = Duration::from_millis(msecs);
37
38    let abort = Arc::new(Notify::new());
39    let abort_ref = abort.clone();
40
41    ctx.spawn(async move {
42        loop {
43            let abort = abort_ref.clone();
44            let aborted;
45
46            let mut interval = tokio::time::interval(duration);
47            interval.tick().await; // Skip the first tick
48            tokio::select! {
49                _ = abort.notified() => {
50                    aborted = true;
51                },
52                _ = interval.tick() => {
53                    aborted = false;
54                }
55            }
56
57            if aborted {
58                break;
59            }
60
61            if let Err(err) = cb.call::<(), ()>(()) {
62                log::error!(target: TARGET, "Failed to call timeout/interval callback: {}", err);
63                break;
64            }
65
66            if !is_interval {
67                break;
68            }
69        }
70        drop(cb);
71        drop(abort_ref);
72    });
73
74    Class::instance(ctx, Timeout { abort })
75}
76
77fn set_timeout<'js>(
78    ctx: Ctx<'js>,
79    cb: Function<'js>,
80    msec: Opt<u64>,
81) -> Result<Class<'js, Timeout>> {
82    set_timeout_interval(ctx, cb, msec.0, false)
83}
84
85fn set_interval<'js>(
86    ctx: Ctx<'js>,
87    cb: Function<'js>,
88    msec: Opt<u64>,
89) -> Result<Class<'js, Timeout>> {
90    set_timeout_interval(ctx, cb, msec.0, true)
91}
92
93fn set_immediate(cb: Function) -> Result<()> {
94    cb.defer::<()>(())?;
95    Ok(())
96}
97
98pub fn init(ctx: &Ctx<'_>) -> Result<()> {
99    let globals = ctx.globals();
100
101    globals.set("setTimeout", Func::from(set_timeout))?;
102    globals.set("clearTimeout", Func::from(clear_timeout))?;
103    globals.set("setInterval", Func::from(set_interval))?;
104    globals.set("clearInterval", Func::from(clear_timeout))?;
105    globals.set("setImmediate", Func::from(set_immediate))?;
106
107    Ok(())
108}
109
110#[cfg(test)]
111mod tests {
112    use futures::FutureExt;
113    use rquickjs::promise::Promise;
114    use rquickjs::CatchResultExt;
115    use rquickjs_extra_test::test_async_with;
116
117    use super::*;
118
119    #[tokio::test]
120    async fn test_set_timeout() {
121        test_async_with(|ctx| {
122            async move {
123                init(&ctx).unwrap();
124
125                let result = ctx
126                    .eval::<Promise, _>(
127                        r#"
128
129                        (async function(){
130                            return new Promise((resolve, reject) => {
131                                setTimeout(() => {
132                                    resolve("Hello World");
133                                }, 100);
134                            });
135                        })()
136                    "#,
137                    )
138                    .catch(&ctx)
139                    .unwrap()
140                    .into_future::<String>()
141                    .await
142                    .catch(&ctx)
143                    .unwrap();
144
145                assert_eq!("Hello World", result);
146            }
147            .boxed_local()
148        })
149        .await
150    }
151
152    #[tokio::test]
153    async fn test_set_interval() {
154        test_async_with(|ctx| {
155            async move {
156                init(&ctx).unwrap();
157
158                let result = ctx
159                    .eval::<Promise, _>(
160                        r#"
161
162                        (async function(){
163                            return new Promise((resolve, reject) => {
164                                let count = 0;
165                                setInterval(() => {
166                                    if (++count === 3) {
167                                        resolve(count);
168                                    }
169                                }, 100);
170                            });
171                        })()
172                    "#,
173                    )
174                    .catch(&ctx)
175                    .unwrap()
176                    .into_future::<u32>()
177                    .await
178                    .catch(&ctx)
179                    .unwrap();
180
181                assert_eq!(3, result);
182            }
183            .boxed_local()
184        })
185        .await
186    }
187}