Skip to main content

rquickjs_core/value/
promise.rs

1//! Javascript promises and future integration.
2use crate::{
3    atom::PredefinedAtom, qjs, Ctx, Error, FromJs, Function, IntoJs, Object, Result, Value,
4};
5#[cfg(feature = "futures")]
6use crate::{function::This, CatchResultExt, CaughtError};
7#[cfg(feature = "futures")]
8use alloc::rc::Rc;
9#[cfg(feature = "futures")]
10use core::{
11    cell::RefCell,
12    future::Future,
13    marker::PhantomData,
14    pin::Pin,
15    task::{Context as TaskContext, Poll, Waker},
16};
17#[cfg(all(feature = "std", feature = "futures"))]
18use std::println;
19
20/// The execution state of a promise.
21#[derive(Clone, Copy, Eq, PartialEq, Debug)]
22pub enum PromiseState {
23    /// The promise has not yet completed.
24    Pending,
25    /// The promise completed succefully.
26    Resolved,
27    /// The promise completed with an error.
28    Rejected,
29}
30
31/// The type of promise event.
32#[derive(Clone, Copy, Eq, PartialEq, Debug)]
33pub enum PromiseHookType {
34    Init,
35    Before,
36    After,
37    Resolve,
38}
39
40/// A JavaScript promise.
41#[derive(Debug, PartialEq, Clone, Hash, Eq)]
42#[repr(transparent)]
43pub struct Promise<'js>(pub(crate) Object<'js>);
44
45impl<'js> Promise<'js> {
46    #[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "futures")))]
47    #[cfg(feature = "futures")]
48    pub fn wrap_future<F, R>(ctx: &Ctx<'js>, future: F) -> Result<Self>
49    where
50        F: Future<Output = R> + 'js,
51        R: IntoJs<'js>,
52    {
53        let (promise, resolve, reject) = ctx.promise()?;
54        let ctx_clone = ctx.clone();
55        let future = async move {
56            let res = future.await.into_js(&ctx_clone).catch(&ctx_clone);
57
58            let err = match res {
59                Ok(x) => resolve.call::<_, ()>((x,)),
60                Err(e) => match e {
61                    CaughtError::Exception(e) => reject.call::<_, ()>((e,)),
62                    CaughtError::Value(e) => reject.call::<_, ()>((e,)),
63                    CaughtError::Error(e) => {
64                        let is_exception = unsafe { qjs::JS_IsException(e.throw(&ctx_clone)) };
65                        debug_assert!(is_exception);
66                        let e = ctx_clone.catch();
67                        reject.call::<_, ()>((e,))
68                    }
69                },
70            };
71            // TODO figure out something better to do here.
72            if let Err(_e) = err {
73                #[cfg(feature = "std")]
74                println!("promise handle function returned error:{}", _e);
75            }
76        };
77        ctx.spawn(future);
78        Ok(promise)
79    }
80
81    /// Create a new JavaScript promise along with its resolve and reject functions.
82    pub fn new(ctx: &Ctx<'js>) -> Result<(Self, Function<'js>, Function<'js>)> {
83        ctx.promise()
84    }
85
86    /// Returns the state of the promise, either pending,resolved or rejected.
87    pub fn state(&self) -> PromiseState {
88        let v = unsafe { qjs::JS_PromiseState(self.ctx().as_ptr(), self.as_js_value()) };
89        match v {
90            qjs::JSPromiseStateEnum_JS_PROMISE_PENDING => PromiseState::Pending,
91            qjs::JSPromiseStateEnum_JS_PROMISE_FULFILLED => PromiseState::Resolved,
92            qjs::JSPromiseStateEnum_JS_PROMISE_REJECTED => PromiseState::Rejected,
93            _ => unreachable!(),
94        }
95    }
96
97    /// Returns the `then` function, used for chaining promises.
98    pub fn then(&self) -> Result<Function<'js>> {
99        self.0.get(PredefinedAtom::Then)
100    }
101
102    /// Returns the `catch` function, used for retrieving the result of a rejected promise.
103    pub fn catch(&self) -> Result<Function<'js>> {
104        self.0.get(PredefinedAtom::Catch)
105    }
106
107    /// Returns the result of the future if there is one.
108    ///
109    /// Returns None if the promise has not yet been completed, Ok if the promise was resolved, and
110    /// [`Error::Exception`] if the promise rejected with the rejected value as the thrown
111    /// value retrievable via [`Ctx::catch`].
112    pub fn result<T: FromJs<'js>>(&self) -> Option<Result<T>> {
113        match self.state() {
114            PromiseState::Pending => None,
115            PromiseState::Resolved => {
116                let v = unsafe { qjs::JS_PromiseResult(self.ctx().as_ptr(), self.as_js_value()) };
117                let v = unsafe { Value::from_js_value(self.ctx().clone(), v) };
118                Some(FromJs::from_js(self.ctx(), v))
119            }
120            PromiseState::Rejected => {
121                unsafe {
122                    let v = qjs::JS_PromiseResult(self.ctx().as_ptr(), self.as_js_value());
123                    qjs::JS_Throw(self.ctx().as_ptr(), v);
124                };
125                Some(Err(Error::Exception))
126            }
127        }
128    }
129
130    /// Runs the quickjs job queue until the promise is either rejected or resolved.
131    ///
132    /// If blocking on the promise would result in blocking, i.e. when the job queue runs out of
133    /// jobs before the promise can be resolved, this function returns [`Error::WouldBlock`]
134    /// indicating that no more work can be done at the moment.
135    ///
136    /// This function only drives the quickjs job queue, futures are not polled.
137    pub fn finish<T: FromJs<'js>>(&self) -> Result<T> {
138        loop {
139            if let Some(x) = self.result() {
140                return x;
141            }
142
143            if !self.ctx.execute_pending_job() {
144                return Err(Error::WouldBlock);
145            }
146        }
147    }
148
149    /// Wrap the promise into a struct which can be polled as a rust future.
150    #[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "futures")))]
151    #[cfg(feature = "futures")]
152    pub fn into_future<T>(self) -> PromiseFuture<'js, T>
153    where
154        T: FromJs<'js>,
155    {
156        PromiseFuture {
157            state: None,
158            promise: self,
159            _marker: PhantomData,
160        }
161    }
162}
163
164/// Future-aware promise
165#[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "futures")))]
166#[cfg(feature = "futures")]
167#[must_use = "futures do nothing unless you `.await` or poll them"]
168#[derive(Debug)]
169pub struct PromiseFuture<'js, T> {
170    state: Option<Rc<RefCell<Waker>>>,
171    promise: Promise<'js>,
172    _marker: PhantomData<T>,
173}
174
175// Nothing is actually pinned so promise future is unpin.
176#[cfg(feature = "futures")]
177impl<'js, T> Unpin for PromiseFuture<'js, T> {}
178
179#[cfg(feature = "futures")]
180impl<'js, T> Future for PromiseFuture<'js, T>
181where
182    T: FromJs<'js>,
183{
184    type Output = Result<T>;
185
186    fn poll(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Self::Output> {
187        let this = self.get_mut();
188
189        if let Some(x) = this.promise.result() {
190            return Poll::Ready(x);
191        }
192
193        // Only bail on uncatchable exceptions (e.g. interrupt handler).
194        // Regular pending exceptions are transient and don't prevent settlement.
195        if this.promise.ctx.has_exception() {
196            let exc = this.promise.ctx.catch();
197            if exc.is_uncatchable_error() {
198                this.promise.ctx.throw(exc);
199                return Poll::Ready(Err(Error::Exception));
200            }
201        }
202
203        if this.state.is_none() {
204            let inner = Rc::new(RefCell::new(cx.waker().clone()));
205            this.state = Some(inner.clone());
206
207            let resolve = Function::new(this.promise.ctx.clone(), move || {
208                inner.borrow().wake_by_ref();
209            })?;
210
211            this.promise.then()?.call::<_, ()>((
212                This(this.promise.clone()),
213                resolve.clone(),
214                resolve,
215            ))?;
216            return Poll::Pending;
217        }
218
219        this.state
220            .as_ref()
221            .unwrap()
222            .borrow_mut()
223            .clone_from(cx.waker());
224
225        Poll::Pending
226    }
227}
228
229/// Wrapper for futures to convert to JS promises
230#[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "futures")))]
231#[repr(transparent)]
232#[cfg(feature = "futures")]
233pub struct Promised<T>(pub T);
234
235#[cfg(feature = "futures")]
236impl<T> From<T> for Promised<T> {
237    fn from(future: T) -> Self {
238        Self(future)
239    }
240}
241
242#[cfg(feature = "futures")]
243impl<'js, T, R> IntoJs<'js> for Promised<T>
244where
245    T: Future<Output = R> + 'js,
246    R: IntoJs<'js> + 'js,
247{
248    fn into_js(self, ctx: &Ctx<'js>) -> Result<Value<'js>> {
249        Promise::wrap_future(ctx, self.0).map(|x| x.into_value())
250    }
251}
252
253/// A type which behaves like a promise but can wrap any javascript value.
254///
255/// This type is usefull when you are unsure if a function will return a promise.
256/// You can call finish and turn it into a future like a normal promise.
257/// When the value this type us converted isn't a promise it will behave like an promise which is
258/// already resolved, otherwise it will call the right functions on the promise.
259#[derive(Debug, PartialEq, Clone, Hash, Eq)]
260pub struct MaybePromise<'js>(Value<'js>);
261
262impl<'js> FromJs<'js> for MaybePromise<'js> {
263    fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> Result<Self> {
264        Ok(MaybePromise(value))
265    }
266}
267
268impl<'js> IntoJs<'js> for MaybePromise<'js> {
269    fn into_js(self, _ctx: &Ctx<'js>) -> Result<Value<'js>> {
270        Ok(self.0)
271    }
272}
273
274impl<'js> MaybePromise<'js> {
275    /// Reference to the inner value
276    pub fn as_value(&self) -> &Value<'js> {
277        &self.0
278    }
279
280    /// Convert into the inner value
281    pub fn into_value(self) -> Value<'js> {
282        self.0
283    }
284
285    /// Convert into the inner value
286    pub fn from_value(value: Value<'js>) -> Self {
287        MaybePromise(value)
288    }
289
290    /// Returns the [`Ctx`] object associated with this value
291    pub fn ctx(&self) -> &Ctx<'js> {
292        self.0.ctx()
293    }
294
295    /// Returns [`PromiseState::Resolved`] if the wrapped value isn't a promise, otherwise calls
296    /// [`Promise::state`] on the promise and returns it's value.
297    pub fn state(&self) -> PromiseState {
298        if let Some(x) = self.0.as_promise() {
299            x.state()
300        } else {
301            PromiseState::Resolved
302        }
303    }
304
305    /// Returns the value if self isn't a promise, otherwise calls [`Promise::result`] on the promise.
306    pub fn result<T: FromJs<'js>>(&self) -> Option<Result<T>> {
307        if let Some(x) = self.0.as_promise() {
308            x.result::<T>()
309        } else {
310            Some(T::from_js(self.0.ctx(), self.0.clone()))
311        }
312    }
313
314    /// Returns the value if self isn't a promise, otherwise calls [`Promise::finish`] on the promise.
315    pub fn finish<T: FromJs<'js>>(&self) -> Result<T> {
316        if let Some(x) = self.0.as_promise() {
317            x.finish::<T>()
318        } else {
319            T::from_js(self.0.ctx(), self.0.clone())
320        }
321    }
322
323    /// Convert self into a future which will return ready if the wrapped value isn't a promise,
324    /// otherwise it will handle the promise like the future returned from
325    /// [`Promise::into_future`].
326    #[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "futures")))]
327    #[cfg(feature = "futures")]
328    pub fn into_future<T: FromJs<'js>>(self) -> MaybePromiseFuture<'js, T> {
329        if self.0.is_promise() {
330            let fut = self.0.into_promise().unwrap().into_future();
331            MaybePromiseFuture(MaybePromiseFutureInner::Future(fut))
332        } else {
333            MaybePromiseFuture(MaybePromiseFutureInner::Ready(self.0))
334        }
335    }
336}
337
338/// Future-aware maybe promise
339#[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "futures")))]
340#[cfg(feature = "futures")]
341#[must_use = "futures do nothing unless you `.await` or poll them"]
342#[derive(Debug)]
343pub struct MaybePromiseFuture<'js, T>(MaybePromiseFutureInner<'js, T>);
344
345#[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "futures")))]
346#[cfg(feature = "futures")]
347#[derive(Debug)]
348enum MaybePromiseFutureInner<'js, T> {
349    Ready(Value<'js>),
350    Future(PromiseFuture<'js, T>),
351}
352
353#[cfg(feature = "futures")]
354impl<'js, T> Future for MaybePromiseFuture<'js, T>
355where
356    T: FromJs<'js>,
357{
358    type Output = Result<T>;
359
360    fn poll(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Self::Output> {
361        match self.get_mut().0 {
362            MaybePromiseFutureInner::Ready(ref x) => Poll::Ready(T::from_js(x.ctx(), x.clone())),
363            MaybePromiseFutureInner::Future(ref mut x) => Pin::new(x).poll(cx),
364        }
365    }
366}
367
368#[cfg(test)]
369mod test {
370    use std::sync::atomic::{AtomicBool, Ordering};
371    #[cfg(feature = "futures")]
372    use std::time::Duration;
373
374    use super::Promise;
375    #[cfg(feature = "futures")]
376    use crate::{
377        function::Async, promise::Promised, AsyncContext, AsyncRuntime, CaughtError, Result,
378    };
379    use crate::{
380        function::Func, prelude::This, promise::PromiseState, CatchResultExt, Context, Function,
381        Runtime,
382    };
383
384    #[cfg(feature = "futures")]
385    async fn set_timeout<'js>(cb: Function<'js>, number: f64) -> Result<()> {
386        tokio::time::sleep(Duration::from_secs_f64(number / 1000.0)).await;
387        cb.call::<_, ()>(())
388    }
389
390    #[cfg(feature = "futures")]
391    #[tokio::test]
392    async fn promise() {
393        let rt = AsyncRuntime::new().unwrap();
394        let ctx = AsyncContext::full(&rt).await.unwrap();
395
396        ctx.async_with(async |ctx| {
397            ctx.globals()
398                .set("setTimeout", Func::from(Async(set_timeout)))
399                .unwrap();
400
401            let func = ctx
402                .eval::<Function, _>(
403                    r"
404                    (function(){
405                        return new Promise((resolve) => {
406                            setTimeout(x => {
407                                resolve(42)
408                            },100)
409                        })
410                    })
411                    ",
412                )
413                .catch(&ctx)
414                .unwrap();
415            let promise: Promise = func.call(()).unwrap();
416            assert_eq!(promise.into_future::<i32>().await.catch(&ctx).unwrap(), 42);
417
418            let func = ctx
419                .eval::<Function, _>(
420                    r"
421                    (function(){
422                        return new Promise((_,reject) => {
423                            setTimeout(x => {
424                                reject(42)
425                            },100)
426                        })
427                    })
428                    ",
429                )
430                .catch(&ctx)
431                .unwrap();
432            let promise: Promise = func.call(()).unwrap();
433            let err = promise.into_future::<()>().await.catch(&ctx);
434            match err {
435                Err(CaughtError::Value(v)) => {
436                    assert_eq!(v.as_int().unwrap(), 42)
437                }
438                _ => panic!(),
439            }
440        })
441        .await
442    }
443
444    #[cfg(feature = "futures")]
445    #[tokio::test]
446    async fn promised() {
447        use crate::Exception;
448
449        let rt = AsyncRuntime::new().unwrap();
450        let ctx = AsyncContext::full(&rt).await.unwrap();
451
452        ctx.async_with(async |ctx| {
453            let promised = Promised::from(async {
454                tokio::time::sleep(Duration::from_millis(100)).await;
455                42
456            });
457
458            let function = ctx
459                .eval::<Function, _>(
460                    r"
461                (async function(v){
462                    let val = await v;
463                    if(val !== 42){
464                        throw new Error('not correct value')
465                    }
466                })
467            ",
468                )
469                .catch(&ctx)
470                .unwrap();
471
472            function
473                .call::<_, Promise>((promised,))
474                .unwrap()
475                .into_future::<()>()
476                .await
477                .unwrap();
478
479            let ctx_clone = ctx.clone();
480            let promised = Promised::from(async move {
481                tokio::time::sleep(Duration::from_millis(100)).await;
482                Result::<()>::Err(Exception::throw_message(&ctx_clone, "some_message"))
483            });
484
485            let function = ctx
486                .eval::<Function, _>(
487                    r"
488                (async function(v){
489                    try{
490                        await v;
491                    }catch(e) {
492                        if (e.message !== 'some_message'){
493                            throw new Error('wrong error')
494                        }
495                        return
496                    }
497                    throw new Error('no error thrown')
498                })
499            ",
500                )
501                .catch(&ctx)
502                .unwrap();
503
504            function
505                .call::<_, Promise>((promised,))
506                .unwrap()
507                .into_future::<()>()
508                .await
509                .unwrap()
510        })
511        .await
512    }
513
514    #[test]
515    fn promise_then() {
516        static DID_EXECUTE: AtomicBool = AtomicBool::new(false);
517
518        let rt = Runtime::new().unwrap();
519        let ctx = Context::full(&rt).unwrap();
520
521        ctx.with(|ctx| {
522            let (promise, resolve, _) = Promise::new(&ctx).unwrap();
523
524            let cb = Func::new(|s: String| {
525                assert_eq!(s, "FOO");
526                DID_EXECUTE.store(true, Ordering::SeqCst);
527            });
528
529            assert_eq!(promise.state(), PromiseState::Pending);
530
531            promise
532                .get::<_, Function>("then")
533                .catch(&ctx)
534                .unwrap()
535                .call::<_, ()>((This(promise.clone()), cb))
536                .catch(&ctx)
537                .unwrap();
538
539            resolve.call::<_, ()>(("FOO",)).unwrap();
540            assert_eq!(promise.state(), PromiseState::Resolved);
541
542            while ctx.execute_pending_job() {}
543
544            assert!(DID_EXECUTE.load(Ordering::SeqCst));
545        })
546    }
547
548    #[cfg(feature = "futures")]
549    #[tokio::test]
550    async fn promise_resolves_despite_stale_exception() {
551        let rt = AsyncRuntime::new().unwrap();
552        let ctx = AsyncContext::full(&rt).await.unwrap();
553
554        ctx.async_with(async |ctx| {
555            // Leave a stale JS exception pending
556            let _ = ctx.eval::<(), _>("throw new Error('stale')");
557
558            let func = ctx
559                .eval::<Function, _>(
560                    r"
561                    (function(){
562                        return new Promise((resolve) => resolve(42))
563                    })
564                    ",
565                )
566                .catch(&ctx)
567                .unwrap();
568            let promise: Promise = func.call(()).unwrap();
569            assert_eq!(promise.into_future::<i32>().await.catch(&ctx).unwrap(), 42);
570        })
571        .await
572    }
573
574    #[cfg(feature = "futures")]
575    #[tokio::test]
576    async fn promise_fails_on_interrupt_exception() {
577        use std::sync::atomic::AtomicUsize;
578
579        let rt = AsyncRuntime::new().unwrap();
580        let counter = AtomicUsize::new(0);
581        rt.set_interrupt_handler(Some(Box::new(move || {
582            counter.fetch_add(1, Ordering::Relaxed) > 10
583        })))
584        .await;
585        let ctx = AsyncContext::full(&rt).await.unwrap();
586
587        ctx.async_with(async |ctx| {
588            let func = ctx
589                .eval::<Function, _>(
590                    r"
591                    (function(){
592                        return new Promise((resolve) => {
593                            while(true){}
594                            resolve(42)
595                        })
596                    })
597                    ",
598                )
599                .catch(&ctx)
600                .unwrap();
601            let promise: Promise = func.call(()).catch(&ctx).unwrap();
602            let result = promise.into_future::<i32>().await;
603            assert!(result.is_err(), "should fail due to interrupt");
604        })
605        .await
606    }
607}