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