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            #[cfg(feature = "typescript")]
278            let stack = crate::typescript::unmap_stack_trace(stack.as_str());
279
280            match reason_str_res {
281                Ok(reason_str) => {
282                    log::error!(
283                        "[{}] unhandled promise rejection, reason: {}\nRejection stack:\n{}",
284                        realm_id,
285                        reason_str,
286                        stack
287                    );
288                }
289                Err(e) => {
290                    log::error!(
291                        "[{}] unhandled promise rejection, could not get reason: {}\nRejection stack:\n{}",
292                        realm_id,
293                        e,
294                        stack
295                    );
296                }
297            }
298        });
299    }
300}
301
302#[cfg(test)]
303pub mod tests {
304    use crate::builder::QuickJsRuntimeBuilder;
305    use crate::facades::tests::init_test_rt;
306    use crate::jsutils::Script;
307    use crate::quickjs_utils::promises::{add_promise_reactions_q, is_promise_q, new_promise_q};
308    use crate::quickjs_utils::{functions, new_null_ref, primitives};
309    use crate::quickjsruntimeadapter::QuickJsRuntimeAdapter;
310    use crate::values::JsValueFacade;
311    use futures::executor::block_on;
312    use std::time::Duration;
313
314    #[test]
315    fn test_instance_of_prom() {
316        log::info!("> test_instance_of_prom");
317
318        let rt = init_test_rt();
319        let io = rt.exe_rt_task_in_event_loop(|q_js_rt| {
320            let q_ctx = q_js_rt.get_main_realm();
321            let res = q_ctx.eval(Script::new(
322                "test_instance_of_prom.es",
323                "(new Promise((res, rej) => {}));",
324            ));
325            match res {
326                Ok(v) => {
327                    log::info!("checking if instance_of prom");
328
329                    is_promise_q(q_ctx, &v)
330                        && is_promise_q(q_ctx, &v)
331                        && is_promise_q(q_ctx, &v)
332                        && is_promise_q(q_ctx, &v)
333                        && is_promise_q(q_ctx, &v)
334                }
335                Err(e) => {
336                    log::error!("err testing instance_of prom: {}", e);
337                    false
338                }
339            }
340        });
341        assert!(io);
342
343        log::info!("< test_instance_of_prom");
344        std::thread::sleep(Duration::from_secs(1));
345    }
346
347    #[test]
348    fn new_prom() {
349        log::info!("> new_prom");
350
351        let rt = init_test_rt();
352        rt.exe_rt_task_in_event_loop(|q_js_rt| {
353            let q_ctx = q_js_rt.get_main_realm();
354            let func_ref = q_ctx
355                .eval(Script::new(
356                    "new_prom.es",
357                    "(function(p){p.then((res) => {console.log('prom resolved to ' + res);});});",
358                ))
359                .ok()
360                .unwrap();
361
362            let prom = new_promise_q(q_ctx).ok().unwrap();
363
364            let res =
365                functions::call_function_q(q_ctx, &func_ref, &[prom.get_promise_obj_ref()], None);
366            if res.is_err() {
367                panic!("func call failed: {}", res.err().unwrap());
368            }
369
370            unsafe {
371                prom.resolve(q_ctx.context, primitives::from_i32(743))
372                    .expect("resolve failed");
373            }
374        });
375        std::thread::sleep(Duration::from_secs(1));
376
377        log::info!("< new_prom");
378    }
379
380    #[test]
381    fn new_prom2() {
382        log::info!("> new_prom2");
383
384        let rt = init_test_rt();
385        rt.exe_rt_task_in_event_loop(|q_js_rt| {
386            let q_ctx = q_js_rt.get_main_realm();
387            let func_ref = q_ctx
388                .eval(Script::new(
389                    "new_prom.es",
390                    "(function(p){p.catch((res) => {console.log('prom rejected to ' + res);});});",
391                ))
392                .ok()
393                .unwrap();
394
395            let prom = new_promise_q(q_ctx).ok().unwrap();
396
397            let res =
398                functions::call_function_q(q_ctx, &func_ref, &[prom.get_promise_obj_ref()], None);
399            if res.is_err() {
400                panic!("func call failed: {}", res.err().unwrap());
401            }
402
403            unsafe {
404                prom.reject(q_ctx.context, primitives::from_i32(130))
405                    .expect("reject failed");
406            }
407        });
408        std::thread::sleep(Duration::from_secs(1));
409
410        log::info!("< new_prom2");
411    }
412
413    #[test]
414    fn test_promise_reactions() {
415        log::info!("> test_promise_reactions");
416
417        let rt = init_test_rt();
418        rt.exe_rt_task_in_event_loop(|q_js_rt| {
419            let q_ctx = q_js_rt.get_main_realm();
420            let prom_ref = q_ctx
421                .eval(Script::new(
422                    "test_promise_reactions.es",
423                    "(new Promise(function(resolve, reject) {resolve(364);}));",
424                ))
425                .expect("script failed");
426
427            let then_cb = functions::new_function_q(
428                q_ctx,
429                "testThen",
430                |_q_ctx, _this, args| {
431                    let res = primitives::to_i32(args.first().unwrap()).ok().unwrap();
432                    log::trace!("prom resolved with: {}", res);
433                    Ok(new_null_ref())
434                },
435                1,
436            )
437            .expect("could not create cb");
438            let finally_cb = functions::new_function_q(
439                q_ctx,
440                "testThen",
441                |_q_ctx, _this, _args| {
442                    log::trace!("prom finalized");
443
444                    Ok(new_null_ref())
445                },
446                1,
447            )
448            .expect("could not create cb");
449
450            add_promise_reactions_q(q_ctx, &prom_ref, Some(then_cb), None, Some(finally_cb))
451                .expect("could not add promise reactions");
452        });
453        std::thread::sleep(Duration::from_secs(1));
454
455        log::info!("< test_promise_reactions");
456    }
457
458    #[tokio::test]
459    async fn test_promise_async() {
460        let rt = init_test_rt();
461        let jsvf = rt
462            .eval(
463                None,
464                Script::new("test_prom_async.js", "Promise.resolve(123)"),
465            )
466            .await
467            .expect("script failed");
468        if let JsValueFacade::JsPromise { cached_promise } = jsvf {
469            let res = cached_promise
470                .get_promise_result()
471                .await
472                .expect("promise resolve send code stuf exploded");
473            match res {
474                Ok(prom_res) => {
475                    if prom_res.is_i32() {
476                        assert_eq!(prom_res.get_i32(), 123);
477                    } else {
478                        panic!("promise did not resolve to an i32.. well that was unexpected!");
479                    }
480                }
481                Err(e) => {
482                    panic!("prom was rejected: {}", e.stringify())
483                }
484            }
485        }
486    }
487    #[test]
488    fn test_promise_nested() {
489        log::info!("> test_promise_nested");
490
491        let rt = init_test_rt();
492
493        let mut jsvf_res = rt.exe_task_in_event_loop(|| {
494            QuickJsRuntimeAdapter::create_context("test").expect("create ctx failed");
495            QuickJsRuntimeAdapter::do_with(|q_js_rt| {
496                let q_ctx = q_js_rt.get_context("test");
497
498                let script = "(new Promise((resolve, reject) => {resolve({a: 7});}).then((obj) => {return {b: obj.a * 5}}));";
499                let esvf_res = q_ctx
500                    .eval(Script::new("test_promise_nested.es", script))
501                    .expect("script failed");
502
503                q_ctx.to_js_value_facade(&esvf_res).expect("poof")
504
505            })
506        });
507
508        while jsvf_res.is_js_promise() {
509            match jsvf_res {
510                JsValueFacade::JsPromise { cached_promise } => {
511                    jsvf_res = cached_promise
512                        .get_promise_result_sync()
513                        .expect("prom timed out")
514                        .expect("prom was rejected");
515                }
516                _ => {}
517            }
518        }
519
520        assert!(jsvf_res.is_js_object());
521
522        match jsvf_res {
523            JsValueFacade::JsObject { cached_object } => {
524                let obj = cached_object.get_object_sync().expect("esvf to map failed");
525                let b = obj.get("b").expect("got no b");
526                assert!(b.is_i32());
527                let i = b.get_i32();
528                assert_eq!(i, 5 * 7);
529            }
530            _ => {}
531        }
532
533        rt.exe_task_in_event_loop(|| {
534            let _ = QuickJsRuntimeAdapter::remove_context("test");
535        })
536    }
537
538    #[test]
539    fn test_to_string_err() {
540        let rt = QuickJsRuntimeBuilder::new().build();
541
542        let res = block_on(rt.eval(
543            None,
544            Script::new(
545                "test_test_to_string_err.js",
546                r#"
547            (async () => {
548                throw Error("poof");
549            })();
550        "#,
551            ),
552        ));
553        match res {
554            Ok(val) => {
555                if let JsValueFacade::JsPromise { cached_promise } = val {
556                    let prom_res =
557                        block_on(cached_promise.get_promise_result()).expect("promise timed out");
558                    match prom_res {
559                        Ok(v) => {
560                            panic!("promise unexpectedly resolved to val: {:?}", v);
561                        }
562                        Err(ev) => {
563                            println!("prom resolved to error: {ev:?}");
564                        }
565                    }
566                } else {
567                    panic!("func did not return a promise");
568                }
569            }
570            Err(e) => {
571                panic!("scrtip failed {}", e)
572            }
573        }
574    }
575}