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        if this.state.is_none() {
194            let inner = Rc::new(RefCell::new(cx.waker().clone()));
195            this.state = Some(inner.clone());
196
197            let resolve = Function::new(this.promise.ctx.clone(), move || {
198                inner.borrow().wake_by_ref();
199            })?;
200
201            this.promise.then()?.call::<_, ()>((
202                This(this.promise.clone()),
203                resolve.clone(),
204                resolve,
205            ))?;
206            return Poll::Pending;
207        }
208
209        this.state
210            .as_ref()
211            .unwrap()
212            .borrow_mut()
213            .clone_from(cx.waker());
214
215        Poll::Pending
216    }
217}
218
219/// Wrapper for futures to convert to JS promises
220#[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "futures")))]
221#[repr(transparent)]
222#[cfg(feature = "futures")]
223pub struct Promised<T>(pub T);
224
225#[cfg(feature = "futures")]
226impl<T> From<T> for Promised<T> {
227    fn from(future: T) -> Self {
228        Self(future)
229    }
230}
231
232#[cfg(feature = "futures")]
233impl<'js, T, R> IntoJs<'js> for Promised<T>
234where
235    T: Future<Output = R> + 'js,
236    R: IntoJs<'js> + 'js,
237{
238    fn into_js(self, ctx: &Ctx<'js>) -> Result<Value<'js>> {
239        Promise::wrap_future(ctx, self.0).map(|x| x.into_value())
240    }
241}
242
243/// A type which behaves like a promise but can wrap any javascript value.
244///
245/// This type is usefull when you are unsure if a function will return a promise.
246/// You can call finish and turn it into a future like a normal promise.
247/// When the value this type us converted isn't a promise it will behave like an promise which is
248/// already resolved, otherwise it will call the right functions on the promise.
249#[derive(Debug, PartialEq, Clone, Hash, Eq)]
250pub struct MaybePromise<'js>(Value<'js>);
251
252impl<'js> FromJs<'js> for MaybePromise<'js> {
253    fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> Result<Self> {
254        Ok(MaybePromise(value))
255    }
256}
257
258impl<'js> IntoJs<'js> for MaybePromise<'js> {
259    fn into_js(self, _ctx: &Ctx<'js>) -> Result<Value<'js>> {
260        Ok(self.0)
261    }
262}
263
264impl<'js> MaybePromise<'js> {
265    /// Reference to the inner value
266    pub fn as_value(&self) -> &Value<'js> {
267        &self.0
268    }
269
270    /// Convert into the inner value
271    pub fn into_value(self) -> Value<'js> {
272        self.0
273    }
274
275    /// Convert into the inner value
276    pub fn from_value(value: Value<'js>) -> Self {
277        MaybePromise(value)
278    }
279
280    /// Returns the [`Ctx`] object associated with this value
281    pub fn ctx(&self) -> &Ctx<'js> {
282        self.0.ctx()
283    }
284
285    /// Returns [`PromiseState::Resolved`] if the wrapped value isn't a promise, otherwise calls
286    /// [`Promise::state`] on the promise and returns it's value.
287    pub fn state(&self) -> PromiseState {
288        if let Some(x) = self.0.as_promise() {
289            x.state()
290        } else {
291            PromiseState::Resolved
292        }
293    }
294
295    /// Returns the value if self isn't a promise, otherwise calls [`Promise::result`] on the promise.
296    pub fn result<T: FromJs<'js>>(&self) -> Option<Result<T>> {
297        if let Some(x) = self.0.as_promise() {
298            x.result::<T>()
299        } else {
300            Some(T::from_js(self.0.ctx(), self.0.clone()))
301        }
302    }
303
304    /// Returns the value if self isn't a promise, otherwise calls [`Promise::finish`] on the promise.
305    pub fn finish<T: FromJs<'js>>(&self) -> Result<T> {
306        if let Some(x) = self.0.as_promise() {
307            x.finish::<T>()
308        } else {
309            T::from_js(self.0.ctx(), self.0.clone())
310        }
311    }
312
313    /// Convert self into a future which will return ready if the wrapped value isn't a promise,
314    /// otherwise it will handle the promise like the future returned from
315    /// [`Promise::into_future`].
316    #[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "futures")))]
317    #[cfg(feature = "futures")]
318    pub fn into_future<T: FromJs<'js>>(self) -> MaybePromiseFuture<'js, T> {
319        if self.0.is_promise() {
320            let fut = self.0.into_promise().unwrap().into_future();
321            MaybePromiseFuture(MaybePromiseFutureInner::Future(fut))
322        } else {
323            MaybePromiseFuture(MaybePromiseFutureInner::Ready(self.0))
324        }
325    }
326}
327
328/// Future-aware maybe promise
329#[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "futures")))]
330#[cfg(feature = "futures")]
331#[must_use = "futures do nothing unless you `.await` or poll them"]
332#[derive(Debug)]
333pub struct MaybePromiseFuture<'js, T>(MaybePromiseFutureInner<'js, T>);
334
335#[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "futures")))]
336#[cfg(feature = "futures")]
337#[derive(Debug)]
338enum MaybePromiseFutureInner<'js, T> {
339    Ready(Value<'js>),
340    Future(PromiseFuture<'js, T>),
341}
342
343#[cfg(feature = "futures")]
344impl<'js, T> Future for MaybePromiseFuture<'js, T>
345where
346    T: FromJs<'js>,
347{
348    type Output = Result<T>;
349
350    fn poll(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Self::Output> {
351        match self.get_mut().0 {
352            MaybePromiseFutureInner::Ready(ref x) => Poll::Ready(T::from_js(x.ctx(), x.clone())),
353            MaybePromiseFutureInner::Future(ref mut x) => Pin::new(x).poll(cx),
354        }
355    }
356}
357
358#[cfg(test)]
359mod test {
360    use std::sync::atomic::{AtomicBool, Ordering};
361    #[cfg(feature = "futures")]
362    use std::time::Duration;
363
364    use super::Promise;
365    #[cfg(feature = "futures")]
366    use crate::{
367        async_with, function::Async, promise::Promised, AsyncContext, AsyncRuntime, CaughtError,
368        Result,
369    };
370    use crate::{
371        function::Func, prelude::This, promise::PromiseState, CatchResultExt, Context, Function,
372        Runtime,
373    };
374
375    #[cfg(feature = "futures")]
376    async fn set_timeout<'js>(cb: Function<'js>, number: f64) -> Result<()> {
377        tokio::time::sleep(Duration::from_secs_f64(number / 1000.0)).await;
378        cb.call::<_, ()>(())
379    }
380
381    #[cfg(feature = "futures")]
382    #[tokio::test]
383    async fn promise() {
384        let rt = AsyncRuntime::new().unwrap();
385        let ctx = AsyncContext::full(&rt).await.unwrap();
386
387        async_with!(ctx => |ctx| {
388            ctx.globals().set("setTimeout",Func::from(Async(set_timeout))).unwrap();
389
390            let func = ctx
391                .eval::<Function, _>(
392                    r"
393                    (function(){
394                        return new Promise((resolve) => {
395                            setTimeout(x => {
396                                resolve(42)
397                            },100)
398                        })
399                    })
400                    ",
401                )
402                .catch(&ctx)
403                .unwrap();
404            let promise: Promise = func.call(()).unwrap();
405            assert_eq!(promise.into_future::<i32>().await.catch(&ctx).unwrap(), 42);
406
407            let func = ctx
408                .eval::<Function, _>(
409                    r"
410                    (function(){
411                        return new Promise((_,reject) => {
412                            setTimeout(x => {
413                                reject(42)
414                            },100)
415                        })
416                    })
417                    ",
418                )
419                .catch(&ctx)
420                .unwrap();
421            let promise: Promise = func.call(()).unwrap();
422            let err = promise.into_future::<()>().await.catch(&ctx);
423            match err {
424                Err(CaughtError::Value(v)) => {
425                    assert_eq!(v.as_int().unwrap(), 42)
426                }
427                _ => panic!(),
428            }
429        })
430        .await
431    }
432
433    #[cfg(feature = "futures")]
434    #[tokio::test]
435    async fn promised() {
436        use crate::Exception;
437
438        let rt = AsyncRuntime::new().unwrap();
439        let ctx = AsyncContext::full(&rt).await.unwrap();
440
441        async_with!(ctx => |ctx| {
442            let promised = Promised::from(async {
443                tokio::time::sleep(Duration::from_millis(100)).await;
444                42
445            });
446
447            let function = ctx.eval::<Function,_>(r"
448                (async function(v){
449                    let val = await v;
450                    if(val !== 42){
451                        throw new Error('not correct value')
452                    }
453                })
454            ").catch(&ctx).unwrap();
455
456            function.call::<_,Promise>((promised,)).unwrap().into_future::<()>().await.unwrap();
457
458            let ctx_clone = ctx.clone();
459            let promised = Promised::from(async move {
460                tokio::time::sleep(Duration::from_millis(100)).await;
461                Result::<()>::Err(Exception::throw_message(&ctx_clone, "some_message"))
462            });
463
464            let function = ctx.eval::<Function,_>(r"
465                (async function(v){
466                    try{
467                        await v;
468                    }catch(e) {
469                        if (e.message !== 'some_message'){
470                            throw new Error('wrong error')
471                        }
472                        return
473                    }
474                    throw new Error('no error thrown')
475                })
476            ")
477                .catch(&ctx)
478                .unwrap();
479
480
481            function.call::<_,Promise>((promised,)).unwrap().into_future::<()>().await.unwrap()
482        })
483        .await
484    }
485
486    #[test]
487    fn promise_then() {
488        static DID_EXECUTE: AtomicBool = AtomicBool::new(false);
489
490        let rt = Runtime::new().unwrap();
491        let ctx = Context::full(&rt).unwrap();
492
493        ctx.with(|ctx| {
494            let (promise, resolve, _) = Promise::new(&ctx).unwrap();
495
496            let cb = Func::new(|s: String| {
497                assert_eq!(s, "FOO");
498                DID_EXECUTE.store(true, Ordering::SeqCst);
499            });
500
501            assert_eq!(promise.state(), PromiseState::Pending);
502
503            promise
504                .get::<_, Function>("then")
505                .catch(&ctx)
506                .unwrap()
507                .call::<_, ()>((This(promise.clone()), cb))
508                .catch(&ctx)
509                .unwrap();
510
511            resolve.call::<_, ()>(("FOO",)).unwrap();
512            assert_eq!(promise.state(), PromiseState::Resolved);
513
514            while ctx.execute_pending_job() {}
515
516            assert!(DID_EXECUTE.load(Ordering::SeqCst));
517        })
518    }
519}