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