Browse Source

Implement `JsPromise` wrapper (#2758)

This Pull Request closes #2687.

It changes the following:

- Adds a `JsPromise` wrapper to manipulate promises from Rust.
- Cleans up the `promise` module to ease the integration of `JsPromise` with it.

cc @lastmjs
pull/2762/head
José Julián Espina 2 years ago
parent
commit
bf47815a49
  1. 27
      boa_engine/src/builtins/async_generator/mod.rs
  2. 27
      boa_engine/src/builtins/iterable/async_from_sync_iterator.rs
  3. 1382
      boa_engine/src/builtins/promise/mod.rs
  4. 1
      boa_engine/src/lib.rs
  5. 19
      boa_engine/src/native_function.rs
  6. 885
      boa_engine/src/object/builtins/jspromise.rs
  7. 8
      boa_engine/src/object/builtins/mod.rs
  8. 1
      boa_engine/src/object/mod.rs
  9. 7
      boa_engine/src/vm/code_block.rs
  10. 12
      boa_engine/src/vm/opcode/await_stm/mod.rs

27
boa_engine/src/builtins/async_generator/mod.rs

@ -114,12 +114,7 @@ impl AsyncGenerator {
// 2. Let promiseCapability be ! NewPromiseCapability(%Promise%).
let promise_capability = PromiseCapability::new(
&context
.intrinsics()
.constructors()
.promise()
.constructor()
.into(),
&context.intrinsics().constructors().promise().constructor(),
context,
)
.expect("cannot fail with promise constructor");
@ -207,12 +202,7 @@ impl AsyncGenerator {
// 2. Let promiseCapability be ! NewPromiseCapability(%Promise%).
let promise_capability = PromiseCapability::new(
&context
.intrinsics()
.constructors()
.promise()
.constructor()
.into(),
&context.intrinsics().constructors().promise().constructor(),
context,
)
.expect("cannot fail with promise constructor");
@ -295,12 +285,7 @@ impl AsyncGenerator {
// 2. Let promiseCapability be ! NewPromiseCapability(%Promise%).
let promise_capability = PromiseCapability::new(
&context
.intrinsics()
.constructors()
.promise()
.constructor()
.into(),
&context.intrinsics().constructors().promise().constructor(),
context,
)
.expect("cannot fail with promise constructor");
@ -557,7 +542,7 @@ impl AsyncGenerator {
// 6. Let promise be ? PromiseResolve(%Promise%, completion.[[Value]]).
let promise_completion = Promise::promise_resolve(
context.intrinsics().constructors().promise().constructor(),
&context.intrinsics().constructors().promise().constructor(),
value,
context,
);
@ -653,8 +638,8 @@ impl AsyncGenerator {
// 11. Perform PerformPromiseThen(promise, onFulfilled, onRejected).
Promise::perform_promise_then(
&promise,
&on_fulfilled.into(),
&on_rejected.into(),
Some(on_fulfilled),
Some(on_rejected),
None,
context,
);

27
boa_engine/src/builtins/iterable/async_from_sync_iterator.rs

@ -100,12 +100,7 @@ impl AsyncFromSyncIterator {
// 3. Let promiseCapability be ! NewPromiseCapability(%Promise%).
let promise_capability = PromiseCapability::new(
&context
.intrinsics()
.constructors()
.promise()
.constructor()
.into(),
&context.intrinsics().constructors().promise().constructor(),
context,
)
.expect("cannot fail with promise constructor");
@ -145,12 +140,7 @@ impl AsyncFromSyncIterator {
// 3. Let promiseCapability be ! NewPromiseCapability(%Promise%).
let promise_capability = PromiseCapability::new(
&context
.intrinsics()
.constructors()
.promise()
.constructor()
.into(),
&context.intrinsics().constructors().promise().constructor(),
context,
)
.expect("cannot fail with promise constructor");
@ -243,12 +233,7 @@ impl AsyncFromSyncIterator {
// 3. Let promiseCapability be ! NewPromiseCapability(%Promise%).
let promise_capability = PromiseCapability::new(
&context
.intrinsics()
.constructors()
.promise()
.constructor()
.into(),
&context.intrinsics().constructors().promise().constructor(),
context,
)
.expect("cannot fail with promise constructor");
@ -348,7 +333,7 @@ impl AsyncFromSyncIterator {
// 6. Let valueWrapper be Completion(PromiseResolve(%Promise%, value)).
let value_wrapper = Promise::promise_resolve(
context.intrinsics().constructors().promise().constructor(),
&context.intrinsics().constructors().promise().constructor(),
value,
context,
);
@ -381,8 +366,8 @@ impl AsyncFromSyncIterator {
// 11. Perform PerformPromiseThen(valueWrapper, onFulfilled, undefined, promiseCapability).
Promise::perform_promise_then(
&value_wrapper,
&on_fulfilled.into(),
&JsValue::Undefined,
Some(on_fulfilled),
None,
Some(promise_capability.clone()),
context,
);

1382
boa_engine/src/builtins/promise/mod.rs

File diff suppressed because it is too large Load Diff

1
boa_engine/src/lib.rs

@ -185,6 +185,7 @@ pub use crate::{
context::Context,
error::{JsError, JsNativeError, JsNativeErrorKind},
native_function::NativeFunction,
object::JsObject,
string::JsString,
symbol::JsSymbol,
value::JsValue,

19
boa_engine/src/native_function.rs

@ -7,12 +7,7 @@ use std::future::Future;
use boa_gc::{custom_trace, Finalize, Gc, Trace};
use crate::{
builtins::Promise,
job::NativeJob,
object::{JsObject, ObjectData},
Context, JsResult, JsValue,
};
use crate::{job::NativeJob, object::JsPromise, Context, JsResult, JsValue};
/// The required signature for all native built-in function pointers.
///
@ -186,22 +181,16 @@ impl NativeFunction {
Fut: Future<Output = JsResult<JsValue>> + 'static,
{
Self::from_copy_closure(move |this, args, context| {
let proto = context.intrinsics().constructors().promise().prototype();
let promise = JsObject::from_proto_and_data(proto, ObjectData::promise(Promise::new()));
let resolving_functions = Promise::create_resolving_functions(&promise, context);
let (promise, resolvers) = JsPromise::new_pending(context);
let future = f(this, args, context);
let future = async move {
let result = future.await;
NativeJob::new(move |ctx| match result {
Ok(v) => resolving_functions
.resolve
.call(&JsValue::undefined(), &[v], ctx),
Ok(v) => resolvers.resolve.call(&JsValue::undefined(), &[v], ctx),
Err(e) => {
let e = e.to_opaque(ctx);
resolving_functions
.reject
.call(&JsValue::undefined(), &[e], ctx)
resolvers.reject.call(&JsValue::undefined(), &[e], ctx)
}
})
};

885
boa_engine/src/object/builtins/jspromise.rs

@ -0,0 +1,885 @@
//! A Rust API wrapper for Boa's promise Builtin ECMAScript Object
#![allow(missing_docs)]
use boa_gc::{Finalize, Trace};
use crate::{
builtins::{
promise::{PromiseState, ResolvingFunctions},
Promise,
},
context::intrinsics::StandardConstructors,
object::{JsObject, JsObjectType, ObjectData},
Context, JsError, JsNativeError, JsResult, JsValue,
};
use super::{JsArray, JsFunction};
/// An ECMAScript [promise] object.
///
/// Known as the concurrency primitive of ECMAScript, this is the main struct used to manipulate,
/// chain and inspect `Promises` from Rust code.
///
/// # Examples
///
/// ```
/// # use boa_engine::{
/// # builtins::promise::PromiseState,
/// # job::SimpleJobQueue,
/// # js_string,
/// # object::{builtins::JsPromise, FunctionObjectBuilder},
/// # property::Attribute,
/// # Context, JsArgs, JsError, JsValue, NativeFunction,
/// # };
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let queue = &SimpleJobQueue::new();
/// let context = &mut Context::builder().job_queue(queue).build()?;
///
/// context.register_global_property("finally", false, Attribute::all());
///
/// let promise = JsPromise::new(
/// |resolvers, context| {
/// let result = js_string!("hello world!").into();
/// resolvers
/// .resolve
/// .call(&JsValue::undefined(), &[result], context)?;
/// Ok(JsValue::undefined())
/// },
/// context,
/// )?;
///
/// let promise = promise
/// .then(
/// Some(
/// FunctionObjectBuilder::new(
/// context,
/// NativeFunction::from_fn_ptr(|_, args, _| {
/// Err(JsError::from_opaque(args.get_or_undefined(0).clone()).into())
/// }),
/// )
/// .build(),
/// ),
/// None,
/// context,
/// )?
/// .catch(
/// FunctionObjectBuilder::new(
/// context,
/// NativeFunction::from_fn_ptr(|_, args, _| {
/// Ok(args.get_or_undefined(0).clone())
/// }),
/// )
/// .build(),
/// context,
/// )?
/// .finally(
/// FunctionObjectBuilder::new(
/// context,
/// NativeFunction::from_fn_ptr(|_, _, context| {
/// context
/// .global_object()
/// .clone()
/// .set("finally", JsValue::from(true), true, context)?;
/// Ok(JsValue::undefined())
/// }),
/// )
/// .build(),
/// context,
/// )?;
///
/// context.run_jobs();
///
/// assert_eq!(
/// promise.state()?,
/// PromiseState::Fulfilled(js_string!("hello world!").into())
/// );
///
/// assert_eq!(
/// context.global_object().clone().get("finally", context)?,
/// JsValue::from(true)
/// );
///
/// # Ok(())
/// # }
/// ```
///
/// [promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
#[derive(Debug, Clone, Trace, Finalize)]
pub struct JsPromise {
inner: JsObject,
}
impl JsPromise {
/// Creates a new promise object from an executor function.
///
/// It is equivalent to calling the [`Promise()`] constructor, which makes it share the same
/// execution semantics as the constructor:
/// - The executor function `executor` is called synchronously just after the promise is created.
/// - The executor return value is ignored.
/// - Any error thrown within the execution of `executor` will call the `reject` function
/// of the newly created promise, unless either `resolve` or `reject` were already called
/// beforehand.
///
/// `executor` receives as an argument the [`ResolvingFunctions`] needed to settle the promise,
/// which can be done by either calling the `resolve` function or the `reject` function.
///
/// # Examples
///
/// ```
/// # use std::error::Error;
/// # use boa_engine::{
/// # job::SimpleJobQueue,
/// # object::builtins::JsPromise,
/// # builtins::promise::PromiseState,
/// # Context, JsValue, js_string
/// # };
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let queue = &SimpleJobQueue::new();
/// let context = &mut Context::builder().job_queue(queue).build()?;
///
/// let promise = JsPromise::new(|resolvers, context| {
/// let result = js_string!("hello world").into();
/// resolvers.resolve.call(&JsValue::undefined(), &[result], context)?;
/// Ok(JsValue::undefined())
/// }, context)?;
///
/// context.run_jobs();
///
/// assert_eq!(promise.state()?, PromiseState::Fulfilled(js_string!("hello world").into()));
/// # Ok(())
/// # }
/// ```
///
/// [`Promise()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise
pub fn new<F>(executor: F, context: &mut Context<'_>) -> JsResult<JsPromise>
where
F: FnOnce(&ResolvingFunctions, &mut Context<'_>) -> JsResult<JsValue>,
{
let promise = JsObject::from_proto_and_data(
context.intrinsics().constructors().promise().prototype(),
ObjectData::promise(Promise::new()),
);
let resolvers = Promise::create_resolving_functions(&promise, context);
if let Err(e) = executor(&resolvers, context) {
let e = e.to_opaque(context);
resolvers
.reject
.call(&JsValue::undefined(), &[e], context)?;
}
Ok(JsPromise { inner: promise })
}
/// Creates a new pending promise and returns it and its associated `ResolvingFunctions`.
///
/// This can be useful when you want to manually settle a promise from Rust code, instead of
/// running an `executor` function that automatically settles the promise on creation
/// (see [`JsPromise::new`]).
///
/// # Examples
///
/// ```
/// # use std::error::Error;
/// # use boa_engine::{
/// # object::builtins::JsPromise,
/// # builtins::promise::PromiseState,
/// # Context, JsValue
/// # };
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let context = &mut Context::default();
///
/// let (promise, resolvers) = JsPromise::new_pending(context);
///
/// assert_eq!(promise.state()?, PromiseState::Pending);
///
/// resolvers.reject.call(&JsValue::undefined(), &[5.into()], context)?;
///
/// assert_eq!(promise.state()?, PromiseState::Rejected(5.into()));
///
/// # Ok(())
/// # }
/// ```
#[inline]
pub fn new_pending(context: &mut Context<'_>) -> (JsPromise, ResolvingFunctions) {
let promise = JsObject::from_proto_and_data(
context.intrinsics().constructors().promise().prototype(),
ObjectData::promise(Promise::new()),
);
let resolvers = Promise::create_resolving_functions(&promise, context);
let promise = JsPromise::from_object(promise)
.expect("this shouldn't fail with a newly created promise");
(promise, resolvers)
}
/// Wraps an existing object with the `JsPromise` interface, returning `Err` if the object
/// is not a valid promise.
///
/// # Examples
///
/// ```
/// # use std::error::Error;
/// # use boa_engine::{
/// # object::builtins::JsPromise,
/// # builtins::promise::PromiseState,
/// # Context, JsObject, JsValue, Source
/// # };
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let context = &mut Context::default();
///
/// let promise = context.eval_script(Source::from_bytes("new Promise((resolve, reject) => resolve())"))?;
/// let promise = promise.as_object().cloned().unwrap();
///
/// let promise = JsPromise::from_object(promise)?;
///
/// assert_eq!(promise.state()?, PromiseState::Fulfilled(JsValue::undefined()));
///
/// assert!(JsPromise::from_object(JsObject::with_null_proto()).is_err());
///
/// # Ok(())
/// # }
/// ```
#[inline]
pub fn from_object(object: JsObject) -> JsResult<JsPromise> {
if !object.is_promise() {
return Err(JsNativeError::typ()
.with_message("`object` is not a Promise")
.into());
}
Ok(JsPromise { inner: object })
}
/// Resolves a `JsValue` into a `JsPromise`.
///
/// Equivalent to the [`Promise.resolve()`] static method.
///
/// This function is mainly used to wrap a plain `JsValue` into a fulfilled promise, but it can
/// also flatten nested layers of [thenables], which essentially converts them into native
/// promises.
///
/// # Examples
///
/// ```
/// # use std::error::Error;
/// # use boa_engine::{
/// # object::builtins::JsPromise,
/// # builtins::promise::PromiseState,
/// # Context, js_string
/// # };
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let context = &mut Context::default();
///
/// let promise = JsPromise::resolve(js_string!("resolved!"), context)?;
///
/// assert_eq!(promise.state()?, PromiseState::Fulfilled(js_string!("resolved!").into()));
///
/// # Ok(())
/// # }
/// ```
///
/// [`Promise.resolve()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve
/// [thenables]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise#thenables
pub fn resolve<V: Into<JsValue>>(value: V, context: &mut Context<'_>) -> JsResult<JsPromise> {
Promise::promise_resolve(
&context.intrinsics().constructors().promise().constructor(),
value.into(),
context,
)
.and_then(JsPromise::from_object)
}
/// Creates a `JsPromise` that is rejected with the reason `error`.
///
/// Equivalent to the [`Promise.reject`] static method.
///
/// `JsPromise::reject` is pretty similar to [`JsPromise::resolve`], with the difference that
/// it always wraps `error` into a rejected promise, even if `error` is a promise or a [thenable].
///
/// # Examples
///
/// ```
/// # use std::error::Error;
/// # use boa_engine::{
/// # object::builtins::JsPromise,
/// # builtins::promise::PromiseState,
/// # Context, js_string, JsError
/// # };
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let context = &mut Context::default();
///
/// let promise = JsPromise::reject(JsError::from_opaque(js_string!("oops!").into()), context)?;
///
/// assert_eq!(promise.state()?, PromiseState::Rejected(js_string!("oops!").into()));
///
/// # Ok(())
/// # }
/// ```
///
/// [`Promise.reject`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/reject
/// [thenable]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise#thenables
pub fn reject<E: Into<JsError>>(error: E, context: &mut Context<'_>) -> JsResult<JsPromise> {
Promise::promise_reject(
&context.intrinsics().constructors().promise().constructor(),
&error.into(),
context,
)
.and_then(JsPromise::from_object)
}
/// Gets the current state of the promise.
///
/// # Examples
///
/// ```
/// # use std::error::Error;
/// # use boa_engine::{
/// # object::builtins::JsPromise,
/// # builtins::promise::PromiseState,
/// # Context
/// # };
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let context = &mut Context::default();
///
/// let promise = JsPromise::new_pending(context).0;
///
/// assert_eq!(promise.state()?, PromiseState::Pending);
///
/// # Ok(())
/// # }
/// ```
#[inline]
pub fn state(&self) -> JsResult<PromiseState> {
// TODO: if we can guarantee that objects cannot change type after creation,
// we can remove this throw.
let promise = self.inner.borrow();
let promise = promise
.as_promise()
.ok_or_else(|| JsNativeError::typ().with_message("object is not a Promise"))?;
Ok(promise.state().clone())
}
/// Schedules callback functions to run when the promise settles.
///
/// Equivalent to the [`Promise.prototype.then`] method.
///
/// The return value is a promise that is always pending on return, regardless of the current
/// state of the original promise. Two handlers can be provided as callbacks to be executed when
/// the original promise settles:
///
/// - If the original promise is fulfilled, `on_fulfilled` is called with the fulfillment value
/// of the original promise.
/// - If the original promise is rejected, `on_rejected` is called with the rejection reason
/// of the original promise.
///
/// The return value of the handlers can be used to mutate the state of the created promise. If
/// the callback:
///
/// - returns a value: the created promise gets fulfilled with the returned value.
/// - doesn't return: the created promise gets fulfilled with undefined.
/// - throws: the created promise gets rejected with the thrown error as its value.
/// - returns a fulfilled promise: the created promise gets fulfilled with that promise's value as its value.
/// - returns a rejected promise: the created promise gets rejected with that promise's value as its value.
/// - returns another pending promise: the created promise remains pending but becomes settled with that
/// promise's value as its value immediately after that promise becomes settled.
///
/// # Examples
///
/// ```
/// # use std::error::Error;
/// # use boa_engine::{
/// # builtins::promise::PromiseState,
/// # job::SimpleJobQueue,
/// # js_string,
/// # object::{builtins::JsPromise, FunctionObjectBuilder},
/// # Context, JsArgs, JsError, JsValue, NativeFunction,
/// # };
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let queue = &SimpleJobQueue::new();
/// let context = &mut Context::builder().job_queue(queue).build()?;
///
/// let promise = JsPromise::new(
/// |resolvers, context| {
/// resolvers
/// .resolve
/// .call(&JsValue::undefined(), &[255.255.into()], context)?;
/// Ok(JsValue::undefined())
/// },
/// context,
/// )?.then(
/// Some(
/// FunctionObjectBuilder::new(
/// context,
/// NativeFunction::from_fn_ptr(|_, args, context| {
/// args.get_or_undefined(0).to_string(context).map(JsValue::from)
/// }),
/// )
/// .build(),
/// ),
/// None,
/// context,
/// )?;
///
/// context.run_jobs();
///
/// assert_eq!(
/// promise.state()?,
/// PromiseState::Fulfilled(js_string!("255.255").into())
/// );
///
/// # Ok(())
/// # }
/// ```
///
/// [`Promise.prototype.then`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then
#[inline]
pub fn then(
&self,
on_fulfilled: Option<JsFunction>,
on_rejected: Option<JsFunction>,
context: &mut Context<'_>,
) -> JsResult<JsPromise> {
let result_promise = Promise::inner_then(self, on_fulfilled, on_rejected, context)?;
JsPromise::from_object(result_promise)
}
/// Schedules a callback to run when the promise is rejected.
///
/// Equivalent to the [`Promise.prototype.catch`] method.
///
/// This is essentially a shortcut for calling [`promise.then(None, Some(function))`][then], which
/// only handles the error case and leaves the fulfilled case untouched.
///
/// # Examples
///
/// ```
/// # use std::error::Error;
/// # use boa_engine::{
/// # js_string,
/// # builtins::promise::PromiseState,
/// # job::SimpleJobQueue,
/// # object::{builtins::JsPromise, FunctionObjectBuilder},
/// # Context, JsArgs, JsNativeError, JsValue, NativeFunction,
/// # };
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let queue = &SimpleJobQueue::new();
/// let context = &mut Context::builder().job_queue(queue).build()?;
///
/// let promise = JsPromise::new(
/// |resolvers, context| {
/// let error = JsNativeError::typ().with_message("thrown");
/// let error = error.to_opaque(context);
/// resolvers
/// .reject
/// .call(&JsValue::undefined(), &[error.into()], context)?;
/// Ok(JsValue::undefined())
/// },
/// context,
/// )?.catch(
/// FunctionObjectBuilder::new(
/// context,
/// NativeFunction::from_fn_ptr(|_, args, context| {
/// args.get_or_undefined(0).to_string(context).map(JsValue::from)
/// }),
/// )
/// .build(),
/// context,
/// )?;
///
/// context.run_jobs();
///
/// assert_eq!(
/// promise.state()?,
/// PromiseState::Fulfilled(js_string!("TypeError: thrown").into())
/// );
///
/// # Ok(())
/// # }
/// ```
///
/// [`Promise.prototype.catch`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch
/// [then]: JsPromise::then
#[inline]
pub fn catch(&self, on_rejected: JsFunction, context: &mut Context<'_>) -> JsResult<JsPromise> {
self.then(None, Some(on_rejected), context)
}
/// Schedules a callback to run when the promise is rejected.
///
/// Equivalent to the [`Promise.prototype.finally()`] method.
///
/// While this could be seen as a shortcut for calling [`promise.then(Some(function), Some(function))`][then],
/// it has slightly different semantics than `then`:
/// - `on_finally` doesn't receive any argument, unlike `on_fulfilled` and `on_rejected`.
/// - `finally()` is transparent; a call like `Promise.resolve("first").finally(() => "second")`
/// returns a promise fulfilled with the value `"first"`, which would return `"second"` if `finally`
/// was a shortcut of `then`.
///
/// # Examples
///
/// ```
/// # use std::error::Error;
/// # use boa_engine::{
/// # job::SimpleJobQueue,
/// # object::{builtins::JsPromise, FunctionObjectBuilder},
/// # property::Attribute,
/// # Context, JsNativeError, JsValue, NativeFunction,
/// # };
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let queue = &SimpleJobQueue::new();
/// let context = &mut Context::builder().job_queue(queue).build()?;
///
/// context.register_global_property("finally", false, Attribute::all());
///
/// let promise = JsPromise::new(
/// |resolvers, context| {
/// let error = JsNativeError::typ().with_message("thrown");
/// let error = error.to_opaque(context);
/// resolvers
/// .reject
/// .call(&JsValue::undefined(), &[error.into()], context)?;
/// Ok(JsValue::undefined())
/// },
/// context,
/// )?.finally(
/// FunctionObjectBuilder::new(
/// context,
/// NativeFunction::from_fn_ptr(|_, _, context| {
/// context
/// .global_object()
/// .clone()
/// .set("finally", JsValue::from(true), true, context)?;
/// Ok(JsValue::undefined())
/// }),
/// )
/// .build(),
/// context,
/// )?;
///
/// context.run_jobs();
///
/// assert_eq!(
/// context.global_object().clone().get("finally", context)?,
/// JsValue::from(true)
/// );
///
/// # Ok(())
/// # }
/// ```
///
/// [`Promise.prototype.finally()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/finally
/// [then]: JsPromise::then
#[inline]
pub fn finally(
&self,
on_finally: JsFunction,
context: &mut Context<'_>,
) -> JsResult<JsPromise> {
let c = self.species_constructor(StandardConstructors::promise, context)?;
let (then, catch) = Promise::then_catch_finally_closures(c, on_finally, context);
self.then(Some(then), Some(catch), context)
}
/// Waits for a list of promises to settle with fulfilled values, rejecting the aggregate promise
/// when any of the inner promises is rejected.
///
/// Equivalent to the [`Promise.all`] static method.
///
/// # Examples
///
/// ```
/// # use std::error::Error;
/// # use boa_engine::{
/// # job::SimpleJobQueue,
/// # js_string,
/// # object::builtins::{JsArray, JsPromise},
/// # Context, JsNativeError, JsValue,
/// # };
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let queue = &SimpleJobQueue::new();
/// let context = &mut Context::builder().job_queue(queue).build()?;
///
/// let promise1 = JsPromise::all(
/// [
/// JsPromise::resolve(0, context)?,
/// JsPromise::resolve(2, context)?,
/// JsPromise::resolve(4, context)?,
/// ],
/// context,
/// )?;
///
/// let promise2 = JsPromise::all(
/// [
/// JsPromise::resolve(1, context)?,
/// JsPromise::reject(JsNativeError::typ(), context)?,
/// JsPromise::resolve(3, context)?,
/// ],
/// context,
/// )?;
///
/// context.run_jobs();
///
/// let array = promise1.state()?.as_fulfilled().and_then(JsValue::as_object).unwrap().clone();
/// let array = JsArray::from_object(array)?;
/// assert_eq!(array.at(0, context)?, 0.into());
/// assert_eq!(array.at(1, context)?, 2.into());
/// assert_eq!(array.at(2, context)?, 4.into());
///
/// let error = promise2.state()?.as_rejected().unwrap().clone();
/// assert_eq!(error.to_string(context)?, js_string!("TypeError"));
///
/// # Ok(())
/// # }
/// ```
///
/// [`Promise.all`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all
pub fn all<I>(promises: I, context: &mut Context<'_>) -> JsResult<JsPromise>
where
I: IntoIterator<Item = JsPromise>,
{
let promises = JsArray::from_iter(promises.into_iter().map(JsValue::from), context);
let c = &context
.intrinsics()
.constructors()
.promise()
.constructor()
.into();
let value = Promise::all(c, &[promises.into()], context)?;
let value = value
.as_object()
.expect("Promise.all always returns an object on success");
JsPromise::from_object(value.clone())
}
/// Waits for a list of promises to settle, fulfilling with an array of the outcomes of every
/// promise.
///
/// Equivalent to the [`Promise.allSettled`] static method.
///
/// # Examples
///
/// ```
/// # use std::error::Error;
/// # use boa_engine::{
/// # job::SimpleJobQueue,
/// # js_string,
/// # object::builtins::{JsArray, JsPromise},
/// # Context, JsNativeError, JsValue,
/// # };
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let queue = &SimpleJobQueue::new();
/// let context = &mut Context::builder().job_queue(queue).build()?;
///
///
/// let promise = JsPromise::all_settled(
/// [
/// JsPromise::resolve(1, context)?,
/// JsPromise::reject(JsNativeError::typ(), context)?,
/// JsPromise::resolve(3, context)?,
/// ],
/// context,
/// )?;
///
/// context.run_jobs();
///
/// let array = promise.state()?.as_fulfilled().and_then(JsValue::as_object).unwrap().clone();
/// let array = JsArray::from_object(array)?;
///
/// let a = array.at(0, context)?.as_object().unwrap().clone();
/// assert_eq!(a.get("status", context)?, js_string!("fulfilled").into());
/// assert_eq!(a.get("value", context)?, 1.into());
///
/// let b = array.at(1, context)?.as_object().unwrap().clone();
/// assert_eq!(b.get("status", context)?, js_string!("rejected").into());
/// assert_eq!(b.get("reason", context)?.to_string(context)?, js_string!("TypeError"));
///
/// let c = array.at(2, context)?.as_object().unwrap().clone();
/// assert_eq!(c.get("status", context)?, js_string!("fulfilled").into());
/// assert_eq!(c.get("value", context)?, 3.into());
///
/// # Ok(())
/// # }
/// ```
///
/// [`Promise.allSettled`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled
pub fn all_settled<I>(promises: I, context: &mut Context<'_>) -> JsResult<JsPromise>
where
I: IntoIterator<Item = JsPromise>,
{
let promises = JsArray::from_iter(promises.into_iter().map(JsValue::from), context);
let c = &context
.intrinsics()
.constructors()
.promise()
.constructor()
.into();
let value = Promise::all_settled(c, &[promises.into()], context)?;
let value = value
.as_object()
.expect("Promise.allSettled always returns an object on success");
JsPromise::from_object(value.clone())
}
/// Returns the first promise that fulfills from a list of promises.
///
/// Equivalent to the [`Promise.any`] static method.
///
/// If after settling all promises in `promises` there isn't a fulfilled promise, the returned
/// promise will be rejected with an `AggregatorError` containing the rejection values of every
/// promise; this includes the case where `promises` is an empty iterator.
///
/// # Examples
///
/// ```
/// # use std::error::Error;
/// # use boa_engine::{
/// # builtins::promise::PromiseState,
/// # job::SimpleJobQueue,
/// # js_string,
/// # object::builtins::JsPromise,
/// # Context, JsNativeError,
/// # };
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let queue = &SimpleJobQueue::new();
/// let context = &mut Context::builder().job_queue(queue).build()?;
///
///
/// let promise = JsPromise::any(
/// [
/// JsPromise::reject(JsNativeError::syntax(), context)?,
/// JsPromise::reject(JsNativeError::typ(), context)?,
/// JsPromise::resolve(js_string!("fulfilled"), context)?,
/// JsPromise::reject(JsNativeError::range(), context)?,
/// ],
/// context,
/// )?;
///
/// context.run_jobs();
///
/// assert_eq!(promise.state()?, PromiseState::Fulfilled(js_string!("fulfilled").into()));
///
/// # Ok(())
/// # }
/// ```
///
/// [`Promise.any`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/any
pub fn any<I>(promises: I, context: &mut Context<'_>) -> JsResult<JsPromise>
where
I: IntoIterator<Item = JsPromise>,
{
let promises = JsArray::from_iter(promises.into_iter().map(JsValue::from), context);
let c = &context
.intrinsics()
.constructors()
.promise()
.constructor()
.into();
let value = Promise::any(c, &[promises.into()], context)?;
let value = value
.as_object()
.expect("Promise.any always returns an object on success");
JsPromise::from_object(value.clone())
}
/// Returns the first promise that settles from a list of promises.
///
/// Equivalent to the [`Promise.race`] static method.
///
/// If the provided iterator is empty, the returned promise will remain on the pending state
/// forever.
///
/// # Examples
///
/// ```
/// # use std::error::Error;
/// # use boa_engine::{
/// # builtins::promise::PromiseState,
/// # job::SimpleJobQueue,
/// # js_string,
/// # object::builtins::JsPromise,
/// # Context, JsValue,
/// # };
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let queue = &SimpleJobQueue::new();
/// let context = &mut Context::builder().job_queue(queue).build()?;
///
/// let (a, resolvers_a) = JsPromise::new_pending(context);
/// let (b, resolvers_b) = JsPromise::new_pending(context);
/// let (c, resolvers_c) = JsPromise::new_pending(context);
///
/// let promise = JsPromise::race([a, b, c], context)?;
///
/// resolvers_b.reject.call(&JsValue::undefined(), &[], context);
/// resolvers_a.resolve.call(&JsValue::undefined(), &[5.into()], context);
/// resolvers_c.reject.call(&JsValue::undefined(), &[js_string!("c error").into()], context);
///
/// context.run_jobs();
///
/// assert_eq!(
/// promise.state()?,
/// PromiseState::Rejected(JsValue::undefined())
/// );
///
/// # Ok(())
/// # }
/// ```
///
/// [`Promise.race`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race
pub fn race<I>(promises: I, context: &mut Context<'_>) -> JsResult<JsPromise>
where
I: IntoIterator<Item = JsPromise>,
{
let promises = JsArray::from_iter(promises.into_iter().map(JsValue::from), context);
let c = &context
.intrinsics()
.constructors()
.promise()
.constructor()
.into();
let value = Promise::race(c, &[promises.into()], context)?;
let value = value
.as_object()
.expect("Promise.race always returns an object on success");
JsPromise::from_object(value.clone())
}
}
impl From<JsPromise> for JsObject {
#[inline]
fn from(o: JsPromise) -> Self {
o.inner.clone()
}
}
impl From<JsPromise> for JsValue {
#[inline]
fn from(o: JsPromise) -> Self {
o.inner.clone().into()
}
}
impl std::ops::Deref for JsPromise {
type Target = JsObject;
#[inline]
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl JsObjectType for JsPromise {}

8
boa_engine/src/object/builtins/mod.rs

@ -1,6 +1,6 @@
//! All Rust API wrappers for Boa's ECMAScript objects.
//!
//! The structs available in this module provide functionality to interact with the implemented ECMAScript object from Rust.
//! The structs available in this module provide functionality to interact with native ECMAScript objects from Rust.
mod jsarray;
mod jsarraybuffer;
@ -10,7 +10,8 @@ mod jsfunction;
mod jsgenerator;
mod jsmap;
mod jsmap_iterator;
pub(crate) mod jsproxy;
mod jspromise;
mod jsproxy;
mod jsregexp;
mod jsset;
mod jsset_iterator;
@ -24,7 +25,8 @@ pub use jsfunction::*;
pub use jsgenerator::*;
pub use jsmap::*;
pub use jsmap_iterator::*;
pub use jsproxy::{JsProxy, JsRevocableProxy};
pub use jspromise::*;
pub use jsproxy::{JsProxy, JsProxyBuilder, JsRevocableProxy};
pub use jsregexp::JsRegExp;
pub use jsset::*;
pub use jsset_iterator::*;

1
boa_engine/src/object/mod.rs

@ -75,7 +75,6 @@ mod property_map;
pub(crate) use builtins::*;
pub use builtins::jsproxy::JsProxyBuilder;
pub use jsobject::*;
pub(crate) trait JsObjectType:

7
boa_engine/src/vm/code_block.rs

@ -575,12 +575,7 @@ pub(crate) fn create_function_object(
let function = if r#async {
let promise_capability = PromiseCapability::new(
&context
.intrinsics()
.constructors()
.promise()
.constructor()
.into(),
&context.intrinsics().constructors().promise().constructor(),
context,
)
.expect("cannot fail per spec");

12
boa_engine/src/vm/opcode/await_stm/mod.rs

@ -27,13 +27,11 @@ impl Operation for Await {
let value = context.vm.pop();
// 2. Let promise be ? PromiseResolve(%Promise%, value).
let resolve_result = Promise::promise_resolve(
context.intrinsics().constructors().promise().constructor(),
let promise = Promise::promise_resolve(
&context.intrinsics().constructors().promise().constructor(),
value,
context,
);
let promise = resolve_result?;
)?;
// 3. Let fulfilledClosure be a new Abstract Closure with parameters (value) that captures asyncContext and performs the following steps when called:
// 4. Let onFulfilled be CreateBuiltinFunction(fulfilledClosure, 1, "", « »).
@ -124,8 +122,8 @@ impl Operation for Await {
// 7. Perform PerformPromiseThen(promise, onFulfilled, onRejected).
Promise::perform_promise_then(
&promise,
&on_fulfilled.into(),
&on_rejected.into(),
Some(on_fulfilled),
Some(on_rejected),
None,
context,
);

Loading…
Cancel
Save