quickjs_runtime/quickjs_utils/
promises.rs

1use crate::jsutils::JsError;
2use crate::quickjs_utils;
3#[cfg(feature = "bellard")]
4use crate::quickjs_utils::class_ids::JS_CLASS_PROMISE;
5use crate::quickjs_utils::errors::get_stack;
6use crate::quickjs_utils::functions;
7use crate::quickjsrealmadapter::QuickJsRealmAdapter;
8use crate::quickjsruntimeadapter::QuickJsRuntimeAdapter;
9use crate::quickjsvalueadapter::QuickJsValueAdapter;
10use libquickjs_sys as q;
11#[cfg(feature = "bellard")]
12use libquickjs_sys::JS_GetClassID;
13#[cfg(feature = "quickjs-ng")]
14use libquickjs_sys::JS_IsPromise;
15
16pub fn is_promise_q(context: &QuickJsRealmAdapter, obj_ref: &QuickJsValueAdapter) -> bool {
17    unsafe { is_promise(context.context, obj_ref) }
18}
19
20#[allow(dead_code)]
21/// # Safety
22/// When passing a context pointer please make sure the corresponding QuickJsContext is still valid
23#[allow(unused_variables)]
24pub unsafe fn is_promise(ctx: *mut q::JSContext, obj: &QuickJsValueAdapter) -> bool {
25    #[cfg(feature = "bellard")]
26    {
27        JS_GetClassID(*obj.borrow_value()) == JS_CLASS_PROMISE
28    }
29    #[cfg(feature = "quickjs-ng")]
30    {
31        JS_IsPromise(*obj.borrow_value())
32    }
33}
34
35pub struct QuickJsPromiseAdapter {
36    promise_obj_ref: QuickJsValueAdapter,
37    reject_function_obj_ref: QuickJsValueAdapter,
38    resolve_function_obj_ref: QuickJsValueAdapter,
39}
40#[allow(dead_code)]
41impl QuickJsPromiseAdapter {
42    pub fn get_promise_obj_ref(&self) -> QuickJsValueAdapter {
43        self.promise_obj_ref.clone()
44    }
45
46    pub fn resolve_q(
47        &self,
48        q_ctx: &QuickJsRealmAdapter,
49        value: QuickJsValueAdapter,
50    ) -> Result<(), JsError> {
51        unsafe { self.resolve(q_ctx.context, value) }
52    }
53    /// # Safety
54    /// When passing a context pointer please make sure the corresponding QuickJsContext is still valid
55    pub unsafe fn resolve(
56        &self,
57        context: *mut q::JSContext,
58        value: QuickJsValueAdapter,
59    ) -> Result<(), JsError> {
60        log::trace!("PromiseRef.resolve()");
61        crate::quickjs_utils::functions::call_function(
62            context,
63            &self.resolve_function_obj_ref,
64            &[value],
65            None,
66        )?;
67        Ok(())
68    }
69    pub fn reject_q(
70        &self,
71        q_ctx: &QuickJsRealmAdapter,
72        value: QuickJsValueAdapter,
73    ) -> Result<(), JsError> {
74        unsafe { self.reject(q_ctx.context, value) }
75    }
76    /// # Safety
77    /// When passing a context pointer please make sure the corresponding QuickJsContext is still valid
78    pub unsafe fn reject(
79        &self,
80        context: *mut q::JSContext,
81        value: QuickJsValueAdapter,
82    ) -> Result<(), JsError> {
83        log::trace!("PromiseRef.reject()");
84        crate::quickjs_utils::functions::call_function(
85            context,
86            &self.reject_function_obj_ref,
87            &[value],
88            None,
89        )?;
90        Ok(())
91    }
92}
93
94impl Clone for QuickJsPromiseAdapter {
95    fn clone(&self) -> Self {
96        Self {
97            promise_obj_ref: self.promise_obj_ref.clone(),
98            reject_function_obj_ref: self.reject_function_obj_ref.clone(),
99            resolve_function_obj_ref: self.resolve_function_obj_ref.clone(),
100        }
101    }
102}
103
104impl QuickJsPromiseAdapter {
105    pub fn js_promise_resolve(
106        &self,
107        context: &QuickJsRealmAdapter,
108        resolution: &QuickJsValueAdapter,
109    ) -> Result<(), JsError> {
110        self.resolve_q(context, resolution.clone())
111    }
112
113    pub fn js_promise_reject(
114        &self,
115        context: &QuickJsRealmAdapter,
116        rejection: &QuickJsValueAdapter,
117    ) -> Result<(), JsError> {
118        self.reject_q(context, rejection.clone())
119    }
120
121    pub fn js_promise_get_value(&self, _realm: &QuickJsRealmAdapter) -> QuickJsValueAdapter {
122        self.promise_obj_ref.clone()
123    }
124}
125
126pub fn new_promise_q(q_ctx: &QuickJsRealmAdapter) -> Result<QuickJsPromiseAdapter, JsError> {
127    unsafe { new_promise(q_ctx.context) }
128}
129
130/// create a new Promise
131/// you can use this to respond asynchronously to method calls from JavaScript by returning a Promise
132/// # Safety
133/// When passing a context pointer please make sure the corresponding QuickJsContext is still valid
134pub unsafe fn new_promise(context: *mut q::JSContext) -> Result<QuickJsPromiseAdapter, JsError> {
135    log::trace!("promises::new_promise()");
136
137    let mut promise_resolution_functions = [quickjs_utils::new_null(), quickjs_utils::new_null()];
138
139    let prom_val = q::JS_NewPromiseCapability(context, promise_resolution_functions.as_mut_ptr());
140
141    let resolve_func_val = *promise_resolution_functions.first().unwrap();
142    let reject_func_val = *promise_resolution_functions.get(1).unwrap();
143
144    let resolve_function_obj_ref = QuickJsValueAdapter::new(
145        context,
146        resolve_func_val,
147        false,
148        true,
149        "promises::new_promise resolve_func_val",
150    );
151    let reject_function_obj_ref = QuickJsValueAdapter::new(
152        context,
153        reject_func_val,
154        false,
155        true,
156        "promises::new_promise reject_func_val",
157    );
158    debug_assert!(functions::is_function(context, &resolve_function_obj_ref));
159    debug_assert!(functions::is_function(context, &reject_function_obj_ref));
160
161    let promise_obj_ref = QuickJsValueAdapter::new(
162        context,
163        prom_val,
164        false,
165        true,
166        "promises::new_promise prom_val",
167    );
168
169    #[cfg(feature = "bellard")]
170    debug_assert_eq!(resolve_function_obj_ref.get_ref_count(), 1);
171    #[cfg(feature = "bellard")]
172    debug_assert_eq!(reject_function_obj_ref.get_ref_count(), 1);
173    #[cfg(feature = "bellard")]
174    debug_assert_eq!(promise_obj_ref.get_ref_count(), 3);
175
176    Ok(QuickJsPromiseAdapter {
177        promise_obj_ref,
178        reject_function_obj_ref,
179        resolve_function_obj_ref,
180    })
181}
182
183pub(crate) fn init_promise_rejection_tracker(q_js_rt: &QuickJsRuntimeAdapter) {
184    let tracker: q::JSHostPromiseRejectionTracker = Some(promise_rejection_tracker);
185
186    unsafe {
187        q::JS_SetHostPromiseRejectionTracker(q_js_rt.runtime, tracker, std::ptr::null_mut());
188    }
189}
190
191pub fn add_promise_reactions_q(
192    context: &QuickJsRealmAdapter,
193    promise_obj_ref: &QuickJsValueAdapter,
194    then_func_obj_ref_opt: Option<QuickJsValueAdapter>,
195    catch_func_obj_ref_opt: Option<QuickJsValueAdapter>,
196    finally_func_obj_ref_opt: Option<QuickJsValueAdapter>,
197) -> Result<(), JsError> {
198    unsafe {
199        add_promise_reactions(
200            context.context,
201            promise_obj_ref,
202            then_func_obj_ref_opt,
203            catch_func_obj_ref_opt,
204            finally_func_obj_ref_opt,
205        )
206    }
207}
208
209#[allow(dead_code)]
210/// # Safety
211/// When passing a context pointer please make sure the corresponding QuickJsContext is still valid
212pub unsafe fn add_promise_reactions(
213    context: *mut q::JSContext,
214    promise_obj_ref: &QuickJsValueAdapter,
215    then_func_obj_ref_opt: Option<QuickJsValueAdapter>,
216    catch_func_obj_ref_opt: Option<QuickJsValueAdapter>,
217    finally_func_obj_ref_opt: Option<QuickJsValueAdapter>,
218) -> Result<(), JsError> {
219    debug_assert!(is_promise(context, promise_obj_ref));
220
221    if let Some(then_func_obj_ref) = then_func_obj_ref_opt {
222        functions::invoke_member_function(context, promise_obj_ref, "then", &[then_func_obj_ref])?;
223    }
224    if let Some(catch_func_obj_ref) = catch_func_obj_ref_opt {
225        functions::invoke_member_function(
226            context,
227            promise_obj_ref,
228            "catch",
229            &[catch_func_obj_ref],
230        )?;
231    }
232    if let Some(finally_func_obj_ref) = finally_func_obj_ref_opt {
233        functions::invoke_member_function(
234            context,
235            promise_obj_ref,
236            "finally",
237            &[finally_func_obj_ref],
238        )?;
239    }
240
241    Ok(())
242}
243
244unsafe extern "C" fn promise_rejection_tracker(
245    ctx: *mut q::JSContext,
246    _promise: q::JSValue,
247    reason: q::JSValue,
248    #[cfg(feature = "bellard")] is_handled: ::std::os::raw::c_int,
249    #[cfg(feature = "quickjs-ng")] is_handled: bool,
250
251    _opaque: *mut ::std::os::raw::c_void,
252) {
253    #[cfg(feature = "bellard")]
254    let handled = is_handled != 0;
255    #[cfg(feature = "quickjs-ng")]
256    let handled = is_handled;
257
258    if !handled {
259        let reason_ref = QuickJsValueAdapter::new(
260            ctx,
261            reason,
262            false,
263            false,
264            "promises::promise_rejection_tracker reason",
265        );
266        let reason_str_res = functions::call_to_string(ctx, &reason_ref);
267        QuickJsRuntimeAdapter::do_with(|rt| {
268            let realm = rt.get_quickjs_context(ctx);
269            let realm_id = realm.get_realm_id();
270            let stack = match get_stack(realm) {
271                Ok(s) => match s.to_string() {
272                    Ok(s) => s,
273                    Err(_) => "".to_string(),
274                },
275                Err(_) => "".to_string(),
276            };
277            match reason_str_res {
278                Ok(reason_str) => {
279                    log::error!(
280                        "[{}] unhandled promise rejection, reason: {}{}",
281                        realm_id,
282                        reason_str,
283                        stack
284                    );
285                }
286                Err(e) => {
287                    log::error!(
288                        "[{}] unhandled promise rejection, could not get reason: {}{}",
289                        realm_id,
290                        e,
291                        stack
292                    );
293                }
294            }
295        });
296    }
297}
298
299#[cfg(test)]
300pub mod tests {
301    use crate::builder::QuickJsRuntimeBuilder;
302    use crate::facades::tests::init_test_rt;
303    use crate::jsutils::Script;
304    use crate::quickjs_utils::promises::{add_promise_reactions_q, is_promise_q, new_promise_q};
305    use crate::quickjs_utils::{functions, new_null_ref, primitives};
306    use crate::quickjsruntimeadapter::QuickJsRuntimeAdapter;
307    use crate::values::JsValueFacade;
308    use futures::executor::block_on;
309    use std::time::Duration;
310
311    #[test]
312    fn test_instance_of_prom() {
313        log::info!("> test_instance_of_prom");
314
315        let rt = init_test_rt();
316        let io = rt.exe_rt_task_in_event_loop(|q_js_rt| {
317            let q_ctx = q_js_rt.get_main_realm();
318            let res = q_ctx.eval(Script::new(
319                "test_instance_of_prom.es",
320                "(new Promise((res, rej) => {}));",
321            ));
322            match res {
323                Ok(v) => {
324                    log::info!("checking if instance_of prom");
325
326                    is_promise_q(q_ctx, &v)
327                        && is_promise_q(q_ctx, &v)
328                        && is_promise_q(q_ctx, &v)
329                        && is_promise_q(q_ctx, &v)
330                        && is_promise_q(q_ctx, &v)
331                }
332                Err(e) => {
333                    log::error!("err testing instance_of prom: {}", e);
334                    false
335                }
336            }
337        });
338        assert!(io);
339
340        log::info!("< test_instance_of_prom");
341        std::thread::sleep(Duration::from_secs(1));
342    }
343
344    #[test]
345    fn new_prom() {
346        log::info!("> new_prom");
347
348        let rt = init_test_rt();
349        rt.exe_rt_task_in_event_loop(|q_js_rt| {
350            let q_ctx = q_js_rt.get_main_realm();
351            let func_ref = q_ctx
352                .eval(Script::new(
353                    "new_prom.es",
354                    "(function(p){p.then((res) => {console.log('prom resolved to ' + res);});});",
355                ))
356                .ok()
357                .unwrap();
358
359            let prom = new_promise_q(q_ctx).ok().unwrap();
360
361            let res =
362                functions::call_function_q(q_ctx, &func_ref, &[prom.get_promise_obj_ref()], None);
363            if res.is_err() {
364                panic!("func call failed: {}", res.err().unwrap());
365            }
366
367            unsafe {
368                prom.resolve(q_ctx.context, primitives::from_i32(743))
369                    .expect("resolve failed");
370            }
371        });
372        std::thread::sleep(Duration::from_secs(1));
373
374        log::info!("< new_prom");
375    }
376
377    #[test]
378    fn new_prom2() {
379        log::info!("> new_prom2");
380
381        let rt = init_test_rt();
382        rt.exe_rt_task_in_event_loop(|q_js_rt| {
383            let q_ctx = q_js_rt.get_main_realm();
384            let func_ref = q_ctx
385                .eval(Script::new(
386                    "new_prom.es",
387                    "(function(p){p.catch((res) => {console.log('prom rejected to ' + res);});});",
388                ))
389                .ok()
390                .unwrap();
391
392            let prom = new_promise_q(q_ctx).ok().unwrap();
393
394            let res =
395                functions::call_function_q(q_ctx, &func_ref, &[prom.get_promise_obj_ref()], None);
396            if res.is_err() {
397                panic!("func call failed: {}", res.err().unwrap());
398            }
399
400            unsafe {
401                prom.reject(q_ctx.context, primitives::from_i32(130))
402                    .expect("reject failed");
403            }
404        });
405        std::thread::sleep(Duration::from_secs(1));
406
407        log::info!("< new_prom2");
408    }
409
410    #[test]
411    fn test_promise_reactions() {
412        log::info!("> test_promise_reactions");
413
414        let rt = init_test_rt();
415        rt.exe_rt_task_in_event_loop(|q_js_rt| {
416            let q_ctx = q_js_rt.get_main_realm();
417            let prom_ref = q_ctx
418                .eval(Script::new(
419                    "test_promise_reactions.es",
420                    "(new Promise(function(resolve, reject) {resolve(364);}));",
421                ))
422                .expect("script failed");
423
424            let then_cb = functions::new_function_q(
425                q_ctx,
426                "testThen",
427                |_q_ctx, _this, args| {
428                    let res = primitives::to_i32(args.first().unwrap()).ok().unwrap();
429                    log::trace!("prom resolved with: {}", res);
430                    Ok(new_null_ref())
431                },
432                1,
433            )
434            .expect("could not create cb");
435            let finally_cb = functions::new_function_q(
436                q_ctx,
437                "testThen",
438                |_q_ctx, _this, _args| {
439                    log::trace!("prom finalized");
440
441                    Ok(new_null_ref())
442                },
443                1,
444            )
445            .expect("could not create cb");
446
447            add_promise_reactions_q(q_ctx, &prom_ref, Some(then_cb), None, Some(finally_cb))
448                .expect("could not add promise reactions");
449        });
450        std::thread::sleep(Duration::from_secs(1));
451
452        log::info!("< test_promise_reactions");
453    }
454
455    #[tokio::test]
456    async fn test_promise_async() {
457        let rt = init_test_rt();
458        let jsvf = rt
459            .eval(
460                None,
461                Script::new("test_prom_async.js", "Promise.resolve(123)"),
462            )
463            .await
464            .expect("script failed");
465        if let JsValueFacade::JsPromise { cached_promise } = jsvf {
466            let res = cached_promise
467                .get_promise_result()
468                .await
469                .expect("promise resolve send code stuf exploded");
470            match res {
471                Ok(prom_res) => {
472                    if prom_res.is_i32() {
473                        assert_eq!(prom_res.get_i32(), 123);
474                    } else {
475                        panic!("promise did not resolve to an i32.. well that was unexpected!");
476                    }
477                }
478                Err(e) => {
479                    panic!("prom was rejected: {}", e.stringify())
480                }
481            }
482        }
483    }
484    #[test]
485    fn test_promise_nested() {
486        log::info!("> test_promise_nested");
487
488        let rt = init_test_rt();
489
490        let mut jsvf_res = rt.exe_task_in_event_loop(|| {
491            QuickJsRuntimeAdapter::create_context("test").expect("create ctx failed");
492            QuickJsRuntimeAdapter::do_with(|q_js_rt| {
493                let q_ctx = q_js_rt.get_context("test");
494
495                let script = "(new Promise((resolve, reject) => {resolve({a: 7});}).then((obj) => {return {b: obj.a * 5}}));";
496                let esvf_res = q_ctx
497                    .eval(Script::new("test_promise_nested.es", script))
498                    .expect("script failed");
499
500                q_ctx.to_js_value_facade(&esvf_res).expect("poof")
501
502            })
503        });
504
505        while jsvf_res.is_js_promise() {
506            match jsvf_res {
507                JsValueFacade::JsPromise { cached_promise } => {
508                    jsvf_res = cached_promise
509                        .get_promise_result_sync()
510                        .expect("prom timed out")
511                        .expect("prom was rejected");
512                }
513                _ => {}
514            }
515        }
516
517        assert!(jsvf_res.is_js_object());
518
519        match jsvf_res {
520            JsValueFacade::JsObject { cached_object } => {
521                let obj = cached_object.get_object_sync().expect("esvf to map failed");
522                let b = obj.get("b").expect("got no b");
523                assert!(b.is_i32());
524                let i = b.get_i32();
525                assert_eq!(i, 5 * 7);
526            }
527            _ => {}
528        }
529
530        rt.exe_task_in_event_loop(|| {
531            let _ = QuickJsRuntimeAdapter::remove_context("test");
532        })
533    }
534
535    #[test]
536    fn test_to_string_err() {
537        let rt = QuickJsRuntimeBuilder::new().build();
538
539        let res = block_on(rt.eval(
540            None,
541            Script::new(
542                "test_test_to_string_err.js",
543                r#"
544            (async () => {
545                throw Error("poof");
546            })();
547        "#,
548            ),
549        ));
550        match res {
551            Ok(val) => {
552                if let JsValueFacade::JsPromise { cached_promise } = val {
553                    let prom_res =
554                        block_on(cached_promise.get_promise_result()).expect("promise timed out");
555                    match prom_res {
556                        Ok(v) => {
557                            panic!("promise unexpectedly resolved to val: {:?}", v);
558                        }
559                        Err(ev) => {
560                            println!("prom resolved to error: {ev:?}");
561                        }
562                    }
563                } else {
564                    panic!("func did not return a promise");
565                }
566            }
567            Err(e) => {
568                panic!("scrtip failed {}", e)
569            }
570        }
571    }
572}