quickjs_runtime/features/
set_timeout.rs

1use crate::jsutils::JsError;
2use crate::quickjs_utils;
3use crate::quickjs_utils::{functions, get_global, objects, parse_args, primitives};
4use crate::quickjsruntimeadapter::QuickJsRuntimeAdapter;
5use hirofa_utils::eventloop::EventLoop;
6use libquickjs_sys as q;
7use std::time::Duration;
8
9/// provides the setImmediate methods for the runtime
10/// # Example
11/// ```rust
12/// use quickjs_runtime::builder::QuickJsRuntimeBuilder;
13/// use quickjs_runtime::jsutils::Script;
14/// use std::time::Duration;
15/// let rt = QuickJsRuntimeBuilder::new().build();
16/// rt.eval_sync(None, Script::new("test_timeout.es", "setTimeout(() => {console.log('timed logging')}, 1000);")).expect("script failed");
17/// std::thread::sleep(Duration::from_secs(2));
18/// ```
19pub fn init(q_js_rt: &QuickJsRuntimeAdapter) -> Result<(), JsError> {
20    log::trace!("set_timeout::init");
21
22    q_js_rt.add_context_init_hook(|_q_js_rt, q_ctx| {
23        let global = unsafe { get_global(q_ctx.context) };
24        #[cfg(feature = "settimeout")]
25        {
26            let set_timeout_func =
27                functions::new_native_function_q(q_ctx, "setTimeout", Some(set_timeout), 2, false)?;
28            let clear_timeout_func = functions::new_native_function_q(
29                q_ctx,
30                "clearTimeout",
31                Some(clear_timeout),
32                1,
33                false,
34            )?;
35            objects::set_property2_q(q_ctx, &global, "setTimeout", &set_timeout_func, 0)?;
36            objects::set_property2_q(q_ctx, &global, "clearTimeout", &clear_timeout_func, 0)?;
37        }
38        #[cfg(feature = "setinterval")]
39        {
40            let set_interval_func = functions::new_native_function_q(
41                q_ctx,
42                "setInterval",
43                Some(set_interval),
44                2,
45                false,
46            )?;
47            let clear_interval_func = functions::new_native_function_q(
48                q_ctx,
49                "clearInterval",
50                Some(clear_interval),
51                1,
52                false,
53            )?;
54
55            objects::set_property2_q(q_ctx, &global, "setInterval", &set_interval_func, 0)?;
56            objects::set_property2_q(q_ctx, &global, "clearInterval", &clear_interval_func, 0)?;
57        }
58        Ok(())
59    })?;
60    Ok(())
61}
62
63#[cfg(feature = "settimeout")]
64unsafe extern "C" fn set_timeout(
65    context: *mut q::JSContext,
66    _this_val: q::JSValue,
67    argc: ::std::os::raw::c_int,
68    argv: *mut q::JSValue,
69) -> q::JSValue {
70    log::trace!("> set_timeout");
71
72    let args = parse_args(context, argc, argv);
73
74    QuickJsRuntimeAdapter::do_with(move |q_js_rt| {
75        let q_ctx = q_js_rt.get_quickjs_context(context);
76        if args.is_empty() {
77            return q_ctx.report_ex("setTimeout requires at least one argument");
78        }
79        if !functions::is_function(context, &args[0]) {
80            return q_ctx.report_ex("setTimeout requires a function as first arg");
81        }
82
83        if args.len() >= 2 && !args[1].is_i32() && !args[1].is_f64() {
84            return q_ctx.report_ex("setTimeout requires a number as second arg");
85        }
86
87        let delay_ms = if args.len() >= 2 {
88            let delay_ref = &args[1];
89            if delay_ref.is_i32() {
90                primitives::to_i32(delay_ref).ok().unwrap() as u64
91            } else {
92                primitives::to_f64(delay_ref).ok().unwrap() as u64
93            }
94        } else {
95            0
96        };
97
98        let q_ctx_id = q_ctx.id.clone();
99
100        let id = EventLoop::add_timeout(
101            move || {
102                QuickJsRuntimeAdapter::do_with(|q_js_rt| {
103                    let func = &args[0];
104                    if let Some(q_ctx) = q_js_rt.opt_context(q_ctx_id.as_str()) {
105                        match functions::call_function_q(q_ctx, func, &args[2..], None) {
106                            Ok(_) => {}
107                            Err(e) => {
108                                log::error!("setTimeout func failed: {}", e);
109                            }
110                        };
111                    } else {
112                        log::error!("setTimeout func failed: no such context: {}", q_ctx_id);
113                    }
114                    q_js_rt.run_pending_jobs_if_any();
115                })
116            },
117            Duration::from_millis(delay_ms),
118        );
119        log::trace!("set_timeout: {}", id);
120        primitives::from_i32(id).clone_value_incr_rc()
121    })
122}
123
124#[cfg(feature = "setinterval")]
125unsafe extern "C" fn set_interval(
126    context: *mut q::JSContext,
127    _this_val: q::JSValue,
128    argc: ::std::os::raw::c_int,
129    argv: *mut q::JSValue,
130) -> q::JSValue {
131    log::trace!("> set_interval");
132
133    let args = parse_args(context, argc, argv);
134
135    QuickJsRuntimeAdapter::do_with(|q_js_rt| {
136        let q_ctx = q_js_rt.get_quickjs_context(context);
137        if args.is_empty() {
138            return q_ctx.report_ex("setInterval requires at least one argument");
139        }
140        if !functions::is_function(context, &args[0]) {
141            return q_ctx.report_ex("setInterval requires a functions as first arg");
142        }
143
144        if args.len() >= 2 && !args[1].is_i32() && !args[1].is_f64() {
145            return q_ctx.report_ex("setInterval requires a number as second arg");
146        }
147
148        let delay_ms = if args.len() >= 2 {
149            let delay_ref = &args[1];
150            if delay_ref.is_i32() {
151                primitives::to_i32(delay_ref).ok().unwrap() as u64
152            } else {
153                primitives::to_f64(delay_ref).ok().unwrap() as u64
154            }
155        } else {
156            0
157        };
158
159        let q_ctx_id = q_ctx.id.clone();
160
161        let id = EventLoop::add_interval(
162            move || {
163                QuickJsRuntimeAdapter::do_with(|q_js_rt| {
164                    if let Some(q_ctx) = q_js_rt.opt_context(q_ctx_id.as_str()) {
165                        let func = &args[0];
166
167                        match functions::call_function_q(q_ctx, func, &args[2..], None) {
168                            Ok(_) => {}
169                            Err(e) => {
170                                log::error!("setInterval func failed: {}", e);
171                            }
172                        };
173                    } else {
174                        log::error!("setInterval func failed: no such context: {}", q_ctx_id);
175                    }
176                    q_js_rt.run_pending_jobs_if_any();
177                })
178            },
179            Duration::from_millis(delay_ms),
180            Duration::from_millis(delay_ms),
181        );
182        log::trace!("set_interval: {}", id);
183        primitives::from_i32(id).clone_value_incr_rc()
184    })
185}
186
187#[cfg(feature = "setinterval")]
188unsafe extern "C" fn clear_interval(
189    context: *mut q::JSContext,
190    _this_val: q::JSValue,
191    argc: ::std::os::raw::c_int,
192    argv: *mut q::JSValue,
193) -> q::JSValue {
194    log::trace!("> clear_interval");
195
196    let args = parse_args(context, argc, argv);
197    QuickJsRuntimeAdapter::do_with(|q_js_rt| {
198        let q_ctx = q_js_rt.get_quickjs_context(context);
199        if args.is_empty() {
200            return q_ctx.report_ex("clearInterval requires at least one argument");
201        }
202        if !&args[0].is_i32() {
203            return q_ctx.report_ex("clearInterval requires a number as first arg");
204        }
205        let id = primitives::to_i32(&args[0]).ok().unwrap();
206        log::trace!("clear_interval: {}", id);
207        EventLoop::clear_interval(id);
208        quickjs_utils::new_null()
209    })
210}
211
212#[cfg(feature = "settimeout")]
213unsafe extern "C" fn clear_timeout(
214    context: *mut q::JSContext,
215    _this_val: q::JSValue,
216    argc: ::std::os::raw::c_int,
217    argv: *mut q::JSValue,
218) -> q::JSValue {
219    log::trace!("> clear_timeout");
220
221    let args = parse_args(context, argc, argv);
222
223    QuickJsRuntimeAdapter::do_with(move |q_js_rt| {
224        let q_ctx = q_js_rt.get_quickjs_context(context);
225        if args.is_empty() {
226            return q_ctx.report_ex("clearTimeout requires at least one argument");
227        }
228        if !&args[0].is_i32() {
229            return q_ctx.report_ex("clearTimeout requires a number as first arg");
230        }
231        let id = primitives::to_i32(&args[0]).ok().unwrap();
232        log::trace!("clear_timeout: {}", id);
233
234        EventLoop::clear_timeout(id);
235
236        quickjs_utils::new_null()
237    })
238}
239
240#[cfg(test)]
241pub mod tests {
242    use crate::facades::tests::init_test_rt;
243    use crate::jsutils::Script;
244    use crate::quickjs_utils::get_global_q;
245    use crate::quickjs_utils::objects::get_property_q;
246    use crate::quickjs_utils::primitives::to_i32;
247    use crate::values::JsValueFacade;
248    use std::time::Duration;
249
250    #[test]
251    fn test_set_timeout_prom_res() {
252        let rt = init_test_rt();
253        let esvf = rt
254            .eval_sync(
255                None,
256                Script::new(
257                    "test_set_timeout_prom_res.es",
258                    "new Promise((resolve, reject) => {\
259                                setTimeout(() => {resolve(123);}, 1000);\
260                            }).then((res) => {\
261                                console.log(\"got %s\", res);\
262                                return res + \"abc\";\
263                            }).catch((ex) => {\
264                                throw Error(\"\" + ex);\
265                            });\
266            ",
267                ),
268            )
269            .expect("script failed");
270        assert!(esvf.is_js_promise());
271
272        let res = match esvf {
273            JsValueFacade::JsPromise { cached_promise } => {
274                cached_promise.get_promise_result_sync().expect("timed out")
275            }
276            _ => Err(JsValueFacade::new_str("poof")),
277        };
278
279        assert!(res.is_ok());
280        let res_str_esvf = res.ok().unwrap();
281        let res_str = res_str_esvf.get_str();
282        assert_eq!(res_str, "123abc");
283        log::info!("done");
284    }
285
286    #[test]
287    fn test_set_timeout_prom_res_nested() {
288        let rt = init_test_rt();
289        let esvf = rt
290            .eval_sync(
291                None,
292                Script::new(
293                    "test_set_timeout_prom_res_nested.es",
294                    "new Promise((resolve, reject) => {\
295                                setTimeout(() => {setTimeout(() => {resolve(123);}, 1000);}, 1000);\
296                            }).then((res) => {\
297                                console.log(\"got %s\", res);\
298                                return res + \"abc\";\
299                            }).catch((ex) => {\
300                                throw Error(\"\" + ex);\
301                            });\
302            ",
303                ),
304            )
305            .expect("script failed");
306        assert!(esvf.is_js_promise());
307
308        match esvf {
309            JsValueFacade::JsPromise { cached_promise } => {
310                let res = cached_promise.get_promise_result_sync().expect("timed out");
311                assert!(res.is_ok());
312                let res_str_esvf = res.ok().unwrap();
313                let res_str = res_str_esvf.get_str();
314                assert_eq!(res_str, "123abc");
315            }
316            _ => {}
317        }
318
319        log::info!("done");
320    }
321
322    #[test]
323    fn test_set_timeout() {
324        let rt = init_test_rt();
325
326        rt.eval_sync(None, Script::new("test_set_interval.es", "let t_id1 = setInterval((a, b) => {console.log('setInterval invoked with %s and %s', a, b);}, 500, 123, 456);")).expect("fail a");
327        rt.eval_sync(None, Script::new("test_set_timeout.es", "let t_id2 = setTimeout((a, b) => {console.log('setTimeout1 invoked with %s and %s', a, b);}, 500, 123, 456);")).expect("fail b");
328        rt.eval_sync(None, Script::new("test_set_timeout.es", "let t_id3 = setTimeout((a, b) => {console.log('setTimeout2 invoked with %s and %s', a, b);}, 600, 123, 456);")).expect("fail b");
329        rt.eval_sync(None, Script::new("test_set_timeout.es", "let t_id4 = setTimeout((a, b) => {console.log('setTimeout3 invoked with %s and %s', a, b);}, 900, 123, 456);")).expect("fail b");
330        std::thread::sleep(Duration::from_secs(3));
331        rt.eval_sync(
332            None,
333            Script::new("test_clearInterval.es", "clearInterval(t_id1);"),
334        )
335        .expect("fail c");
336        rt.eval_sync(
337            None,
338            Script::new("test_clearTimeout2.es", "clearTimeout(t_id2);"),
339        )
340        .expect("fail d");
341
342        rt.eval_sync(
343            None,
344            Script::new("test_set_timeout2.es", "this.__ti_num__ = 0;"),
345        )
346        .expect("fail qewr");
347
348        rt.eval_sync(
349            None,
350            Script::new("test_set_timeout2.es", "this.__it_num__ = 0;"),
351        )
352        .expect("fail qewr");
353
354        rt.eval_sync(
355            None,
356            Script::new(
357                "test_set_timeout3.es",
358                "setTimeout(() => {console.log('seto1');this.__ti_num__++;}, 455);",
359            ),
360        )
361        .expect("fail a1");
362        rt.eval_sync(
363            None,
364            Script::new(
365                "test_set_timeout3.es",
366                "setTimeout(() => {console.log('seto2');this.__ti_num__++;}, 366);",
367            ),
368        )
369        .expect("fail a2");
370        rt.eval_sync(
371            None,
372            Script::new(
373                "test_set_timeout3.es",
374                "setTimeout(() => {console.log('seto3');this.__ti_num__++;}, 1001);",
375            ),
376        )
377        .expect("fail a3");
378        rt.eval_sync(
379            None,
380            Script::new(
381                "test_set_timeout3.es",
382                "setTimeout(() => {console.log('seto4');this.__ti_num__++;}, 2002);",
383            ),
384        )
385        .expect("fail a4");
386
387        rt.eval_sync(
388            None,
389            Script::new(
390                "test_set_interval.es",
391                "setInterval(() => {this.__it_num__++;}, 1600);",
392            ),
393        )
394        .expect("fail a");
395        rt.eval_sync(
396            None,
397            Script::new(
398                "test_set_interval.es",
399                "setInterval(() => {this.__it_num__++;}, 2500);",
400            ),
401        )
402        .expect("fail a");
403
404        std::thread::sleep(Duration::from_secs(6));
405
406        let i = rt.exe_rt_task_in_event_loop(|q_js_rt| {
407            let q_ctx = q_js_rt.get_main_realm();
408            let global = get_global_q(q_ctx);
409            let ti_num = get_property_q(q_ctx, &global, "__ti_num__")
410                .ok()
411                .expect("could not get ti num prop from global");
412            let it_num = get_property_q(q_ctx, &global, "__it_num__")
413                .ok()
414                .expect("could not get it num prop from global");
415
416            (
417                to_i32(&ti_num)
418                    .ok()
419                    .expect("could not convert ti num to num"),
420                to_i32(&it_num)
421                    .ok()
422                    .expect("could not convert it num to num"),
423            )
424        });
425        assert_eq!(i.1, 5);
426        assert_eq!(i.0, 4);
427
428        rt.gc_sync();
429    }
430}