rquickjs_extra_timers/
lib.rs

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