Rust编写的JavaScript引擎,该项目是一个试验性质的项目。
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

2423 lines
99 KiB

//! Boa's implementation of ECMAScript's global `Promise` object.
#[cfg(test)]
mod tests;
use super::{iterable::IteratorRecord, BuiltInBuilder, BuiltInConstructor, IntrinsicObject};
use crate::{
builtins::{Array, BuiltInObject},
context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors},
error::JsNativeError,
job::{JobCallback, NativeJob},
js_string,
native_function::NativeFunction,
object::{
internal_methods::get_prototype_from_constructor, FunctionObjectBuilder, JsFunction,
JsObject, CONSTRUCTOR,
},
property::Attribute,
realm::Realm,
string::{common::StaticJsStrings, utf16},
symbol::JsSymbol,
value::JsValue,
Context, JsArgs, JsError, JsResult, JsString,
};
use boa_gc::{custom_trace, Finalize, Gc, GcRefCell, Trace};
use boa_macros::JsData;
use boa_profiler::Profiler;
use std::{cell::Cell, rc::Rc};
use tap::{Conv, Pipe};
// ==================== Public API ====================
/// The current state of a [`Promise`].
#[derive(Debug, Clone, Finalize, PartialEq, Eq)]
pub enum PromiseState {
/// The promise hasn't been resolved.
Pending,
/// The promise was fulfilled with a success value.
Fulfilled(JsValue),
/// The promise was rejected with a failure reason.
Rejected(JsValue),
}
unsafe impl Trace for PromiseState {
custom_trace!(this, mark, {
match this {
Self::Fulfilled(v) | Self::Rejected(v) => mark(v),
Self::Pending => {}
}
});
}
impl PromiseState {
/// Gets the inner `JsValue` of a fulfilled promise state, or returns `None` if
/// the state is not `Fulfilled`.
#[must_use]
pub const fn as_fulfilled(&self) -> Option<&JsValue> {
match self {
Self::Fulfilled(v) => Some(v),
_ => None,
}
}
/// Gets the inner `JsValue` of a rejected promise state, or returns `None` if
/// the state is not `Rejected`.
#[must_use]
pub const fn as_rejected(&self) -> Option<&JsValue> {
match self {
Self::Rejected(v) => Some(v),
_ => None,
}
}
}
/// The internal representation of a `Promise` object.
#[derive(Debug, Trace, Finalize, JsData)]
pub struct Promise {
state: PromiseState,
fulfill_reactions: Vec<ReactionRecord>,
reject_reactions: Vec<ReactionRecord>,
handled: bool,
}
/// The operation type of the [`HostPromiseRejectionTracker`][fn] abstract operation.
///
/// # Note
///
/// Per the spec:
///
/// > If operation is "handle", an implementation should not hold a reference to promise in a way
/// that would interfere with garbage collection. An implementation may hold a reference to promise
/// if operation is "reject", since it is expected that rejections will be rare and not on hot code paths.
///
/// [fn]: https://tc39.es/ecma262/#sec-host-promise-rejection-tracker
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OperationType {
/// A promise was rejected without any handlers.
Reject,
/// A handler was added to a rejected promise for the first time.
Handle,
}
/// Functions used to resolve a pending promise.
///
/// This is equivalent to the parameters `resolveFunc` and `rejectFunc` of the executor passed to
/// the [`Promise()`] constructor.
///
/// Both functions are always associated with the promise from which they were created. This
/// means that by simply calling `resolve.call(this, &[values], context)` or
/// `reject.call(this, &[error], context)`, the state of the original promise will be updated with
/// the resolution value.
///
/// [`Promise()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise
#[derive(Debug, Clone, Finalize)]
pub struct ResolvingFunctions {
/// The `resolveFunc` parameter of the executor passed to `Promise()`.
pub resolve: JsFunction,
/// The `rejectFunc` parameter of the executor passed to `Promise()`.
pub reject: JsFunction,
}
// Manually implementing `Trace` to allow destructuring.
unsafe impl Trace for ResolvingFunctions {
custom_trace!(this, mark, {
mark(&this.resolve);
mark(&this.reject);
});
}
// ==================== Private API ====================
/// `IfAbruptRejectPromise ( value, capability )`
///
/// `IfAbruptRejectPromise` is a shorthand for a sequence of algorithm steps that use a `PromiseCapability` Record.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-ifabruptrejectpromise
macro_rules! if_abrupt_reject_promise {
($value:expr, $capability:expr, $context: expr) => {
match $value {
// 1. If value is an abrupt completion, then
Err(err) => {
let err = err.to_opaque($context);
// a. Perform ? Call(capability.[[Reject]], undefined, « value.[[Value]] »).
$capability
.reject()
.call(&JsValue::undefined(), &[err], $context)?;
// b. Return capability.[[Promise]].
return Ok($capability.promise().clone().into());
}
// 2. Else if value is a Completion Record, set value to value.[[Value]].
Ok(value) => value,
}
};
}
pub(crate) use if_abrupt_reject_promise;
/// The internal `PromiseCapability` data type.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-promisecapability-records
#[derive(Debug, Clone, Finalize)]
pub(crate) struct PromiseCapability {
/// The `[[Promise]]` field.
pub(crate) promise: JsObject,
/// The resolving functions,
pub(crate) functions: ResolvingFunctions,
}
// SAFETY: manually implementing `Trace` to allow destructuring.
unsafe impl Trace for PromiseCapability {
custom_trace!(this, mark, {
mark(&this.promise);
mark(&this.functions);
});
}
/// The internal `PromiseReaction` data type.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-promisereaction-records
#[derive(Debug, Trace, Finalize)]
pub(crate) struct ReactionRecord {
/// The `[[Capability]]` field.
promise_capability: Option<PromiseCapability>,
/// The `[[Type]]` field.
#[unsafe_ignore_trace]
reaction_type: ReactionType,
/// The `[[Handler]]` field.
handler: Option<JobCallback>,
}
/// The `[[Type]]` field values of a `PromiseReaction` record.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-promisereaction-records
#[derive(Debug, Clone, Copy)]
enum ReactionType {
Fulfill,
Reject,
}
impl PromiseCapability {
/// `NewPromiseCapability ( C )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-newpromisecapability
pub(crate) fn new(c: &JsObject, context: &mut Context) -> JsResult<Self> {
#[derive(Debug, Clone, Trace, Finalize)]
struct RejectResolve {
reject: JsValue,
resolve: JsValue,
}
// 1. If IsConstructor(C) is false, throw a TypeError exception.
if !c.is_constructor() {
return Err(JsNativeError::typ()
.with_message("PromiseCapability: expected constructor")
.into());
}
// 2. NOTE: C is assumed to be a constructor function that supports the parameter conventions of the Promise constructor (see 27.2.3.1).
// 3. Let promiseCapability be the PromiseCapability Record { [[Promise]]: undefined, [[Resolve]]: undefined, [[Reject]]: undefined }.
let promise_capability = Gc::new(GcRefCell::new(RejectResolve {
reject: JsValue::undefined(),
resolve: JsValue::undefined(),
}));
// 4. Let executorClosure be a new Abstract Closure with parameters (resolve, reject) that captures promiseCapability and performs the following steps when called:
// 5. Let executor be CreateBuiltinFunction(executorClosure, 2, "", « »).
let executor = FunctionObjectBuilder::new(
context.realm(),
NativeFunction::from_copy_closure_with_captures(
|_this, args: &[JsValue], captures, _| {
let mut promise_capability = captures.borrow_mut();
// a. If promiseCapability.[[Resolve]] is not undefined, throw a TypeError exception.
if !promise_capability.resolve.is_undefined() {
return Err(JsNativeError::typ()
.with_message("promiseCapability.[[Resolve]] is not undefined")
.into());
}
// b. If promiseCapability.[[Reject]] is not undefined, throw a TypeError exception.
if !promise_capability.reject.is_undefined() {
return Err(JsNativeError::typ()
.with_message("promiseCapability.[[Reject]] is not undefined")
.into());
}
let resolve = args.get_or_undefined(0);
let reject = args.get_or_undefined(1);
// c. Set promiseCapability.[[Resolve]] to resolve.
promise_capability.resolve = resolve.clone();
// d. Set promiseCapability.[[Reject]] to reject.
promise_capability.reject = reject.clone();
// e. Return undefined.
Ok(JsValue::Undefined)
},
promise_capability.clone(),
),
)
.name("")
.length(2)
.build()
.into();
// 6. Let promise be ? Construct(C, « executor »).
let promise = c.construct(&[executor], None, context)?;
let promise_capability = promise_capability.borrow();
let resolve = promise_capability.resolve.clone();
let reject = promise_capability.reject.clone();
// 7. If IsCallable(promiseCapability.[[Resolve]]) is false, throw a TypeError exception.
let resolve = resolve
.as_object()
.cloned()
.and_then(JsFunction::from_object)
.ok_or_else(|| {
JsNativeError::typ().with_message("promiseCapability.[[Resolve]] is not callable")
})?;
// 8. If IsCallable(promiseCapability.[[Reject]]) is false, throw a TypeError exception.
let reject = reject
.as_object()
.cloned()
.and_then(JsFunction::from_object)
.ok_or_else(|| {
JsNativeError::typ().with_message("promiseCapability.[[Reject]] is not callable")
})?;
// 9. Set promiseCapability.[[Promise]] to promise.
// 10. Return promiseCapability.
Ok(Self {
promise,
functions: ResolvingFunctions { resolve, reject },
})
}
/// Returns the promise object.
pub(crate) const fn promise(&self) -> &JsObject {
&self.promise
}
/// Returns the resolve function.
pub(crate) const fn resolve(&self) -> &JsFunction {
&self.functions.resolve
}
/// Returns the reject function.
pub(crate) const fn reject(&self) -> &JsFunction {
&self.functions.reject
}
}
impl IntrinsicObject for Promise {
fn init(realm: &Realm) {
let _timer = Profiler::global().start_event(std::any::type_name::<Self>(), "init");
let get_species = BuiltInBuilder::callable(realm, Self::get_species)
.name(js_string!("get [Symbol.species]"))
.build();
BuiltInBuilder::from_standard_constructor::<Self>(realm)
.static_method(Self::all, js_string!("all"), 1)
.static_method(Self::all_settled, js_string!("allSettled"), 1)
.static_method(Self::any, js_string!("any"), 1)
.static_method(Self::race, js_string!("race"), 1)
.static_method(Self::reject, js_string!("reject"), 1)
.static_method(Self::resolve, js_string!("resolve"), 1)
.static_method(Self::r#try, js_string!("try"), 1)
.static_method(Self::with_resolvers, js_string!("withResolvers"), 0)
.static_accessor(
JsSymbol::species(),
Some(get_species),
None,
Attribute::CONFIGURABLE,
)
.method(Self::then, js_string!("then"), 2)
.method(Self::catch, js_string!("catch"), 1)
.method(Self::finally, js_string!("finally"), 1)
// <https://tc39.es/ecma262/#sec-promise.prototype-@@tostringtag>
.property(
JsSymbol::to_string_tag(),
Self::NAME,
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.build();
}
fn get(intrinsics: &Intrinsics) -> JsObject {
Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor()
}
}
impl BuiltInObject for Promise {
const NAME: JsString = StaticJsStrings::PROMISE;
}
impl BuiltInConstructor for Promise {
const LENGTH: usize = 1;
const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor =
StandardConstructors::promise;
/// `Promise ( executor )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-promise-executor
fn constructor(
new_target: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. If NewTarget is undefined, throw a TypeError exception.
if new_target.is_undefined() {
return Err(JsNativeError::typ()
.with_message("Promise NewTarget cannot be undefined")
.into());
}
// 2. If IsCallable(executor) is false, throw a TypeError exception.
let executor = args
.get_or_undefined(0)
.as_callable()
.ok_or_else(|| JsNativeError::typ().with_message("Promise executor is not callable"))?;
// 3. Let promise be ? OrdinaryCreateFromConstructor(NewTarget, "%Promise.prototype%", « [[PromiseState]], [[PromiseResult]], [[PromiseFulfillReactions]], [[PromiseRejectReactions]], [[PromiseIsHandled]] »).
let promise =
get_prototype_from_constructor(new_target, StandardConstructors::promise, context)?;
let promise = JsObject::from_proto_and_data_with_shared_shape(
context.root_shape(),
promise,
// 4. Set promise.[[PromiseState]] to pending.
// 5. Set promise.[[PromiseFulfillReactions]] to a new empty List.
// 6. Set promise.[[PromiseRejectReactions]] to a new empty List.
// 7. Set promise.[[PromiseIsHandled]] to false.
Self::new(),
);
// 8. Let resolvingFunctions be CreateResolvingFunctions(promise).
let resolving_functions = Self::create_resolving_functions(&promise, context);
// 9. Let completion Completion(Call(executor, undefined, « resolvingFunctions.[[Resolve]], resolvingFunctions.[[Reject]] »)be ).
let completion = executor.call(
&JsValue::Undefined,
&[
resolving_functions.resolve.clone().into(),
resolving_functions.reject.clone().into(),
],
context,
);
// 10. If completion is an abrupt completion, then
if let Err(e) = completion {
let e = e.to_opaque(context);
// a. Perform ? Call(resolvingFunctions.[[Reject]], undefined, « completion.[[Value]] »).
resolving_functions
.reject
.call(&JsValue::Undefined, &[e], context)?;
}
// 11. Return promise.
promise.conv::<JsValue>().pipe(Ok)
}
}
impl Promise {
/// Creates a new, pending `Promise`.
pub(crate) fn new() -> Self {
Self {
state: PromiseState::Pending,
fulfill_reactions: Vec::default(),
reject_reactions: Vec::default(),
handled: false,
}
}
/// Gets the current state of the promise.
pub(crate) const fn state(&self) -> &PromiseState {
&self.state
}
/// [`Promise.try ( callbackfn, ...args )`][spec]
///
/// Calls the given function and returns a new promise that is resolved if the function
/// completes normally and rejected if it throws.
///
/// [spec]: https://tc39.es/proposal-promise-try/#sec-promise.try
pub(crate) fn r#try(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let callback = args.get_or_undefined(0);
let callback_args = args.get(1..).unwrap_or(&[]);
// 1. Let C be the this value.
// 2. If C is not an Object, throw a TypeError exception.
let c = this.as_object().ok_or_else(|| {
JsNativeError::typ().with_message("Promise.try() called on a non-object")
})?;
// 3. Let promiseCapability be ? NewPromiseCapability(C).
let promise_capability = PromiseCapability::new(c, context)?;
// 4. Let status be Completion(Call(callbackfn, undefined, args)).
let status = callback.call(&JsValue::undefined(), callback_args, context);
match status {
// 5. If status is an abrupt completion, then
Err(err) => {
let value = err.to_opaque(context);
// a. Perform ? Call(promiseCapability.[[Reject]], undefined, « status.[[Value]] »).
promise_capability.functions.reject.call(
&JsValue::undefined(),
&[value],
context,
)?;
}
// 6. Else,
Ok(value) => {
// a. Perform ? Call(promiseCapability.[[Resolve]], undefined, « status.[[Value]] »).
promise_capability.functions.resolve.call(
&JsValue::undefined(),
&[value],
context,
)?;
}
}
// 7. Return promiseCapability.[[Promise]].
Ok(promise_capability.promise.clone().into())
}
/// [`Promise.withResolvers ( )`][spec]
///
/// Creates a new promise that is pending, and returns that promise plus the resolve and reject
/// functions associated with it.
///
/// [spec]: https://tc39.es/ecma262/#sec-promise.withResolvers
pub(crate) fn with_resolvers(
this: &JsValue,
_args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. Let C be the this value.
use super::OrdinaryObject;
let c = this.as_object().ok_or_else(|| {
JsNativeError::typ().with_message("Promise.withResolvers() called on a non-object")
})?;
// 2. Let promiseCapability be ? NewPromiseCapability(C).
let PromiseCapability {
promise,
functions: ResolvingFunctions { resolve, reject },
} = PromiseCapability::new(c, context)?;
// 3. Let obj be OrdinaryObjectCreate(%Object.prototype%).
// 4. Perform ! CreateDataPropertyOrThrow(obj, "promise", promiseCapability.[[Promise]]).
// 5. Perform ! CreateDataPropertyOrThrow(obj, "resolve", promiseCapability.[[Resolve]]).
// 6. Perform ! CreateDataPropertyOrThrow(obj, "reject", promiseCapability.[[Reject]]).
let obj = context.intrinsics().templates().with_resolvers().create(
OrdinaryObject,
vec![promise.into(), resolve.into(), reject.into()],
);
// 7. Return obj.
Ok(obj.into())
}
/// `Promise.all ( iterable )`
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-promise.all
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all
pub(crate) fn all(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. Let C be the this value.
let c = this.as_object().ok_or_else(|| {
JsNativeError::typ().with_message("Promise.all() called on a non-object")
})?;
// 2. Let promiseCapability be ? NewPromiseCapability(C).
let promise_capability = PromiseCapability::new(c, context)?;
// 3. Let promiseResolve be Completion(GetPromiseResolve(C)).
let promise_resolve = Self::get_promise_resolve(c, context);
// 4. IfAbruptRejectPromise(promiseResolve, promiseCapability).
let promise_resolve =
if_abrupt_reject_promise!(promise_resolve, promise_capability, context);
// 5. Let iteratorRecord be Completion(GetIterator(iterable)).
let iterator_record = args.get_or_undefined(0).get_iterator(context, None, None);
// 6. IfAbruptRejectPromise(iteratorRecord, promiseCapability).
let mut iterator_record =
if_abrupt_reject_promise!(iterator_record, promise_capability, context);
// 7. Let result be Completion(PerformPromiseAll(iteratorRecord, C, promiseCapability, promiseResolve)).
let mut result = Self::perform_promise_all(
&mut iterator_record,
c,
&promise_capability,
&promise_resolve,
context,
)
.map(JsValue::from);
// 8. If result is an abrupt completion, then
if result.is_err() {
// a. If iteratorRecord.[[Done]] is false, set result to Completion(IteratorClose(iteratorRecord, result)).
if !iterator_record.done() {
result = iterator_record.close(result, context);
}
// b. IfAbruptRejectPromise(result, promiseCapability).
let result = if_abrupt_reject_promise!(result, promise_capability, context);
return Ok(result);
}
// 9. Return ? result.
result
}
/// `PerformPromiseAll ( iteratorRecord, constructor, resultCapability, promiseResolve )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-performpromiseall
pub(crate) fn perform_promise_all(
iterator_record: &mut IteratorRecord,
constructor: &JsObject,
result_capability: &PromiseCapability,
promise_resolve: &JsObject,
context: &mut Context,
) -> JsResult<JsObject> {
#[derive(Debug, Trace, Finalize)]
struct ResolveElementCaptures {
#[unsafe_ignore_trace]
already_called: Rc<Cell<bool>>,
index: usize,
values: Gc<GcRefCell<Vec<JsValue>>>,
capability_resolve: JsFunction,
#[unsafe_ignore_trace]
remaining_elements_count: Rc<Cell<i32>>,
}
// 1. Let values be a new empty List.
let values = Gc::new(GcRefCell::new(Vec::new()));
// 2. Let remainingElementsCount be the Record { [[Value]]: 1 }.
let remaining_elements_count = Rc::new(Cell::new(1));
// 3. Let index be 0.
let mut index = 0;
// 4. Repeat,
loop {
// a. Let next be Completion(IteratorStep(iteratorRecord)).
// b. If next is an abrupt completion, set iteratorRecord.[[Done]] to true.
// c. ReturnIfAbrupt(next).
let done = iterator_record.step(context)?;
// d. If next is false, then
// i. Set iteratorRecord.[[Done]] to true.
if done {
// ii. Set remainingElementsCount.[[Value]] to remainingElementsCount.[[Value]] - 1.
remaining_elements_count.set(remaining_elements_count.get() - 1);
// iii. If remainingElementsCount.[[Value]] is 0, then
if remaining_elements_count.get() == 0 {
// 1. Let valuesArray be CreateArrayFromList(values).
let values_array = crate::builtins::Array::create_array_from_list(
values.borrow().iter().cloned(),
context,
);
// 2. Perform ? Call(resultCapability.[[Resolve]], undefined, « valuesArray »).
result_capability.functions.resolve.call(
&JsValue::undefined(),
&[values_array.into()],
context,
)?;
}
// iv. Return resultCapability.[[Promise]].
return Ok(result_capability.promise.clone());
}
// e. Let nextValue be Completion(IteratorValue(next)).
// f. If nextValue is an abrupt completion, set iteratorRecord.[[Done]] to true.
// g. ReturnIfAbrupt(nextValue).
let next_value = iterator_record.value(context)?;
// h. Append undefined to values.
values.borrow_mut().push(JsValue::Undefined);
// i. Let nextPromise be ? Call(promiseResolve, constructor, « nextValue »).
let next_promise =
promise_resolve.call(&constructor.clone().into(), &[next_value], context)?;
// j. Let steps be the algorithm steps defined in Promise.all Resolve Element Functions.
// k. Let length be the number of non-optional parameters of the function definition in Promise.all Resolve Element Functions.
// l. Let onFulfilled be CreateBuiltinFunction(steps, length, "", « [[AlreadyCalled]], [[Index]], [[Values]], [[Capability]], [[RemainingElements]] »).
// m. Set onFulfilled.[[AlreadyCalled]] to false.
// n. Set onFulfilled.[[Index]] to index.
// o. Set onFulfilled.[[Values]] to values.
// p. Set onFulfilled.[[Capability]] to resultCapability.
// q. Set onFulfilled.[[RemainingElements]] to remainingElementsCount.
let on_fulfilled = FunctionObjectBuilder::new(
context.realm(),
NativeFunction::from_copy_closure_with_captures(
|_, args, captures, context| {
// https://tc39.es/ecma262/#sec-promise.all-resolve-element-functions
// 1. Let F be the active function object.
// 2. If F.[[AlreadyCalled]] is true, return undefined.
if captures.already_called.get() {
return Ok(JsValue::undefined());
}
// 3. Set F.[[AlreadyCalled]] to true.
captures.already_called.set(true);
// 4. Let index be F.[[Index]].
// 5. Let values be F.[[Values]].
// 6. Let promiseCapability be F.[[Capability]].
// 7. Let remainingElementsCount be F.[[RemainingElements]].
// 8. Set values[index] to x.
captures.values.borrow_mut()[captures.index] =
args.get_or_undefined(0).clone();
// 9. Set remainingElementsCount.[[Value]] to remainingElementsCount.[[Value]] - 1.
captures
.remaining_elements_count
.set(captures.remaining_elements_count.get() - 1);
// 10. If remainingElementsCount.[[Value]] is 0, then
if captures.remaining_elements_count.get() == 0 {
// a. Let valuesArray be CreateArrayFromList(values).
let values_array = crate::builtins::Array::create_array_from_list(
captures.values.borrow().as_slice().iter().cloned(),
context,
);
// b. Return ? Call(promiseCapability.[[Resolve]], undefined, « valuesArray »).
return captures.capability_resolve.call(
&JsValue::undefined(),
&[values_array.into()],
context,
);
}
// 11. Return undefined.
Ok(JsValue::undefined())
},
ResolveElementCaptures {
already_called: Rc::new(Cell::new(false)),
index,
values: values.clone(),
capability_resolve: result_capability.functions.resolve.clone(),
remaining_elements_count: remaining_elements_count.clone(),
},
),
)
.name("")
.length(1)
.constructor(false)
.build();
// r. Set remainingElementsCount.[[Value]] to remainingElementsCount.[[Value]] + 1.
remaining_elements_count.set(remaining_elements_count.get() + 1);
// s. Perform ? Invoke(nextPromise, "then", « onFulfilled, resultCapability.[[Reject]] »).
next_promise.invoke(
utf16!("then"),
&[
on_fulfilled.into(),
result_capability.functions.reject.clone().into(),
],
context,
)?;
// t. Set index to index + 1.
index += 1;
}
}
/// `Promise.allSettled ( iterable )`
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-promise.allsettled
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled
pub(crate) fn all_settled(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. Let C be the this value.
let c = this.as_object().ok_or_else(|| {
JsNativeError::typ().with_message("Promise.allSettled() called on a non-object")
})?;
// 2. Let promiseCapability be ? NewPromiseCapability(C).
let promise_capability = PromiseCapability::new(c, context)?;
// 3. Let promiseResolve be Completion(GetPromiseResolve(C)).
let promise_resolve = Self::get_promise_resolve(c, context);
// 4. IfAbruptRejectPromise(promiseResolve, promiseCapability).
let promise_resolve =
if_abrupt_reject_promise!(promise_resolve, promise_capability, context);
// 5. Let iteratorRecord be Completion(GetIterator(iterable)).
let iterator_record = args.get_or_undefined(0).get_iterator(context, None, None);
// 6. IfAbruptRejectPromise(iteratorRecord, promiseCapability).
let mut iterator_record =
if_abrupt_reject_promise!(iterator_record, promise_capability, context);
// 7. Let result be Completion(PerformPromiseAllSettled(iteratorRecord, C, promiseCapability, promiseResolve)).
let mut result = Self::perform_promise_all_settled(
&mut iterator_record,
c,
&promise_capability,
&promise_resolve,
context,
)
.map(JsValue::from);
// 8. If result is an abrupt completion, then
if result.is_err() {
// a. If iteratorRecord.[[Done]] is false, set result to Completion(IteratorClose(iteratorRecord, result)).
if !iterator_record.done() {
result = iterator_record.close(result, context);
}
// b. IfAbruptRejectPromise(result, promiseCapability).
let result = if_abrupt_reject_promise!(result, promise_capability, context);
return Ok(result);
}
// 9. Return ? result.
result
}
/// `PerformPromiseAllSettled ( iteratorRecord, constructor, resultCapability, promiseResolve )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-performpromiseallsettled
pub(crate) fn perform_promise_all_settled(
iterator_record: &mut IteratorRecord,
constructor: &JsObject,
result_capability: &PromiseCapability,
promise_resolve: &JsObject,
context: &mut Context,
) -> JsResult<JsObject> {
#[derive(Debug, Trace, Finalize)]
struct ResolveRejectElementCaptures {
#[unsafe_ignore_trace]
already_called: Rc<Cell<bool>>,
index: usize,
values: Gc<GcRefCell<Vec<JsValue>>>,
capability: JsFunction,
#[unsafe_ignore_trace]
remaining_elements: Rc<Cell<i32>>,
}
// 1. Let values be a new empty List.
let values = Gc::new(GcRefCell::new(Vec::new()));
// 2. Let remainingElementsCount be the Record { [[Value]]: 1 }.
let remaining_elements_count = Rc::new(Cell::new(1));
// 3. Let index be 0.
let mut index = 0;
// 4. Repeat,
loop {
// a. Let next be Completion(IteratorStep(iteratorRecord)).
// b. If next is an abrupt completion, set iteratorRecord.[[Done]] to true.
// c. ReturnIfAbrupt(next).
let done = iterator_record.step(context)?;
// d. If next is false, then
if done {
// i. Set iteratorRecord.[[Done]] to true.
// ii. Set remainingElementsCount.[[Value]] to remainingElementsCount.[[Value]] - 1.
remaining_elements_count.set(remaining_elements_count.get() - 1);
// iii. If remainingElementsCount.[[Value]] is 0, then
if remaining_elements_count.get() == 0 {
// 1. Let valuesArray be CreateArrayFromList(values).
let values_array = crate::builtins::Array::create_array_from_list(
values.borrow().as_slice().iter().cloned(),
context,
);
// 2. Perform ? Call(resultCapability.[[Resolve]], undefined, « valuesArray »).
result_capability.functions.resolve.call(
&JsValue::undefined(),
&[values_array.into()],
context,
)?;
}
// iv. Return resultCapability.[[Promise]].
return Ok(result_capability.promise.clone());
}
// e. Let nextValue be Completion(IteratorValue(next)).
// f. If nextValue is an abrupt completion, set iteratorRecord.[[Done]] to true.
// g. ReturnIfAbrupt(nextValue).
let next_value = iterator_record.value(context)?;
// h. Append undefined to values.
values.borrow_mut().push(JsValue::undefined());
// i. Let nextPromise be ? Call(promiseResolve, constructor, « nextValue »).
let next_promise =
promise_resolve.call(&constructor.clone().into(), &[next_value], context)?;
// j. Let stepsFulfilled be the algorithm steps defined in Promise.allSettled Resolve Element Functions.
// k. Let lengthFulfilled be the number of non-optional parameters of the function definition in Promise.allSettled Resolve Element Functions.
// l. Let onFulfilled be CreateBuiltinFunction(stepsFulfilled, lengthFulfilled, "", « [[AlreadyCalled]], [[Index]], [[Values]], [[Capability]], [[RemainingElements]] »).
// m. Let alreadyCalled be the Record { [[Value]]: false }.
// n. Set onFulfilled.[[AlreadyCalled]] to alreadyCalled.
// o. Set onFulfilled.[[Index]] to index.
// p. Set onFulfilled.[[Values]] to values.
// q. Set onFulfilled.[[Capability]] to resultCapability.
// r. Set onFulfilled.[[RemainingElements]] to remainingElementsCount.
let on_fulfilled = FunctionObjectBuilder::new(
context.realm(),
NativeFunction::from_copy_closure_with_captures(
|_, args, captures, context| {
// https://tc39.es/ecma262/#sec-promise.allsettled-resolve-element-functions
// 1. Let F be the active function object.
// 2. Let alreadyCalled be F.[[AlreadyCalled]].
// 3. If alreadyCalled.[[Value]] is true, return undefined.
if captures.already_called.get() {
return Ok(JsValue::undefined());
}
// 4. Set alreadyCalled.[[Value]] to true.
captures.already_called.set(true);
// 5. Let index be F.[[Index]].
// 6. Let values be F.[[Values]].
// 7. Let promiseCapability be F.[[Capability]].
// 8. Let remainingElementsCount be F.[[RemainingElements]].
// 9. Let obj be OrdinaryObjectCreate(%Object.prototype%).
let obj = JsObject::with_object_proto(context.intrinsics());
// 10. Perform ! CreateDataPropertyOrThrow(obj, "status", "fulfilled").
obj.create_data_property_or_throw(
utf16!("status"),
js_string!("fulfilled"),
context,
)
.expect("cannot fail per spec");
// 11. Perform ! CreateDataPropertyOrThrow(obj, "value", x).
obj.create_data_property_or_throw(
utf16!("value"),
args.get_or_undefined(0).clone(),
context,
)
.expect("cannot fail per spec");
// 12. Set values[index] to obj.
captures.values.borrow_mut()[captures.index] = obj.into();
// 13. Set remainingElementsCount.[[Value]] to remainingElementsCount.[[Value]] - 1.
captures
.remaining_elements
.set(captures.remaining_elements.get() - 1);
// 14. If remainingElementsCount.[[Value]] is 0, then
if captures.remaining_elements.get() == 0 {
// a. Let valuesArray be CreateArrayFromList(values).
let values_array = Array::create_array_from_list(
captures.values.borrow().as_slice().iter().cloned(),
context,
);
// b. Return ? Call(promiseCapability.[[Resolve]], undefined, « valuesArray »).
return captures.capability.call(
&JsValue::undefined(),
&[values_array.into()],
context,
);
}
// 15. Return undefined.
Ok(JsValue::undefined())
},
ResolveRejectElementCaptures {
already_called: Rc::new(Cell::new(false)),
index,
values: values.clone(),
capability: result_capability.functions.resolve.clone(),
remaining_elements: remaining_elements_count.clone(),
},
),
)
.name("")
.length(1)
.constructor(false)
.build();
// s. Let stepsRejected be the algorithm steps defined in Promise.allSettled Reject Element Functions.
// t. Let lengthRejected be the number of non-optional parameters of the function definition in Promise.allSettled Reject Element Functions.
// u. Let onRejected be CreateBuiltinFunction(stepsRejected, lengthRejected, "", « [[AlreadyCalled]], [[Index]], [[Values]], [[Capability]], [[RemainingElements]] »).
// v. Set onRejected.[[AlreadyCalled]] to alreadyCalled.
// w. Set onRejected.[[Index]] to index.
// x. Set onRejected.[[Values]] to values.
// y. Set onRejected.[[Capability]] to resultCapability.
// z. Set onRejected.[[RemainingElements]] to remainingElementsCount.
let on_rejected = FunctionObjectBuilder::new(
context.realm(),
NativeFunction::from_copy_closure_with_captures(
|_, args, captures, context| {
// https://tc39.es/ecma262/#sec-promise.allsettled-reject-element-functions
// 1. Let F be the active function object.
// 2. Let alreadyCalled be F.[[AlreadyCalled]].
// 3. If alreadyCalled.[[Value]] is true, return undefined.
if captures.already_called.get() {
return Ok(JsValue::undefined());
}
// 4. Set alreadyCalled.[[Value]] to true.
captures.already_called.set(true);
// 5. Let index be F.[[Index]].
// 6. Let values be F.[[Values]].
// 7. Let promiseCapability be F.[[Capability]].
// 8. Let remainingElementsCount be F.[[RemainingElements]].
// 9. Let obj be OrdinaryObjectCreate(%Object.prototype%).
let obj = JsObject::with_object_proto(context.intrinsics());
// 10. Perform ! CreateDataPropertyOrThrow(obj, "status", "rejected").
obj.create_data_property_or_throw(
utf16!("status"),
js_string!("rejected"),
context,
)
.expect("cannot fail per spec");
// 11. Perform ! CreateDataPropertyOrThrow(obj, "reason", x).
obj.create_data_property_or_throw(
utf16!("reason"),
args.get_or_undefined(0).clone(),
context,
)
.expect("cannot fail per spec");
// 12. Set values[index] to obj.
captures.values.borrow_mut()[captures.index] = obj.into();
// 13. Set remainingElementsCount.[[Value]] to remainingElementsCount.[[Value]] - 1.
captures
.remaining_elements
.set(captures.remaining_elements.get() - 1);
// 14. If remainingElementsCount.[[Value]] is 0, then
if captures.remaining_elements.get() == 0 {
// a. Let valuesArray be CreateArrayFromList(values).
let values_array = Array::create_array_from_list(
captures.values.borrow().as_slice().iter().cloned(),
context,
);
// b. Return ? Call(promiseCapability.[[Resolve]], undefined, « valuesArray »).
return captures.capability.call(
&JsValue::undefined(),
&[values_array.into()],
context,
);
}
// 15. Return undefined.
Ok(JsValue::undefined())
},
ResolveRejectElementCaptures {
already_called: Rc::new(Cell::new(false)),
index,
values: values.clone(),
capability: result_capability.functions.resolve.clone(),
remaining_elements: remaining_elements_count.clone(),
},
),
)
.name("")
.length(1)
.constructor(false)
.build();
// aa. Set remainingElementsCount.[[Value]] to remainingElementsCount.[[Value]] + 1.
remaining_elements_count.set(remaining_elements_count.get() + 1);
// ab. Perform ? Invoke(nextPromise, "then", « onFulfilled, onRejected »).
next_promise.invoke(
utf16!("then"),
&[on_fulfilled.into(), on_rejected.into()],
context,
)?;
// ac. Set index to index + 1.
index += 1;
}
}
/// `Promise.any ( iterable )`
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-promise.any
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/any
pub(crate) fn any(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. Let C be the this value.
let c = this.as_object().ok_or_else(|| {
JsNativeError::typ().with_message("Promise.any() called on a non-object")
})?;
// 2. Let promiseCapability be ? NewPromiseCapability(C).
let promise_capability = PromiseCapability::new(c, context)?;
// 3. Let promiseResolve be Completion(GetPromiseResolve(C)).
let promise_resolve = Self::get_promise_resolve(c, context);
// 4. IfAbruptRejectPromise(promiseResolve, promiseCapability).
let promise_resolve =
if_abrupt_reject_promise!(promise_resolve, promise_capability, context);
// 5. Let iteratorRecord be Completion(GetIterator(iterable)).
let iterator_record = args.get_or_undefined(0).get_iterator(context, None, None);
// 6. IfAbruptRejectPromise(iteratorRecord, promiseCapability).
let mut iterator_record =
if_abrupt_reject_promise!(iterator_record, promise_capability, context);
// 7. Let result be Completion(PerformPromiseAny(iteratorRecord, C, promiseCapability, promiseResolve)).
let mut result = Self::perform_promise_any(
&mut iterator_record,
c,
&promise_capability,
&promise_resolve,
context,
)
.map(JsValue::from);
// 8. If result is an abrupt completion, then
if result.is_err() {
// a. If iteratorRecord.[[Done]] is false, set result to Completion(IteratorClose(iteratorRecord, result)).
if !iterator_record.done() {
result = iterator_record.close(result, context);
}
// b. IfAbruptRejectPromise(result, promiseCapability).
let result = if_abrupt_reject_promise!(result, promise_capability, context);
return Ok(result);
}
// 9. Return ? result.
result
}
/// `PerformPromiseAny ( iteratorRecord, constructor, resultCapability, promiseResolve )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-performpromiseany
pub(crate) fn perform_promise_any(
iterator_record: &mut IteratorRecord,
constructor: &JsObject,
result_capability: &PromiseCapability,
promise_resolve: &JsObject,
context: &mut Context,
) -> JsResult<JsObject> {
#[derive(Debug, Trace, Finalize)]
struct RejectElementCaptures {
#[unsafe_ignore_trace]
already_called: Rc<Cell<bool>>,
index: usize,
errors: Gc<GcRefCell<Vec<JsValue>>>,
capability_reject: JsFunction,
#[unsafe_ignore_trace]
remaining_elements_count: Rc<Cell<i32>>,
}
// 1. Let errors be a new empty List.
let errors = Gc::new(GcRefCell::new(Vec::new()));
// 2. Let remainingElementsCount be the Record { [[Value]]: 1 }.
let remaining_elements_count = Rc::new(Cell::new(1));
// 3. Let index be 0.
let mut index = 0;
// 4. Repeat,
loop {
// a. Let next be Completion(IteratorStep(iteratorRecord)).
// b. If next is an abrupt completion, set iteratorRecord.[[Done]] to true.
// c. ReturnIfAbrupt(next).
let done = iterator_record.step(context)?;
// d. If next is false, then
if done {
// i. Set iteratorRecord.[[Done]] to true.
// ii. Set remainingElementsCount.[[Value]] to remainingElementsCount.[[Value]] - 1.
remaining_elements_count.set(remaining_elements_count.get() - 1);
// iii. If remainingElementsCount.[[Value]] is 0, then
if remaining_elements_count.get() == 0 {
// 1. Let error be a newly created AggregateError object.
// 2. Perform ! DefinePropertyOrThrow(error, "errors", PropertyDescriptor { [[Configurable]]: true, [[Enumerable]]: false, [[Writable]]: true, [[Value]]: CreateArrayFromList(errors) }).
let error = JsNativeError::aggregate(
errors
.borrow()
.iter()
.cloned()
.map(JsError::from_opaque)
.collect(),
)
.with_message("no promise in Promise.any was fulfilled.");
// 3. Return ThrowCompletion(error).
return Err(error.into());
}
// iv. Return resultCapability.[[Promise]].
return Ok(result_capability.promise.clone());
}
// e. Let nextValue be Completion(IteratorValue(next)).
// f. If nextValue is an abrupt completion, set iteratorRecord.[[Done]] to true.
// g. ReturnIfAbrupt(nextValue).
let next_value = iterator_record.value(context)?;
// h. Append undefined to errors.
errors.borrow_mut().push(JsValue::undefined());
// i. Let nextPromise be ? Call(promiseResolve, constructor, « nextValue »).
let next_promise =
promise_resolve.call(&constructor.clone().into(), &[next_value], context)?;
// j. Let stepsRejected be the algorithm steps defined in Promise.any Reject Element Functions.
// k. Let lengthRejected be the number of non-optional parameters of the function definition in Promise.any Reject Element Functions.
// l. Let onRejected be CreateBuiltinFunction(stepsRejected, lengthRejected, "", « [[AlreadyCalled]], [[Index]], [[Errors]], [[Capability]], [[RemainingElements]] »).
// m. Set onRejected.[[AlreadyCalled]] to false.
// n. Set onRejected.[[Index]] to index.
// o. Set onRejected.[[Errors]] to errors.
// p. Set onRejected.[[Capability]] to resultCapability.
// q. Set onRejected.[[RemainingElements]] to remainingElementsCount.
let on_rejected = FunctionObjectBuilder::new(
context.realm(),
NativeFunction::from_copy_closure_with_captures(
|_, args, captures, context| {
// https://tc39.es/ecma262/#sec-promise.any-reject-element-functions
// 1. Let F be the active function object.
// 2. If F.[[AlreadyCalled]] is true, return undefined.
if captures.already_called.get() {
return Ok(JsValue::undefined());
}
// 3. Set F.[[AlreadyCalled]] to true.
captures.already_called.set(true);
// 4. Let index be F.[[Index]].
// 5. Let errors be F.[[Errors]].
// 6. Let promiseCapability be F.[[Capability]].
// 7. Let remainingElementsCount be F.[[RemainingElements]].
// 8. Set errors[index] to x.
captures.errors.borrow_mut()[captures.index] =
args.get_or_undefined(0).clone();
// 9. Set remainingElementsCount.[[Value]] to remainingElementsCount.[[Value]] - 1.
captures
.remaining_elements_count
.set(captures.remaining_elements_count.get() - 1);
// 10. If remainingElementsCount.[[Value]] is 0, then
if captures.remaining_elements_count.get() == 0 {
// a. Let error be a newly created AggregateError object.
// b. Perform ! DefinePropertyOrThrow(error, "errors", PropertyDescriptor { [[Configurable]]: true, [[Enumerable]]: false, [[Writable]]: true, [[Value]]: CreateArrayFromList(errors) }).
let error = JsNativeError::aggregate(
captures
.errors
.borrow()
.iter()
.cloned()
.map(JsError::from_opaque)
.collect(),
)
.with_message("no promise in Promise.any was fulfilled.");
// c. Return ? Call(promiseCapability.[[Reject]], undefined, « error »).
return captures.capability_reject.call(
&JsValue::undefined(),
&[error.to_opaque(context).into()],
context,
);
}
// 11. Return undefined.
Ok(JsValue::undefined())
},
RejectElementCaptures {
already_called: Rc::new(Cell::new(false)),
index,
errors: errors.clone(),
capability_reject: result_capability.functions.reject.clone(),
remaining_elements_count: remaining_elements_count.clone(),
},
),
)
.name("")
.length(1)
.constructor(false)
.build();
// r. Set remainingElementsCount.[[Value]] to remainingElementsCount.[[Value]] + 1.
remaining_elements_count.set(remaining_elements_count.get() + 1);
// s. Perform ? Invoke(nextPromise, "then", « resultCapability.[[Resolve]], onRejected »).
next_promise.invoke(
utf16!("then"),
&[
result_capability.functions.resolve.clone().into(),
on_rejected.into(),
],
context,
)?;
// t. Set index to index + 1.
index += 1;
}
}
/// `Promise.race ( iterable )`
///
/// The `race` function returns a new promise which is settled in the same way as the first
/// passed promise to settle. It resolves all elements of the passed `iterable` to promises.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-promise.race
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race
pub(crate) fn race(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let iterable = args.get_or_undefined(0);
// 1. Let C be the this value.
let c = this.as_object().ok_or_else(|| {
JsNativeError::typ().with_message("Promise.race() called on a non-object")
})?;
// 2. Let promiseCapability be ? NewPromiseCapability(C).
let promise_capability = PromiseCapability::new(c, context)?;
// 3. Let promiseResolve be Completion(GetPromiseResolve(C)).
let promise_resolve = Self::get_promise_resolve(c, context);
// 4. IfAbruptRejectPromise(promiseResolve, promiseCapability).
let promise_resolve =
if_abrupt_reject_promise!(promise_resolve, promise_capability, context);
// 5. Let iteratorRecord be Completion(GetIterator(iterable)).
let iterator_record = iterable.get_iterator(context, None, None);
// 6. IfAbruptRejectPromise(iteratorRecord, promiseCapability).
let mut iterator_record =
if_abrupt_reject_promise!(iterator_record, promise_capability, context);
// 7. Let result be Completion(PerformPromiseRace(iteratorRecord, C, promiseCapability, promiseResolve)).
let mut result = Self::perform_promise_race(
&mut iterator_record,
c,
&promise_capability,
&promise_resolve,
context,
)
.map(JsValue::from);
// 8. If result is an abrupt completion, then
if result.is_err() {
// a. If iteratorRecord.[[Done]] is false, set result to Completion(IteratorClose(iteratorRecord, result)).
if !iterator_record.done() {
result = iterator_record.close(result, context);
}
// b. IfAbruptRejectPromise(result, promiseCapability).
let result = if_abrupt_reject_promise!(result, promise_capability, context);
Ok(result)
} else {
// 9. Return ? result.
result
}
}
/// `PerformPromiseRace ( iteratorRecord, constructor, resultCapability, promiseResolve )`
///
/// The abstract operation `PerformPromiseRace` takes arguments `iteratorRecord`, `constructor`
/// (a constructor), `resultCapability` (a [`PromiseCapability`] Record), and `promiseResolve`
/// (a function object) and returns either a normal completion containing an ECMAScript
/// language value or a throw completion.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-performpromiserace
pub(crate) fn perform_promise_race(
iterator_record: &mut IteratorRecord,
constructor: &JsObject,
result_capability: &PromiseCapability,
promise_resolve: &JsObject,
context: &mut Context,
) -> JsResult<JsObject> {
let constructor = constructor.clone().into();
// 1. Repeat,
loop {
// a. Let next be Completion(IteratorStep(iteratorRecord)).
// b. If next is an abrupt completion, set iteratorRecord.[[Done]] to true.
// c. ReturnIfAbrupt(next).
let done = iterator_record.step(context)?;
if done {
// d. If next is false, then
// i. Set iteratorRecord.[[Done]] to true.
// ii. Return resultCapability.[[Promise]].
return Ok(result_capability.promise.clone());
}
// e. Let nextValue be Completion(IteratorValue(next)).
// f. If nextValue is an abrupt completion, set iteratorRecord.[[Done]] to true.
// g. ReturnIfAbrupt(nextValue).
let next_value = iterator_record.value(context)?;
// h. Let nextPromise be ? Call(promiseResolve, constructor, « nextValue »).
let next_promise = promise_resolve.call(&constructor, &[next_value], context)?;
// i. Perform ? Invoke(nextPromise, "then", « resultCapability.[[Resolve]], resultCapability.[[Reject]] »).
next_promise.invoke(
utf16!("then"),
&[
result_capability.functions.resolve.clone().into(),
result_capability.functions.reject.clone().into(),
],
context,
)?;
}
}
/// `Promise.reject ( r )`
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-promise.reject
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/reject
pub(crate) fn reject(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let r = args.get_or_undefined(0).clone();
// 1. Let C be the this value.
let c = this.as_object().ok_or_else(|| {
JsNativeError::typ().with_message("Promise.reject() called on a non-object")
})?;
Self::promise_reject(c, &JsError::from_opaque(r), context).map(JsValue::from)
}
/// Utility function to create a rejected promise.
pub(crate) fn promise_reject(
c: &JsObject,
e: &JsError,
context: &mut Context,
) -> JsResult<JsObject> {
let e = e.to_opaque(context);
// 2. Let promiseCapability be ? NewPromiseCapability(C).
let promise_capability = PromiseCapability::new(c, context)?;
// 3. Perform ? Call(promiseCapability.[[Reject]], undefined, « r »).
promise_capability
.functions
.reject
.call(&JsValue::undefined(), &[e], context)?;
// 4. Return promiseCapability.[[Promise]].
Ok(promise_capability.promise.clone())
}
/// `Promise.resolve ( x )`
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-promise.resolve
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve
pub(crate) fn resolve(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let x = args.get_or_undefined(0);
// 1. Let C be the this value.
// 2. If Type(C) is not Object, throw a TypeError exception.
let c = this.as_object().ok_or_else(|| {
JsNativeError::typ().with_message("Promise.resolve() called on a non-object")
})?;
// 3. Return ? PromiseResolve(C, x).
Self::promise_resolve(c, x.clone(), context).map(JsValue::from)
}
/// `PromiseResolve ( C, x )`
///
/// The abstract operation `PromiseResolve` takes arguments `C` (a constructor) and `x` (an
/// ECMAScript language value) and returns either a normal completion containing an ECMAScript
/// language value or a throw completion. It returns a new promise resolved with `x`.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-promise-resolve
pub(crate) fn promise_resolve(
c: &JsObject,
x: JsValue,
context: &mut Context,
) -> JsResult<JsObject> {
// 1. If IsPromise(x) is true, then
if let Some(x) = x.as_promise() {
// a. Let xConstructor be ? Get(x, "constructor").
let x_constructor = x.get(CONSTRUCTOR, context)?;
// b. If SameValue(xConstructor, C) is true, return x.
if x_constructor
.as_object()
.map_or(false, |o| JsObject::equals(o, c))
{
return Ok(x.clone());
}
}
// 2. Let promiseCapability be ? NewPromiseCapability(C).
let promise_capability = PromiseCapability::new(&c.clone(), context)?;
// 3. Perform ? Call(promiseCapability.[[Resolve]], undefined, « x »).
promise_capability
.functions
.resolve
.call(&JsValue::Undefined, &[x], context)?;
// 4. Return promiseCapability.[[Promise]].
Ok(promise_capability.promise.clone())
}
/// `get Promise [ @@species ]`
///
/// The `Promise [ @@species ]` accessor property returns the Promise constructor.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-get-promise-@@species
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/@@species
#[allow(clippy::unnecessary_wraps)]
fn get_species(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
// 1. Return the this value.
Ok(this.clone())
}
/// `Promise.prototype.catch ( onRejected )`
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-promise.prototype.catch
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch
pub(crate) fn catch(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let on_rejected = args.get_or_undefined(0);
// 1. Let promise be the this value.
let promise = this;
// 2. Return ? Invoke(promise, "then", « undefined, onRejected »).
promise.invoke(
utf16!("then"),
&[JsValue::undefined(), on_rejected.clone()],
context,
)
}
/// `Promise.prototype.finally ( onFinally )`
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-promise.prototype.finally
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/finally
pub(crate) fn finally(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. Let promise be the this value.
let promise = this;
// 2. If Type(promise) is not Object, throw a TypeError exception.
let Some(promise) = promise.as_object() else {
return Err(JsNativeError::typ()
.with_message("finally called with a non-object promise")
.into());
};
// 3. Let C be ? SpeciesConstructor(promise, %Promise%).
let c = promise.species_constructor(StandardConstructors::promise, context)?;
// 4. Assert: IsConstructor(C) is true.
debug_assert!(c.is_constructor());
let on_finally = args.get_or_undefined(0);
let Some(on_finally) = on_finally
.as_object()
.cloned()
.and_then(JsFunction::from_object)
else {
// 5. If IsCallable(onFinally) is false, then
// a. Let thenFinally be onFinally.
// b. Let catchFinally be onFinally.
// 7. Return ? Invoke(promise, "then", « thenFinally, catchFinally »).
let then = promise.get(utf16!("then"), context)?;
return then.call(this, &[on_finally.clone(), on_finally.clone()], context);
};
let (then_finally, catch_finally) =
Self::then_catch_finally_closures(c, on_finally, context);
// 7. Return ? Invoke(promise, "then", « thenFinally, catchFinally »).
let then = promise.get(utf16!("then"), context)?;
then.call(this, &[then_finally.into(), catch_finally.into()], context)
}
pub(crate) fn then_catch_finally_closures(
c: JsObject,
on_finally: JsFunction,
context: &mut Context,
) -> (JsFunction, JsFunction) {
/// Capture object for the `thenFinallyClosure` abstract closure.
#[derive(Debug, Trace, Finalize)]
struct FinallyCaptures {
on_finally: JsFunction,
c: JsObject,
}
// a. Let thenFinallyClosure be a new Abstract Closure with parameters (value) that captures onFinally and C and performs the following steps when called:
let then_finally_closure = FunctionObjectBuilder::new(
context.realm(),
NativeFunction::from_copy_closure_with_captures(
|_this, args, captures, context| {
/// Capture object for the abstract `returnValue` closure.
#[derive(Debug, Trace, Finalize)]
struct ReturnValueCaptures {
value: JsValue,
}
let value = args.get_or_undefined(0);
// i. Let result be ? Call(onFinally, undefined).
let result = captures
.on_finally
.call(&JsValue::undefined(), &[], context)?;
// ii. Let promise be ? PromiseResolve(C, result).
let promise = Self::promise_resolve(&captures.c, result, context)?;
// iii. Let returnValue be a new Abstract Closure with no parameters that captures value and performs the following steps when called:
let return_value = FunctionObjectBuilder::new(
context.realm(),
NativeFunction::from_copy_closure_with_captures(
|_this, _args, captures, _context| {
// 1. Return value.
Ok(captures.value.clone())
},
ReturnValueCaptures {
value: value.clone(),
},
),
);
// iv. Let valueThunk be CreateBuiltinFunction(returnValue, 0, "", « »).
let value_thunk = return_value.length(0).name("").build();
// v. Return ? Invoke(promise, "then", « valueThunk »).
promise.invoke(utf16!("then"), &[value_thunk.into()], context)
},
FinallyCaptures {
on_finally: on_finally.clone(),
c: c.clone(),
},
),
);
// b. Let thenFinally be CreateBuiltinFunction(thenFinallyClosure, 1, "", « »).
let then_finally = then_finally_closure.length(1).name("").build();
// c. Let catchFinallyClosure be a new Abstract Closure with parameters (reason) that captures onFinally and C and performs the following steps when called:
let catch_finally_closure = FunctionObjectBuilder::new(
context.realm(),
NativeFunction::from_copy_closure_with_captures(
|_this, args, captures, context| {
/// Capture object for the abstract `throwReason` closure.
#[derive(Debug, Trace, Finalize)]
struct ThrowReasonCaptures {
reason: JsValue,
}
let reason = args.get_or_undefined(0);
// i. Let result be ? Call(onFinally, undefined).
let result = captures
.on_finally
.call(&JsValue::undefined(), &[], context)?;
// ii. Let promise be ? PromiseResolve(C, result).
let promise = Self::promise_resolve(&captures.c, result, context)?;
// iii. Let throwReason be a new Abstract Closure with no parameters that captures reason and performs the following steps when called:
let throw_reason = FunctionObjectBuilder::new(
context.realm(),
NativeFunction::from_copy_closure_with_captures(
|_this, _args, captures, _context| {
// 1. Return ThrowCompletion(reason).
Err(JsError::from_opaque(captures.reason.clone()))
},
ThrowReasonCaptures {
reason: reason.clone(),
},
),
);
// iv. Let thrower be CreateBuiltinFunction(throwReason, 0, "", « »).
let thrower = throw_reason.length(0).name("").build();
// v. Return ? Invoke(promise, "then", « thrower »).
promise.invoke(utf16!("then"), &[thrower.into()], context)
},
FinallyCaptures { on_finally, c },
),
);
// d. Let catchFinally be CreateBuiltinFunction(catchFinallyClosure, 1, "", « »).
let catch_finally = catch_finally_closure.length(1).name("").build();
(then_finally, catch_finally)
}
/// `Promise.prototype.then ( onFulfilled, onRejected )`
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-promise.prototype.then
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then
pub(crate) fn then(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. Let promise be the this value.
let promise = this;
// 2. If IsPromise(promise) is false, throw a TypeError exception.
let promise = promise.as_promise().ok_or_else(|| {
JsNativeError::typ().with_message("Promise.prototype.then: this is not a promise")
})?;
let on_fulfilled = args
.get_or_undefined(0)
.as_object()
.cloned()
.and_then(JsFunction::from_object);
let on_rejected = args
.get_or_undefined(1)
.as_object()
.cloned()
.and_then(JsFunction::from_object);
// continues in `Promise::inner_then`
Self::inner_then(promise, on_fulfilled, on_rejected, context).map(JsValue::from)
}
/// Schedules callback functions for the eventual completion of `promise` — either fulfillment
/// or rejection.
pub(crate) fn inner_then(
promise: &JsObject,
on_fulfilled: Option<JsFunction>,
on_rejected: Option<JsFunction>,
context: &mut Context,
) -> JsResult<JsObject> {
// 3. Let C be ? SpeciesConstructor(promise, %Promise%).
let c = promise.species_constructor(StandardConstructors::promise, context)?;
// 4. Let resultCapability be ? NewPromiseCapability(C).
let result_capability = PromiseCapability::new(&c, context)?;
let result_promise = result_capability.promise.clone();
// 5. Return PerformPromiseThen(promise, onFulfilled, onRejected, resultCapability).
Self::perform_promise_then(
promise,
on_fulfilled,
on_rejected,
Some(result_capability),
context,
);
Ok(result_promise)
}
/// `PerformPromiseThen ( promise, onFulfilled, onRejected [ , resultCapability ] )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-performpromisethen
pub(crate) fn perform_promise_then(
promise: &JsObject,
on_fulfilled: Option<JsFunction>,
on_rejected: Option<JsFunction>,
result_capability: Option<PromiseCapability>,
context: &mut Context,
) {
// 1. Assert: IsPromise(promise) is true.
// 2. If resultCapability is not present, then
// a. Set resultCapability to undefined.
// 3. If IsCallable(onFulfilled) is false, then
// a. Let onFulfilledJobCallback be empty.
// Argument already asserts this.
let on_fulfilled_job_callback = on_fulfilled
// 4. Else,
// a. Let onFulfilledJobCallback be HostMakeJobCallback(onFulfilled).
.map(|f| context.host_hooks().make_job_callback(f, context));
// 5. If IsCallable(onRejected) is false, then
// a. Let onRejectedJobCallback be empty.
// Argument already asserts this.
let on_rejected_job_callback = on_rejected
// 6. Else,
// a. Let onRejectedJobCallback be HostMakeJobCallback(onRejected).
.map(|f| context.host_hooks().make_job_callback(f, context));
// 7. Let fulfillReaction be the PromiseReaction { [[Capability]]: resultCapability, [[Type]]: Fulfill, [[Handler]]: onFulfilledJobCallback }.
let fulfill_reaction = ReactionRecord {
promise_capability: result_capability.clone(),
reaction_type: ReactionType::Fulfill,
handler: on_fulfilled_job_callback,
};
// 8. Let rejectReaction be the PromiseReaction { [[Capability]]: resultCapability, [[Type]]: Reject, [[Handler]]: onRejectedJobCallback }.
let reject_reaction = ReactionRecord {
promise_capability: result_capability,
reaction_type: ReactionType::Reject,
handler: on_rejected_job_callback,
};
let (state, handled) = {
let promise = promise
.downcast_ref::<Self>()
.expect("IsPromise(promise) is false");
(promise.state.clone(), promise.handled)
};
match state {
// 9. If promise.[[PromiseState]] is pending, then
PromiseState::Pending => {
let mut promise = promise
.downcast_mut::<Self>()
.expect("IsPromise(promise) is false");
// a. Append fulfillReaction as the last element of the List that is promise.[[PromiseFulfillReactions]].
promise.fulfill_reactions.push(fulfill_reaction);
// b. Append rejectReaction as the last element of the List that is promise.[[PromiseRejectReactions]].
promise.reject_reactions.push(reject_reaction);
}
// 10. Else if promise.[[PromiseState]] is fulfilled, then
// a. Let value be promise.[[PromiseResult]].
PromiseState::Fulfilled(ref value) => {
// b. Let fulfillJob be NewPromiseReactionJob(fulfillReaction, value).
let fulfill_job =
new_promise_reaction_job(fulfill_reaction, value.clone(), context);
// c. Perform HostEnqueuePromiseJob(fulfillJob.[[Job]], fulfillJob.[[Realm]]).
context
.job_queue()
.enqueue_promise_job(fulfill_job, context);
}
// 11. Else,
// a. Assert: The value of promise.[[PromiseState]] is rejected.
// b. Let reason be promise.[[PromiseResult]].
PromiseState::Rejected(ref reason) => {
// c. If promise.[[PromiseIsHandled]] is false, perform HostPromiseRejectionTracker(promise, "handle").
if !handled {
context.host_hooks().promise_rejection_tracker(
promise,
OperationType::Handle,
context,
);
}
// d. Let rejectJob be NewPromiseReactionJob(rejectReaction, reason).
let reject_job = new_promise_reaction_job(reject_reaction, reason.clone(), context);
// e. Perform HostEnqueuePromiseJob(rejectJob.[[Job]], rejectJob.[[Realm]]).
context.job_queue().enqueue_promise_job(reject_job, context);
// 12. Set promise.[[PromiseIsHandled]] to true.
promise
.downcast_mut::<Self>()
.expect("IsPromise(promise) is false")
.handled = true;
}
}
// 13. If resultCapability is undefined, then
// a. Return undefined.
// 14. Else,
// a. Return resultCapability.[[Promise]].
// skipped because we can already access the promise from `result_capability`
}
/// `GetPromiseResolve ( promiseConstructor )`
///
/// The abstract operation `GetPromiseResolve` takes argument `promiseConstructor` (a
/// constructor) and returns either a normal completion containing a function object or a throw
/// completion.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-getpromiseresolve
pub(crate) fn get_promise_resolve(
promise_constructor: &JsObject,
context: &mut Context,
) -> JsResult<JsObject> {
// 1. Let promiseResolve be ? Get(promiseConstructor, "resolve").
let promise_resolve = promise_constructor.get(utf16!("resolve"), context)?;
// 2. If IsCallable(promiseResolve) is false, throw a TypeError exception.
promise_resolve.as_callable().cloned().ok_or_else(|| {
JsNativeError::typ()
.with_message("retrieving a non-callable promise resolver")
.into()
})
}
/// `CreateResolvingFunctions ( promise )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-createresolvingfunctions
pub(crate) fn create_resolving_functions(
promise: &JsObject,
context: &mut Context,
) -> ResolvingFunctions {
/// `TriggerPromiseReactions ( reactions, argument )`
///
/// The abstract operation `TriggerPromiseReactions` takes arguments `reactions` (a `List` of
/// `PromiseReaction` Records) and `argument` and returns unused. It enqueues a new `Job` for
/// each record in `reactions`. Each such `Job` processes the `[[Type]]` and `[[Handler]]` of
/// the `PromiseReaction` Record, and if the `[[Handler]]` is not `empty`, calls it passing the
/// given argument. If the `[[Handler]]` is `empty`, the behaviour is determined by the
/// `[[Type]]`.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-triggerpromisereactions
fn trigger_promise_reactions(
reactions: Vec<ReactionRecord>,
argument: &JsValue,
context: &mut Context,
) {
// 1. For each element reaction of reactions, do
for reaction in reactions {
// a. Let job be NewPromiseReactionJob(reaction, argument).
let job = new_promise_reaction_job(reaction, argument.clone(), context);
// b. Perform HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]).
context.job_queue().enqueue_promise_job(job, context);
}
// 2. Return unused.
}
/// `FulfillPromise ( promise, value )`
///
/// The abstract operation `FulfillPromise` takes arguments `promise` and `value` and returns
/// `unused`.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-fulfillpromise
///
/// # Panics
///
/// Panics if `Promise` is not pending.
fn fulfill_promise(promise: &JsObject, value: JsValue, context: &mut Context) {
let mut promise = promise
.downcast_mut::<Promise>()
.expect("IsPromise(promise) is false");
// 1. Assert: The value of promise.[[PromiseState]] is pending.
assert!(
matches!(promise.state, PromiseState::Pending),
"promise was not pending"
);
// reordering these statements does not affect the semantics
// 2. Let reactions be promise.[[PromiseFulfillReactions]].
// 4. Set promise.[[PromiseFulfillReactions]] to undefined.
let reactions = std::mem::take(&mut promise.fulfill_reactions);
// 5. Set promise.[[PromiseRejectReactions]] to undefined.
promise.reject_reactions.clear();
// 7. Perform TriggerPromiseReactions(reactions, value).
trigger_promise_reactions(reactions, &value, context);
// 3. Set promise.[[PromiseResult]] to value.
// 6. Set promise.[[PromiseState]] to fulfilled.
promise.state = PromiseState::Fulfilled(value);
// 8. Return unused.
}
/// `RejectPromise ( promise, reason )`
///
/// The abstract operation `RejectPromise` takes arguments `promise` and `reason` and returns
/// `unused`.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-rejectpromise
///
/// # Panics
///
/// Panics if `Promise` is not pending.
fn reject_promise(promise: &JsObject, reason: JsValue, context: &mut Context) {
let handled = {
let mut promise = promise
.downcast_mut::<Promise>()
.expect("IsPromise(promise) is false");
// 1. Assert: The value of promise.[[PromiseState]] is pending.
assert!(
matches!(promise.state, PromiseState::Pending),
"Expected promise.[[PromiseState]] to be pending"
);
// reordering these statements does not affect the semantics
// 2. Let reactions be promise.[[PromiseRejectReactions]].
// 5. Set promise.[[PromiseRejectReactions]] to undefined.
let reactions = std::mem::take(&mut promise.reject_reactions);
// 4. Set promise.[[PromiseFulfillReactions]] to undefined.
promise.fulfill_reactions.clear();
// 8. Perform TriggerPromiseReactions(reactions, reason).
trigger_promise_reactions(reactions, &reason, context);
// 3. Set promise.[[PromiseResult]] to reason.
// 6. Set promise.[[PromiseState]] to rejected.
promise.state = PromiseState::Rejected(reason);
promise.handled
};
// 7. If promise.[[PromiseIsHandled]] is false, perform HostPromiseRejectionTracker(promise, "reject").
if !handled {
context.host_hooks().promise_rejection_tracker(
promise,
OperationType::Reject,
context,
);
}
// 9. Return unused.
}
// 1. Let alreadyResolved be the Record { [[Value]]: false }.
// 5. Set resolve.[[Promise]] to promise.
// 6. Set resolve.[[AlreadyResolved]] to alreadyResolved.
let promise = Gc::new(Cell::new(Some(promise.clone())));
// 2. Let stepsResolve be the algorithm steps defined in Promise Resolve Functions.
// 3. Let lengthResolve be the number of non-optional parameters of the function definition in Promise Resolve Functions.
// 4. Let resolve be CreateBuiltinFunction(stepsResolve, lengthResolve, "", « [[Promise]], [[AlreadyResolved]] »).
let resolve = FunctionObjectBuilder::new(
context.realm(),
NativeFunction::from_copy_closure_with_captures(
|_this, args, captures, context| {
// https://tc39.es/ecma262/#sec-promise-resolve-functions
// 1. Let F be the active function object.
// 2. Assert: F has a [[Promise]] internal slot whose value is an Object.
// 3. Let promise be F.[[Promise]].
// 4. Let alreadyResolved be F.[[AlreadyResolved]].
// 5. If alreadyResolved.[[Value]] is true, return undefined.
// 6. Set alreadyResolved.[[Value]] to true.
let Some(promise) = captures.take() else {
return Ok(JsValue::undefined());
};
let resolution = args.get_or_undefined(0);
// 7. If SameValue(resolution, promise) is true, then
if JsValue::same_value(resolution, &promise.clone().into()) {
// a. Let selfResolutionError be a newly created TypeError object.
let self_resolution_error = JsNativeError::typ()
.with_message("SameValue(resolution, promise) is true")
.to_opaque(context);
// b. Perform RejectPromise(promise, selfResolutionError).
reject_promise(&promise, self_resolution_error.into(), context);
// c. Return undefined.
return Ok(JsValue::Undefined);
}
let Some(then) = resolution.as_object() else {
// 8. If Type(resolution) is not Object, then
// a. Perform FulfillPromise(promise, resolution).
fulfill_promise(&promise, resolution.clone(), context);
// b. Return undefined.
return Ok(JsValue::Undefined);
};
// 9. Let then be Completion(Get(resolution, "then")).
let then_action = match then.get(utf16!("then"), context) {
// 10. If then is an abrupt completion, then
Err(e) => {
// a. Perform RejectPromise(promise, then.[[Value]]).
reject_promise(&promise, e.to_opaque(context), context);
// b. Return undefined.
return Ok(JsValue::Undefined);
}
// 11. Let thenAction be then.[[Value]].
Ok(then) => then,
};
// 12. If IsCallable(thenAction) is false, then
let Some(then_action) = then_action
.as_object()
.cloned()
.and_then(JsFunction::from_object)
else {
// a. Perform FulfillPromise(promise, resolution).
fulfill_promise(&promise, resolution.clone(), context);
// b. Return undefined.
return Ok(JsValue::Undefined);
};
// 13. Let thenJobCallback be HostMakeJobCallback(thenAction).
let then_job_callback =
context.host_hooks().make_job_callback(then_action, context);
// 14. Let job be NewPromiseResolveThenableJob(promise, resolution, thenJobCallback).
let job = new_promise_resolve_thenable_job(
promise.clone(),
resolution.clone(),
then_job_callback,
context,
);
// 15. Perform HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]).
context.job_queue().enqueue_promise_job(job, context);
// 16. Return undefined.
Ok(JsValue::Undefined)
},
promise.clone(),
),
)
.name("")
.length(1)
.constructor(false)
.build();
// 10. Set reject.[[Promise]] to promise.
// 11. Set reject.[[AlreadyResolved]] to alreadyResolved.
// 7. Let stepsReject be the algorithm steps defined in Promise Reject Functions.
// 8. Let lengthReject be the number of non-optional parameters of the function definition in Promise Reject Functions.
// 9. Let reject be CreateBuiltinFunction(stepsReject, lengthReject, "", « [[Promise]], [[AlreadyResolved]] »).
let reject = FunctionObjectBuilder::new(
context.realm(),
NativeFunction::from_copy_closure_with_captures(
|_this, args, captures, context| {
// https://tc39.es/ecma262/#sec-promise-reject-functions
// 1. Let F be the active function object.
// 2. Assert: F has a [[Promise]] internal slot whose value is an Object.
// 3. Let promise be F.[[Promise]].
// 4. Let alreadyResolved be F.[[AlreadyResolved]].
// 5. If alreadyResolved.[[Value]] is true, return undefined.
// 6. Set alreadyResolved.[[Value]] to true.
let Some(promise) = captures.take() else {
return Ok(JsValue::undefined());
};
// 7. Perform RejectPromise(promise, reason).
reject_promise(&promise, args.get_or_undefined(0).clone(), context);
// 8. Return undefined.
Ok(JsValue::Undefined)
},
promise,
),
)
.name("")
.length(1)
.constructor(false)
.build();
// 12. Return the Record { [[Resolve]]: resolve, [[Reject]]: reject }.
ResolvingFunctions { resolve, reject }
}
}
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-newpromisereactionjob
fn new_promise_reaction_job(
mut reaction: ReactionRecord,
argument: JsValue,
context: &mut Context,
) -> NativeJob {
// Inverting order since `job` captures `reaction` by value.
// 2. Let handlerRealm be null.
// 3. If reaction.[[Handler]] is not empty, then
// a. Let getHandlerRealmResult be Completion(GetFunctionRealm(reaction.[[Handler]].[[Callback]])).
// b. If getHandlerRealmResult is a normal completion, set handlerRealm to getHandlerRealmResult.[[Value]].
// c. Else, set handlerRealm to the current Realm Record.
// d. NOTE: handlerRealm is never null unless the handler is undefined. When the handler is a
// revoked Proxy and no ECMAScript code runs, handlerRealm is used to create error objects.
let realm = reaction
.handler
.as_ref()
.and_then(|handler| handler.callback().get_function_realm(context).ok())
.unwrap_or_else(|| context.realm().clone());
// 1. Let job be a new Job Abstract Closure with no parameters that captures reaction and argument and performs the following steps when called:
let job = move |context: &mut Context| {
// a. Let promiseCapability be reaction.[[Capability]].
let promise_capability = reaction.promise_capability.take();
// b. Let type be reaction.[[Type]].
let reaction_type = reaction.reaction_type;
// c. Let handler be reaction.[[Handler]].
let handler = reaction.handler.take();
let handler_result = match handler {
// d. If handler is empty, then
None => match reaction_type {
// i. If type is Fulfill, let handlerResult be NormalCompletion(argument).
ReactionType::Fulfill => Ok(argument.clone()),
// ii. Else,
// 1. Assert: type is Reject.
ReactionType::Reject => {
// 2. Let handlerResult be ThrowCompletion(argument).
Err(argument.clone())
}
},
// e. Else, let handlerResult be Completion(HostCallJobCallback(handler, undefined, « argument »)).
Some(handler) => context
.host_hooks()
.call_job_callback(handler, &JsValue::Undefined, &[argument.clone()], context)
.map_err(|e| e.to_opaque(context)),
};
match promise_capability {
None => {
// f. If promiseCapability is undefined, then
// i. Assert: handlerResult is not an abrupt completion.
assert!(
handler_result.is_ok(),
"Assertion: <handlerResult is not an abrupt completion> failed"
);
// ii. Return empty.
Ok(JsValue::Undefined)
}
Some(promise_capability_record) => {
// g. Assert: promiseCapability is a PromiseCapability Record.
let PromiseCapability {
promise: _,
functions: ResolvingFunctions { resolve, reject },
} = &promise_capability_record;
match handler_result {
// h. If handlerResult is an abrupt completion, then
Err(value) => {
// i. Return ? Call(promiseCapability.[[Reject]], undefined, « handlerResult.[[Value]] »).
reject.call(&JsValue::Undefined, &[value], context)
}
// i. Else,
Ok(value) => {
// i. Return ? Call(promiseCapability.[[Resolve]], undefined, « handlerResult.[[Value]] »).
resolve.call(&JsValue::Undefined, &[value], context)
}
}
}
}
};
// 4. Return the Record { [[Job]]: job, [[Realm]]: handlerRealm }.
NativeJob::with_realm(job, realm, context)
}
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-newpromiseresolvethenablejob
fn new_promise_resolve_thenable_job(
promise_to_resolve: JsObject,
thenable: JsValue,
then: JobCallback,
context: &mut Context,
) -> NativeJob {
// Inverting order since `job` captures variables by value.
// 2. Let getThenRealmResult be Completion(GetFunctionRealm(then.[[Callback]])).
// 3. If getThenRealmResult is a normal completion, let thenRealm be getThenRealmResult.[[Value]].
// 4. Else, let thenRealm be the current Realm Record.
// 5. NOTE: thenRealm is never null. When then.[[Callback]] is a revoked Proxy and no code runs, thenRealm is used to create error objects.
let realm = then
.callback()
.get_function_realm(context)
.unwrap_or_else(|_| context.realm().clone());
// 1. Let job be a new Job Abstract Closure with no parameters that captures promiseToResolve, thenable, and then and performs the following steps when called:
let job = move |context: &mut Context| {
// a. Let resolvingFunctions be CreateResolvingFunctions(promiseToResolve).
let resolving_functions = Promise::create_resolving_functions(&promise_to_resolve, context);
// b. Let thenCallResult be Completion(HostCallJobCallback(then, thenable, « resolvingFunctions.[[Resolve]], resolvingFunctions.[[Reject]] »)).
let then_call_result = context.host_hooks().call_job_callback(
then,
&thenable,
&[
resolving_functions.resolve.clone().into(),
resolving_functions.reject.clone().into(),
],
context,
);
// c. If thenCallResult is an abrupt completion, then
if let Err(value) = then_call_result {
let value = value.to_opaque(context);
// i. Return ? Call(resolvingFunctions.[[Reject]], undefined, « thenCallResult.[[Value]] »).
return resolving_functions
.reject
.call(&JsValue::Undefined, &[value], context);
}
// d. Return ? thenCallResult.
then_call_result
};
// 6. Return the Record { [[Job]]: job, [[Realm]]: thenRealm }.
NativeJob::with_realm(job, realm, context)
}