rquickjs_extra_timers/
lib.rs1use 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; 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}