use std::cell::{Cell, RefCell};
use std::collections::VecDeque;
use std::rc::Rc;
use crate::builtins::error::JsError;
use crate::error::StatorError;
use crate::interpreter::{dispatch_call_with_this, dispatch_get_property_value};
use crate::objects::property_map::PropertyMap;
use crate::objects::value::JsValue;
type MicrotaskQueueInner = Rc<RefCell<VecDeque<Box<dyn FnOnce()>>>>;
pub type PromiseHandler = Box<dyn Fn(JsValue) -> Result<JsValue, JsValue>>;
pub type PromiseFinallyHandler = Box<dyn Fn() -> Result<JsValue, JsValue>>;
thread_local! {
static ACTIVE_MTQ: RefCell<Option<MicrotaskQueue>> = const { RefCell::new(None) };
static NEXT_PROMISE_ID: Cell<usize> = const { Cell::new(1) };
static ACTIVE_REJECTION_EVENTS: RefCell<VecDeque<PromiseRejectionEvent>> =
const { RefCell::new(VecDeque::new()) };
}
pub fn install_active_microtask_queue(q: &MicrotaskQueue) {
ACTIVE_MTQ.with(|cell| *cell.borrow_mut() = Some(q.clone()));
}
pub fn drain_active_microtask_queue() -> usize {
ACTIVE_MTQ.with(|cell| {
if let Some(q) = cell.borrow().as_ref() {
let mut count = 0usize;
loop {
let task = q.0.borrow_mut().pop_front();
match task {
Some(t) => {
t();
count += 1;
}
None => break,
}
}
count
} else {
0
}
})
}
pub fn enqueue_active_microtask(task: Box<dyn FnOnce()>) -> bool {
ACTIVE_MTQ.with(|cell| {
if let Some(q) = cell.borrow().as_ref() {
q.enqueue(task);
true
} else {
false
}
})
}
pub fn clear_active_microtask_queue() {
ACTIVE_MTQ.with(|cell| *cell.borrow_mut() = None);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PromiseRejectionEventKind {
RejectedWithNoHandler,
HandlerAddedAfterReject,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PromiseRejectionEvent {
pub kind: PromiseRejectionEventKind,
pub promise_id: usize,
pub reason: String,
}
pub fn drain_active_promise_rejection_events() -> Vec<PromiseRejectionEvent> {
ACTIVE_REJECTION_EVENTS.with(|cell| cell.borrow_mut().drain(..).collect())
}
pub fn active_promise_rejection_event_count() -> usize {
ACTIVE_REJECTION_EVENTS.with(|cell| cell.borrow().len())
}
pub fn clear_active_promise_rejection_events() {
ACTIVE_REJECTION_EVENTS.with(|cell| cell.borrow_mut().clear());
}
fn next_promise_id() -> usize {
NEXT_PROMISE_ID.with(|cell| {
let id = cell.get();
cell.set(id.saturating_add(1).max(1));
id
})
}
fn enqueue_rejection_event(kind: PromiseRejectionEventKind, promise_id: usize, reason: &JsValue) {
ACTIVE_REJECTION_EVENTS.with(|cell| {
cell.borrow_mut().push_back(PromiseRejectionEvent {
kind,
promise_id,
reason: reason.to_display_string(),
});
});
}
fn cancel_queued_rejected_with_no_handler(promise_id: usize) -> bool {
ACTIVE_REJECTION_EVENTS.with(|cell| {
let mut events = cell.borrow_mut();
if let Some(index) = events.iter().position(|event| {
event.promise_id == promise_id
&& event.kind == PromiseRejectionEventKind::RejectedWithNoHandler
}) {
events.remove(index);
return true;
}
false
})
}
#[derive(Clone)]
pub struct MicrotaskQueue(MicrotaskQueueInner);
impl Default for MicrotaskQueue {
fn default() -> Self {
Self::new()
}
}
impl MicrotaskQueue {
pub fn new() -> Self {
Self(Rc::new(RefCell::new(VecDeque::new())))
}
pub fn enqueue(&self, task: Box<dyn FnOnce()>) {
self.0.borrow_mut().push_back(task);
}
pub fn drain(&self) {
loop {
let task = self.0.borrow_mut().pop_front();
match task {
Some(t) => t(),
None => break,
}
}
}
pub fn is_empty(&self) -> bool {
self.0.borrow().is_empty()
}
pub fn len(&self) -> usize {
self.0.borrow().len()
}
}
enum PromiseStateInner {
Pending {
fulfill_reactions: Vec<Box<dyn FnOnce(JsValue)>>,
reject_reactions: Vec<Box<dyn FnOnce(JsValue)>>,
},
Fulfilled(JsValue),
Rejected(JsValue),
}
struct PromiseInner {
promise_id: usize,
state: PromiseStateInner,
is_handled: bool,
reported_unhandled: bool,
prototype: Option<JsValue>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum PromiseState {
Pending,
Fulfilled(JsValue),
Rejected(JsValue),
}
#[derive(Clone)]
pub struct JsPromise(Rc<RefCell<PromiseInner>>);
impl std::fmt::Debug for JsPromise {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "JsPromise({:?})", self.state())
}
}
impl PartialEq for JsPromise {
fn eq(&self, other: &Self) -> bool {
Rc::ptr_eq(&self.0, &other.0)
}
}
impl JsPromise {
fn new_pending() -> Self {
Self(Rc::new(RefCell::new(PromiseInner {
promise_id: next_promise_id(),
state: PromiseStateInner::Pending {
fulfill_reactions: Vec::new(),
reject_reactions: Vec::new(),
},
is_handled: false,
reported_unhandled: false,
prototype: None,
})))
}
pub fn state(&self) -> PromiseState {
match &self.0.borrow().state {
PromiseStateInner::Pending { .. } => PromiseState::Pending,
PromiseStateInner::Fulfilled(v) => PromiseState::Fulfilled(v.clone()),
PromiseStateInner::Rejected(r) => PromiseState::Rejected(r.clone()),
}
}
pub fn is_pending(&self) -> bool {
matches!(self.0.borrow().state, PromiseStateInner::Pending { .. })
}
pub fn is_fulfilled(&self) -> bool {
matches!(self.0.borrow().state, PromiseStateInner::Fulfilled(_))
}
pub fn is_rejected(&self) -> bool {
matches!(self.0.borrow().state, PromiseStateInner::Rejected(_))
}
pub fn is_handled(&self) -> bool {
self.0.borrow().is_handled
}
pub fn promise_id(&self) -> usize {
self.0.borrow().promise_id
}
pub(crate) fn prototype(&self) -> Option<JsValue> {
self.0.borrow().prototype.clone()
}
pub(crate) fn set_prototype(&self, prototype: Option<JsValue>) {
self.0.borrow_mut().prototype = prototype;
}
pub fn value(&self) -> Option<JsValue> {
match &self.0.borrow().state {
PromiseStateInner::Fulfilled(v) => Some(v.clone()),
_ => None,
}
}
pub fn reason(&self) -> Option<JsValue> {
match &self.0.borrow().state {
PromiseStateInner::Rejected(r) => Some(r.clone()),
_ => None,
}
}
fn resolve(&self, value: JsValue, queue: &MicrotaskQueue) {
if let JsValue::Promise(other) = &value {
if Rc::ptr_eq(&self.0, &other.0) {
self.reject(
JsValue::String("TypeError: promise cannot resolve itself".into()),
queue,
);
return;
}
let fulfill_self = self.clone();
let reject_self = self.clone();
let fulfill_queue = queue.clone();
let reject_queue = queue.clone();
other.add_reactions(
Box::new(move |resolved| fulfill_self.resolve(resolved, &fulfill_queue)),
Box::new(move |reason| reject_self.reject(reason, &reject_queue)),
queue,
);
return;
}
let then_fn = match get_thenable_method(&value) {
Ok(then_fn) => then_fn,
Err(reason) => {
self.reject(reason, queue);
return;
}
};
if let Some(then_fn) = then_fn {
let target = self.clone();
let then_queue = queue.clone();
let then_value = value.clone();
queue.enqueue(Box::new(move || {
let already_called = Rc::new(Cell::new(false));
let resolve_self = target.clone();
let resolve_queue = then_queue.clone();
let resolve_called = Rc::clone(&already_called);
let resolve_fn = JsValue::NativeFunction(Rc::new(move |args| {
if resolve_called.replace(true) {
return Ok(JsValue::Undefined);
}
let resolved = args.first().cloned().unwrap_or(JsValue::Undefined);
resolve_self.resolve(resolved, &resolve_queue);
Ok(JsValue::Undefined)
}));
let reject_self = target.clone();
let reject_queue = then_queue.clone();
let reject_called = Rc::clone(&already_called);
let reject_fn = JsValue::NativeFunction(Rc::new(move |args| {
if reject_called.replace(true) {
return Ok(JsValue::Undefined);
}
let reason = args.first().cloned().unwrap_or(JsValue::Undefined);
reject_self.reject(reason, &reject_queue);
Ok(JsValue::Undefined)
}));
match dispatch_call_with_this(
&then_fn,
then_value.clone(),
vec![resolve_fn, reject_fn],
) {
Ok(_) => {}
Err(err) if !already_called.replace(true) => {
target.reject(rejection_reason_from_error(&err), &then_queue);
}
Err(_) => {}
}
}));
return;
}
let reactions = {
let mut inner = self.0.borrow_mut();
if !matches!(inner.state, PromiseStateInner::Pending { .. }) {
return;
}
let old = std::mem::replace(
&mut inner.state,
PromiseStateInner::Fulfilled(value.clone()),
);
match old {
PromiseStateInner::Pending {
fulfill_reactions, ..
} => fulfill_reactions,
_ => unreachable!(),
}
};
for reaction in reactions {
let val = value.clone();
queue.enqueue(Box::new(move || reaction(val)));
}
}
fn reject(&self, reason: JsValue, queue: &MicrotaskQueue) {
let (promise_id, should_report_unhandled, reactions) = {
let mut inner = self.0.borrow_mut();
if !matches!(inner.state, PromiseStateInner::Pending { .. }) {
return;
}
let promise_id = inner.promise_id;
let should_report_unhandled = !inner.is_handled;
if should_report_unhandled {
inner.reported_unhandled = true;
}
let old = std::mem::replace(
&mut inner.state,
PromiseStateInner::Rejected(reason.clone()),
);
let reactions = match old {
PromiseStateInner::Pending {
reject_reactions, ..
} => reject_reactions,
_ => unreachable!(),
};
(promise_id, should_report_unhandled, reactions)
};
if should_report_unhandled {
enqueue_rejection_event(
PromiseRejectionEventKind::RejectedWithNoHandler,
promise_id,
&reason,
);
}
for reaction in reactions {
let r = reason.clone();
queue.enqueue(Box::new(move || reaction(r)));
}
}
fn add_reactions(
&self,
fulfill: Box<dyn FnOnce(JsValue)>,
reject: Box<dyn FnOnce(JsValue)>,
queue: &MicrotaskQueue,
) {
enum Settled {
Fulfilled(JsValue),
Rejected(JsValue),
}
let mut handler_added_after_reject = None;
let settled = {
let mut inner = self.0.borrow_mut();
inner.is_handled = true;
if let PromiseStateInner::Pending {
fulfill_reactions,
reject_reactions,
} = &mut inner.state
{
fulfill_reactions.push(fulfill);
reject_reactions.push(reject);
return;
}
let promise_id = inner.promise_id;
let reported_unhandled = inner.reported_unhandled;
let settled = match &inner.state {
PromiseStateInner::Pending {
fulfill_reactions: _,
reject_reactions: _,
} => unreachable!(),
PromiseStateInner::Fulfilled(v) => Settled::Fulfilled(v.clone()),
PromiseStateInner::Rejected(r) => {
if reported_unhandled {
handler_added_after_reject = Some((promise_id, r.clone()));
}
Settled::Rejected(r.clone())
}
};
if reported_unhandled {
inner.reported_unhandled = false;
}
settled
};
if let Some((promise_id, reason)) = handler_added_after_reject
&& !cancel_queued_rejected_with_no_handler(promise_id)
{
enqueue_rejection_event(
PromiseRejectionEventKind::HandlerAddedAfterReject,
promise_id,
&reason,
);
}
match settled {
Settled::Fulfilled(v) => queue.enqueue(Box::new(move || fulfill(v))),
Settled::Rejected(r) => queue.enqueue(Box::new(move || reject(r))),
}
}
}
fn is_thenable_callable(value: &JsValue) -> bool {
matches!(
value,
JsValue::Function(_) | JsValue::NativeFunction(_) | JsValue::Proxy(_)
) || matches!(value, JsValue::PlainObject(map) if map.borrow().contains_key("__call__"))
}
fn get_thenable_method(value: &JsValue) -> Result<Option<JsValue>, JsValue> {
if !matches!(
value,
JsValue::PlainObject(_)
| JsValue::Error(_)
| JsValue::Function(_)
| JsValue::NativeFunction(_)
| JsValue::Proxy(_)
) {
return Ok(None);
}
match dispatch_get_property_value(value, JsValue::String("then".into())) {
Ok(then_fn) if is_thenable_callable(&then_fn) => Ok(Some(then_fn)),
Ok(_) => Ok(None),
Err(error) => Err(rejection_reason_from_error(&error)),
}
}
fn rejection_reason_from_error(error: &StatorError) -> JsValue {
JsValue::String(error.to_string().into())
}
pub fn promise_new<F>(executor: F, queue: &MicrotaskQueue) -> JsPromise
where
F: FnOnce(Box<dyn FnOnce(JsValue)>, Box<dyn FnOnce(JsValue)>),
{
let p = JsPromise::new_pending();
let p1 = p.clone();
let q1 = queue.clone();
let resolve: Box<dyn FnOnce(JsValue)> = Box::new(move |v| p1.resolve(v, &q1));
let p2 = p.clone();
let q2 = queue.clone();
let reject: Box<dyn FnOnce(JsValue)> = Box::new(move |r| p2.reject(r, &q2));
executor(resolve, reject);
p
}
pub fn promise_try<F>(callable: F, args: Vec<JsValue>, queue: &MicrotaskQueue) -> JsPromise
where
F: FnOnce(Vec<JsValue>) -> Result<JsValue, JsValue>,
{
match callable(args) {
Ok(value) => promise_resolve(value, queue),
Err(reason) => promise_reject(reason, queue),
}
}
pub fn promise_resolve(value: JsValue, queue: &MicrotaskQueue) -> JsPromise {
if let JsValue::Promise(promise) = value {
return promise;
}
let promise = JsPromise::new_pending();
promise.resolve(value, queue);
promise
}
pub fn promise_reject(reason: JsValue, queue: &MicrotaskQueue) -> JsPromise {
promise_new(|_resolve, reject| reject(reason), queue)
}
pub(crate) fn promise_reject_with_result(
reason: JsValue,
p_result: JsPromise,
queue: &MicrotaskQueue,
) -> JsPromise {
p_result.reject(reason, queue);
p_result
}
pub(crate) fn promise_pending() -> JsPromise {
JsPromise::new_pending()
}
pub fn promise_then(
promise: &JsPromise,
on_fulfilled: Option<PromiseHandler>,
on_rejected: Option<PromiseHandler>,
queue: &MicrotaskQueue,
) -> JsPromise {
promise_then_with_result(
promise,
on_fulfilled,
on_rejected,
JsPromise::new_pending(),
queue,
)
}
pub(crate) fn promise_then_with_result(
promise: &JsPromise,
on_fulfilled: Option<PromiseHandler>,
on_rejected: Option<PromiseHandler>,
p2: JsPromise,
queue: &MicrotaskQueue,
) -> JsPromise {
let p2a = p2.clone();
let qa = queue.clone();
let fulfill_reaction: Box<dyn FnOnce(JsValue)> = Box::new(move |v| {
let result = if let Some(h) = &on_fulfilled {
h(v)
} else {
Ok(v)
};
match result {
Ok(val) => p2a.resolve(val, &qa),
Err(reason) => p2a.reject(reason, &qa),
}
});
let p2b = p2.clone();
let qb = queue.clone();
let reject_reaction: Box<dyn FnOnce(JsValue)> = Box::new(move |r| {
let result = if let Some(h) = &on_rejected {
h(r)
} else {
Err(r)
};
match result {
Ok(val) => p2b.resolve(val, &qb),
Err(reason) => p2b.reject(reason, &qb),
}
});
promise.add_reactions(fulfill_reaction, reject_reaction, queue);
p2
}
pub fn promise_catch(
promise: &JsPromise,
on_rejected: PromiseHandler,
queue: &MicrotaskQueue,
) -> JsPromise {
promise_catch_with_result(promise, on_rejected, JsPromise::new_pending(), queue)
}
pub(crate) fn promise_catch_with_result(
promise: &JsPromise,
on_rejected: PromiseHandler,
result_promise: JsPromise,
queue: &MicrotaskQueue,
) -> JsPromise {
promise_then_with_result(promise, None, Some(on_rejected), result_promise, queue)
}
pub fn promise_finally(
promise: &JsPromise,
on_finally: PromiseFinallyHandler,
queue: &MicrotaskQueue,
) -> JsPromise {
promise_finally_with_result(promise, on_finally, JsPromise::new_pending(), queue)
}
pub(crate) fn promise_finally_with_result(
promise: &JsPromise,
on_finally: PromiseFinallyHandler,
result_promise: JsPromise,
queue: &MicrotaskQueue,
) -> JsPromise {
let on_finally = Rc::new(on_finally);
let p2_fulfill = result_promise.clone();
let queue_fulfill = queue.clone();
let on_finally_fulfill = Rc::clone(&on_finally);
let fulfill_reaction: Box<dyn FnOnce(JsValue)> = Box::new(move |value| {
let original = Ok(value);
match on_finally_fulfill() {
Ok(finalizer_value) => {
settle_promise_finally(finalizer_value, original, &p2_fulfill, &queue_fulfill)
}
Err(reason) => p2_fulfill.reject(reason, &queue_fulfill),
}
});
let p2_reject = result_promise.clone();
let queue_reject = queue.clone();
let on_finally_reject = Rc::clone(&on_finally);
let reject_reaction: Box<dyn FnOnce(JsValue)> = Box::new(move |reason| {
let original = Err(reason);
match on_finally_reject() {
Ok(finalizer_value) => {
settle_promise_finally(finalizer_value, original, &p2_reject, &queue_reject)
}
Err(finalizer_reason) => p2_reject.reject(finalizer_reason, &queue_reject),
}
});
promise.add_reactions(fulfill_reaction, reject_reaction, queue);
result_promise
}
fn settle_promise_finally(
finalizer_value: JsValue,
original: Result<JsValue, JsValue>,
result_promise: &JsPromise,
queue: &MicrotaskQueue,
) {
if let JsValue::Promise(cleanup_promise) = &finalizer_value
&& cleanup_promise == result_promise
{
result_promise.reject(
JsValue::String("TypeError: promise cannot resolve itself".into()),
queue,
);
return;
}
let cleanup_promise = promise_resolve(finalizer_value, queue);
let original_for_cleanup = original.clone();
let cleanup_target = result_promise.clone();
let cleanup_queue = queue.clone();
let cleanup_reaction: Box<dyn FnOnce(JsValue)> =
Box::new(move |_| match &original_for_cleanup {
Ok(value) => cleanup_target.resolve(value.clone(), &cleanup_queue),
Err(reason) => cleanup_target.reject(reason.clone(), &cleanup_queue),
});
let reject_target = result_promise.clone();
let reject_queue = queue.clone();
let reject_reaction: Box<dyn FnOnce(JsValue)> =
Box::new(move |reason| reject_target.reject(reason, &reject_queue));
cleanup_promise.add_reactions(cleanup_reaction, reject_reaction, queue);
}
pub(crate) fn promise_all_with_result(
promises: Vec<JsPromise>,
p_result: JsPromise,
queue: &MicrotaskQueue,
) -> JsPromise {
let count = promises.len();
if count == 0 {
p_result.resolve(JsValue::new_array(Vec::new()), queue);
return p_result;
}
let results: Rc<RefCell<Vec<Option<JsValue>>>> = Rc::new(RefCell::new(vec![None; count]));
let remaining: Rc<RefCell<usize>> = Rc::new(RefCell::new(count));
for (i, p) in promises.into_iter().enumerate() {
let results_f = Rc::clone(&results);
let remaining_f = Rc::clone(&remaining);
let p_result_f = p_result.clone();
let q_f = queue.clone();
let p_result_r = p_result.clone();
let q_r = queue.clone();
let on_fulfilled = Some(Box::new(move |v: JsValue| {
results_f.borrow_mut()[i] = Some(v);
let mut rem = remaining_f.borrow_mut();
*rem -= 1;
if *rem == 0 {
let all: Vec<JsValue> = results_f
.borrow()
.iter()
.map(|r| r.clone().expect("slot filled by preceding reaction"))
.collect();
p_result_f.resolve(JsValue::new_array(all), &q_f);
}
Ok(JsValue::Undefined)
}) as PromiseHandler);
let on_rejected = Some(Box::new(move |r: JsValue| {
p_result_r.reject(r.clone(), &q_r);
Err(r)
}) as PromiseHandler);
promise_then(&p, on_fulfilled, on_rejected, queue);
}
p_result
}
pub fn promise_all(promises: Vec<JsPromise>, queue: &MicrotaskQueue) -> JsPromise {
promise_all_with_result(promises, JsPromise::new_pending(), queue)
}
pub(crate) fn promise_all_settled_with_result(
promises: Vec<JsPromise>,
p_result: JsPromise,
queue: &MicrotaskQueue,
) -> JsPromise {
let count = promises.len();
if count == 0 {
p_result.resolve(JsValue::new_array(Vec::new()), queue);
return p_result;
}
let results: Rc<RefCell<Vec<Option<JsValue>>>> = Rc::new(RefCell::new(vec![None; count]));
let remaining: Rc<RefCell<usize>> = Rc::new(RefCell::new(count));
for (i, p) in promises.into_iter().enumerate() {
let results_f = Rc::clone(&results);
let remaining_f = Rc::clone(&remaining);
let p_result_f = p_result.clone();
let q_f = queue.clone();
let results_r = Rc::clone(&results);
let remaining_r = Rc::clone(&remaining);
let p_result_r = p_result.clone();
let q_r = queue.clone();
let on_fulfilled = Some(Box::new(move |v: JsValue| {
let mut obj = PropertyMap::new();
obj.insert("status".into(), JsValue::String("fulfilled".into()));
obj.insert("value".into(), v);
results_f.borrow_mut()[i] = Some(JsValue::PlainObject(Rc::new(RefCell::new(obj))));
settle_all_settled(&results_f, &remaining_f, &p_result_f, &q_f);
Ok(JsValue::Undefined)
}) as PromiseHandler);
let on_rejected = Some(Box::new(move |r: JsValue| {
let mut obj = PropertyMap::new();
obj.insert("status".into(), JsValue::String("rejected".into()));
obj.insert("reason".into(), r);
results_r.borrow_mut()[i] = Some(JsValue::PlainObject(Rc::new(RefCell::new(obj))));
settle_all_settled(&results_r, &remaining_r, &p_result_r, &q_r);
Ok(JsValue::Undefined)
}) as PromiseHandler);
promise_then(&p, on_fulfilled, on_rejected, queue);
}
p_result
}
pub fn promise_all_settled(promises: Vec<JsPromise>, queue: &MicrotaskQueue) -> JsPromise {
promise_all_settled_with_result(promises, JsPromise::new_pending(), queue)
}
fn settle_all_settled(
results: &Rc<RefCell<Vec<Option<JsValue>>>>,
remaining: &Rc<RefCell<usize>>,
p_result: &JsPromise,
queue: &MicrotaskQueue,
) {
let mut rem = remaining.borrow_mut();
*rem -= 1;
if *rem == 0 {
let all: Vec<JsValue> = results
.borrow()
.iter()
.map(|r| r.clone().expect("slot filled by preceding reaction"))
.collect();
p_result.resolve(JsValue::new_array(all), queue);
}
}
pub(crate) fn promise_any_with_result(
promises: Vec<JsPromise>,
p_result: JsPromise,
queue: &MicrotaskQueue,
) -> JsPromise {
let count = promises.len();
if count == 0 {
let agg = JsError::new_aggregate(Vec::new(), "All promises were rejected".to_string());
p_result.reject(JsValue::Error(Rc::new(agg)), queue);
return p_result;
}
let errors: Rc<RefCell<Vec<Option<JsValue>>>> = Rc::new(RefCell::new(vec![None; count]));
let remaining: Rc<RefCell<usize>> = Rc::new(RefCell::new(count));
for (i, p) in promises.into_iter().enumerate() {
let p_result_f = p_result.clone();
let q_f = queue.clone();
let errors_r = Rc::clone(&errors);
let remaining_r = Rc::clone(&remaining);
let p_result_r = p_result.clone();
let q_r = queue.clone();
let on_fulfilled = Some(Box::new(move |v: JsValue| {
p_result_f.resolve(v.clone(), &q_f);
Ok(v)
}) as PromiseHandler);
let on_rejected = Some(Box::new(move |r: JsValue| {
errors_r.borrow_mut()[i] = Some(r.clone());
let mut rem = remaining_r.borrow_mut();
*rem -= 1;
if *rem == 0 {
let all: Vec<JsValue> = errors_r
.borrow()
.iter()
.map(|e| e.clone().expect("slot filled by preceding reaction"))
.collect();
let agg = JsError::new_aggregate(all, "All promises were rejected".to_string());
p_result_r.reject(JsValue::Error(Rc::new(agg)), &q_r);
}
Err(r)
}) as PromiseHandler);
promise_then(&p, on_fulfilled, on_rejected, queue);
}
p_result
}
pub fn promise_any(promises: Vec<JsPromise>, queue: &MicrotaskQueue) -> JsPromise {
promise_any_with_result(promises, JsPromise::new_pending(), queue)
}
pub(crate) fn promise_race_with_result(
promises: Vec<JsPromise>,
p_result: JsPromise,
queue: &MicrotaskQueue,
) -> JsPromise {
for p in promises {
let p_result_f = p_result.clone();
let q_f = queue.clone();
let p_result_r = p_result.clone();
let q_r = queue.clone();
let on_fulfilled = Some(Box::new(move |v: JsValue| {
p_result_f.resolve(v.clone(), &q_f);
Ok(v)
}) as PromiseHandler);
let on_rejected = Some(Box::new(move |r: JsValue| {
p_result_r.reject(r.clone(), &q_r);
Err(r)
}) as PromiseHandler);
promise_then(&p, on_fulfilled, on_rejected, queue);
}
p_result
}
pub fn promise_race(promises: Vec<JsPromise>, queue: &MicrotaskQueue) -> JsPromise {
promise_race_with_result(promises, JsPromise::new_pending(), queue)
}
pub struct PromiseWithResolvers {
pub promise: JsPromise,
pub resolve: Box<dyn FnOnce(JsValue)>,
pub reject: Box<dyn FnOnce(JsValue)>,
}
pub fn promise_with_resolvers(queue: &MicrotaskQueue) -> PromiseWithResolvers {
let p = JsPromise::new_pending();
let p1 = p.clone();
let q1 = queue.clone();
let resolve: Box<dyn FnOnce(JsValue)> = Box::new(move |v| p1.resolve(v, &q1));
let p2 = p.clone();
let q2 = queue.clone();
let reject: Box<dyn FnOnce(JsValue)> = Box::new(move |r| p2.reject(r, &q2));
PromiseWithResolvers {
promise: p,
resolve,
reject,
}
}
pub struct UnhandledRejectionTracker {
promises: Vec<JsPromise>,
}
impl Default for UnhandledRejectionTracker {
fn default() -> Self {
Self::new()
}
}
impl UnhandledRejectionTracker {
pub fn new() -> Self {
Self {
promises: Vec::new(),
}
}
pub fn track(&mut self, promise: JsPromise) {
self.promises.push(promise);
}
pub fn collect_unhandled(&mut self) -> Vec<JsPromise> {
let unhandled: Vec<JsPromise> = self
.promises
.drain(..)
.filter(|p| p.is_rejected() && !p.is_handled())
.collect();
unhandled
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_microtask_queue_fifo_ordering() {
let queue = MicrotaskQueue::new();
let log: Rc<RefCell<Vec<i32>>> = Rc::new(RefCell::new(Vec::new()));
for i in 0..3_i32 {
let log = Rc::clone(&log);
queue.enqueue(Box::new(move || log.borrow_mut().push(i)));
}
queue.drain();
assert_eq!(*log.borrow(), vec![0, 1, 2]);
}
#[test]
fn test_microtask_queue_drain_picks_up_new_tasks() {
let queue = MicrotaskQueue::new();
let log: Rc<RefCell<Vec<i32>>> = Rc::new(RefCell::new(Vec::new()));
let log1 = Rc::clone(&log);
let queue2 = queue.clone();
queue.enqueue(Box::new(move || {
log1.borrow_mut().push(1);
let log2 = Rc::clone(&log1);
queue2.enqueue(Box::new(move || log2.borrow_mut().push(2)));
}));
queue.drain();
assert_eq!(*log.borrow(), vec![1, 2]);
}
#[test]
fn test_microtask_queue_len_and_is_empty() {
let queue = MicrotaskQueue::new();
assert!(queue.is_empty());
assert_eq!(queue.len(), 0);
queue.enqueue(Box::new(|| {}));
assert!(!queue.is_empty());
assert_eq!(queue.len(), 1);
queue.drain();
assert!(queue.is_empty());
}
#[test]
fn test_promise_new_resolves_synchronously() {
let queue = MicrotaskQueue::new();
let p = promise_new(|resolve, _| resolve(JsValue::Smi(42)), &queue);
assert!(p.is_fulfilled());
assert_eq!(p.value(), Some(JsValue::Smi(42)));
}
#[test]
fn test_promise_new_rejects_synchronously() {
let queue = MicrotaskQueue::new();
let p = promise_new(
|_, reject| reject(JsValue::String("err".to_string().into())),
&queue,
);
assert!(p.is_rejected());
assert_eq!(p.reason(), Some(JsValue::String("err".to_string().into())));
}
#[test]
fn test_promise_new_stays_pending_when_executor_does_nothing() {
let queue = MicrotaskQueue::new();
let p = promise_new(|_, _| {}, &queue);
assert!(p.is_pending());
}
#[test]
fn test_promise_resolve_static() {
let queue = MicrotaskQueue::new();
let p = promise_resolve(JsValue::Boolean(true), &queue);
assert!(p.is_fulfilled());
assert_eq!(p.value(), Some(JsValue::Boolean(true)));
}
#[test]
fn test_promise_reject_static() {
clear_active_promise_rejection_events();
let queue = MicrotaskQueue::new();
let p = promise_reject(JsValue::Smi(0), &queue);
assert!(p.is_rejected());
assert_eq!(p.reason(), Some(JsValue::Smi(0)));
}
#[test]
fn test_active_rejection_events_report_unhandled_rejection() {
clear_active_promise_rejection_events();
let queue = MicrotaskQueue::new();
let p = promise_reject(JsValue::String("edge-reject".into()), &queue);
let events = drain_active_promise_rejection_events();
assert_eq!(events.len(), 1);
assert_eq!(
events[0].kind,
PromiseRejectionEventKind::RejectedWithNoHandler
);
assert_eq!(events[0].promise_id, p.promise_id());
assert_eq!(events[0].reason, "edge-reject");
}
#[test]
fn test_active_rejection_events_cancel_when_handler_added_before_drain() {
clear_active_promise_rejection_events();
let queue = MicrotaskQueue::new();
let p = promise_reject(JsValue::String("handled".into()), &queue);
promise_catch(&p, Box::new(|_| Ok(JsValue::Undefined)), &queue);
assert!(drain_active_promise_rejection_events().is_empty());
}
#[test]
fn test_active_rejection_events_report_handler_after_host_observed() {
clear_active_promise_rejection_events();
let queue = MicrotaskQueue::new();
let p = promise_reject(JsValue::String("late".into()), &queue);
let rejected_events = drain_active_promise_rejection_events();
assert_eq!(rejected_events.len(), 1);
promise_catch(&p, Box::new(|_| Ok(JsValue::Undefined)), &queue);
let handled_events = drain_active_promise_rejection_events();
assert_eq!(handled_events.len(), 1);
assert_eq!(
handled_events[0].kind,
PromiseRejectionEventKind::HandlerAddedAfterReject
);
assert_eq!(handled_events[0].promise_id, p.promise_id());
assert_eq!(handled_events[0].reason, "late");
}
#[test]
fn test_promise_then_fulfilled_chain() {
let queue = MicrotaskQueue::new();
let p = promise_resolve(JsValue::Smi(1), &queue);
let result: Rc<RefCell<JsValue>> = Rc::new(RefCell::new(JsValue::Undefined));
let r2 = Rc::clone(&result);
promise_then(
&p,
Some(Box::new(move |v| {
*r2.borrow_mut() = v.clone();
Ok(v)
})),
None,
&queue,
);
queue.drain();
assert_eq!(*result.borrow(), JsValue::Smi(1));
}
#[test]
fn test_promise_then_rejection_propagates_without_handler() {
let queue = MicrotaskQueue::new();
let p = promise_reject(JsValue::Smi(99), &queue);
let result: Rc<RefCell<JsValue>> = Rc::new(RefCell::new(JsValue::Undefined));
let r2 = Rc::clone(&result);
let p2 = promise_then(&p, None, None, &queue);
promise_then(
&p2,
None,
Some(Box::new(move |r| {
*r2.borrow_mut() = r.clone();
Ok(JsValue::Undefined)
})),
&queue,
);
queue.drain();
assert_eq!(*result.borrow(), JsValue::Smi(99));
}
#[test]
fn test_promise_then_transforms_value() {
let queue = MicrotaskQueue::new();
let p = promise_resolve(JsValue::Smi(5), &queue);
let p2 = promise_then(
&p,
Some(Box::new(|v| {
if let JsValue::Smi(n) = v {
Ok(JsValue::Smi(n * 2))
} else {
Ok(v)
}
})),
None,
&queue,
);
queue.drain();
assert_eq!(p2.value(), Some(JsValue::Smi(10)));
}
#[test]
fn test_promise_then_handler_returns_err_rejects_downstream() {
let queue = MicrotaskQueue::new();
let p = promise_resolve(JsValue::Smi(1), &queue);
let p2 = promise_then(
&p,
Some(Box::new(|_v| {
Err(JsValue::String("boom".to_string().into()))
})),
None,
&queue,
);
queue.drain();
assert!(p2.is_rejected());
assert_eq!(
p2.reason(),
Some(JsValue::String("boom".to_string().into()))
);
}
#[test]
fn test_promise_then_multi_hop_chain() {
let queue = MicrotaskQueue::new();
let p = promise_resolve(JsValue::Smi(1), &queue);
let p2 = promise_then(
&p,
Some(Box::new(|v| {
if let JsValue::Smi(n) = v {
Ok(JsValue::Smi(n + 1))
} else {
Ok(v)
}
})),
None,
&queue,
);
let p3 = promise_then(
&p2,
Some(Box::new(|v| {
if let JsValue::Smi(n) = v {
Ok(JsValue::Smi(n * 10))
} else {
Ok(v)
}
})),
None,
&queue,
);
queue.drain();
assert_eq!(p3.value(), Some(JsValue::Smi(20)));
}
#[test]
fn test_promise_then_on_pending_fires_after_resolve() {
let queue = MicrotaskQueue::new();
let wr = promise_with_resolvers(&queue);
let result: Rc<RefCell<JsValue>> = Rc::new(RefCell::new(JsValue::Undefined));
let r2 = Rc::clone(&result);
promise_then(
&wr.promise,
Some(Box::new(move |v| {
*r2.borrow_mut() = v.clone();
Ok(v)
})),
None,
&queue,
);
assert_eq!(*result.borrow(), JsValue::Undefined);
(wr.resolve)(JsValue::Smi(7));
queue.drain();
assert_eq!(*result.borrow(), JsValue::Smi(7));
}
#[test]
fn test_promise_catch_handles_rejection() {
let queue = MicrotaskQueue::new();
let p = promise_reject(JsValue::String("fail".to_string().into()), &queue);
let caught: Rc<RefCell<JsValue>> = Rc::new(RefCell::new(JsValue::Undefined));
let c2 = Rc::clone(&caught);
let p2 = promise_catch(
&p,
Box::new(move |r| {
*c2.borrow_mut() = r.clone();
Ok(JsValue::Undefined)
}),
&queue,
);
queue.drain();
assert_eq!(*caught.borrow(), JsValue::String("fail".to_string().into()));
assert!(p2.is_fulfilled());
}
#[test]
fn test_promise_finally_runs_on_fulfill() {
let queue = MicrotaskQueue::new();
let ran: Rc<RefCell<bool>> = Rc::new(RefCell::new(false));
let ran2 = Rc::clone(&ran);
let p = promise_resolve(JsValue::Smi(1), &queue);
let p2 = promise_finally(
&p,
Box::new(move || {
*ran2.borrow_mut() = true;
Ok(JsValue::Undefined)
}),
&queue,
);
queue.drain();
assert!(*ran.borrow());
assert_eq!(p2.value(), Some(JsValue::Smi(1)));
}
#[test]
fn test_promise_finally_runs_on_reject() {
let queue = MicrotaskQueue::new();
let ran: Rc<RefCell<bool>> = Rc::new(RefCell::new(false));
let ran2 = Rc::clone(&ran);
let p = promise_reject(JsValue::Smi(0), &queue);
let p2 = promise_finally(
&p,
Box::new(move || {
*ran2.borrow_mut() = true;
Ok(JsValue::Undefined)
}),
&queue,
);
queue.drain();
assert!(*ran.borrow());
assert!(p2.is_rejected());
assert_eq!(p2.reason(), Some(JsValue::Smi(0)));
}
#[test]
fn test_promise_finally_can_replace_with_new_rejection() {
let queue = MicrotaskQueue::new();
let p = promise_resolve(JsValue::Smi(1), &queue);
let p2 = promise_finally(
&p,
Box::new(|| Err(JsValue::String("new error".to_string().into()))),
&queue,
);
queue.drain();
assert!(p2.is_rejected());
assert_eq!(
p2.reason(),
Some(JsValue::String("new error".to_string().into()))
);
}
#[test]
fn test_promise_all_resolves_when_all_fulfill() {
let queue = MicrotaskQueue::new();
let p = promise_all(
vec![
promise_resolve(JsValue::Smi(1), &queue),
promise_resolve(JsValue::Smi(2), &queue),
promise_resolve(JsValue::Smi(3), &queue),
],
&queue,
);
queue.drain();
if let Some(JsValue::Array(arr)) = p.value() {
assert_eq!(
*arr.borrow(),
vec![JsValue::Smi(1), JsValue::Smi(2), JsValue::Smi(3)]
);
} else {
panic!("expected Array");
}
}
#[test]
fn test_promise_all_rejects_on_first_rejection() {
let queue = MicrotaskQueue::new();
let p = promise_all(
vec![
promise_resolve(JsValue::Smi(1), &queue),
promise_reject(JsValue::String("err".to_string().into()), &queue),
promise_resolve(JsValue::Smi(3), &queue),
],
&queue,
);
queue.drain();
assert!(p.is_rejected());
assert_eq!(p.reason(), Some(JsValue::String("err".to_string().into())));
}
#[test]
fn test_promise_all_empty_resolves_with_empty_array() {
let queue = MicrotaskQueue::new();
let p = promise_all(vec![], &queue);
assert!(p.is_fulfilled());
if let Some(JsValue::Array(arr)) = p.value() {
assert!(arr.borrow().is_empty());
} else {
panic!("expected Array");
}
}
#[test]
fn test_promise_all_settled_never_rejects() {
let queue = MicrotaskQueue::new();
let p = promise_all_settled(
vec![
promise_resolve(JsValue::Smi(1), &queue),
promise_reject(JsValue::String("boom".to_string().into()), &queue),
],
&queue,
);
queue.drain();
assert!(p.is_fulfilled());
if let Some(JsValue::Array(arr)) = p.value() {
assert_eq!(arr.borrow().len(), 2);
if let JsValue::PlainObject(obj0) = &arr.borrow()[0] {
let map0 = obj0.borrow();
assert_eq!(
map0.get("status"),
Some(&JsValue::String("fulfilled".into()))
);
assert_eq!(map0.get("value"), Some(&JsValue::Smi(1)));
} else {
panic!("expected PlainObject at [0]");
}
if let JsValue::PlainObject(obj1) = &arr.borrow()[1] {
let map1 = obj1.borrow();
assert_eq!(
map1.get("status"),
Some(&JsValue::String("rejected".into()))
);
assert_eq!(
map1.get("reason"),
Some(&JsValue::String("boom".to_string().into()))
);
} else {
panic!("expected PlainObject at [1]");
}
} else {
panic!("expected JsValue::Array");
}
}
#[test]
fn test_promise_any_resolves_with_first_fulfill() {
let queue = MicrotaskQueue::new();
let p = promise_any(
vec![
promise_reject(JsValue::Smi(0), &queue),
promise_resolve(JsValue::Smi(2), &queue),
promise_resolve(JsValue::Smi(3), &queue),
],
&queue,
);
queue.drain();
assert!(p.is_fulfilled());
assert_eq!(p.value(), Some(JsValue::Smi(2)));
}
#[test]
fn test_promise_any_rejects_when_all_reject() {
let queue = MicrotaskQueue::new();
let p = promise_any(
vec![
promise_reject(JsValue::Smi(1), &queue),
promise_reject(JsValue::Smi(2), &queue),
],
&queue,
);
queue.drain();
assert!(p.is_rejected());
if let Some(JsValue::Error(err)) = p.reason() {
assert_eq!(err.name(), "AggregateError");
assert_eq!(err.errors, vec![JsValue::Smi(1), JsValue::Smi(2)]);
} else {
panic!("expected AggregateError reason");
}
}
#[test]
fn test_promise_any_empty_rejects_with_aggregate_error() {
let queue = MicrotaskQueue::new();
let p = promise_any(vec![], &queue);
assert!(p.is_rejected());
if let Some(JsValue::Error(err)) = p.reason() {
assert_eq!(err.name(), "AggregateError");
assert!(err.errors.is_empty());
} else {
panic!("expected AggregateError reason");
}
}
#[test]
fn test_promise_race_first_fulfill_wins() {
let queue = MicrotaskQueue::new();
let p = promise_race(
vec![
promise_resolve(JsValue::Smi(1), &queue),
promise_resolve(JsValue::Smi(2), &queue),
],
&queue,
);
queue.drain();
assert!(p.is_fulfilled());
assert_eq!(p.value(), Some(JsValue::Smi(1)));
}
#[test]
fn test_promise_race_first_rejection_wins() {
let queue = MicrotaskQueue::new();
let p = promise_race(
vec![
promise_reject(JsValue::String("no".to_string().into()), &queue),
promise_resolve(JsValue::Smi(2), &queue),
],
&queue,
);
queue.drain();
assert!(p.is_rejected());
assert_eq!(p.reason(), Some(JsValue::String("no".to_string().into())));
}
#[test]
fn test_promise_with_resolvers_resolve() {
let queue = MicrotaskQueue::new();
let wr = promise_with_resolvers(&queue);
assert!(wr.promise.is_pending());
(wr.resolve)(JsValue::Smi(42));
assert!(wr.promise.is_fulfilled());
assert_eq!(wr.promise.value(), Some(JsValue::Smi(42)));
}
#[test]
fn test_promise_with_resolvers_reject() {
let queue = MicrotaskQueue::new();
let wr = promise_with_resolvers(&queue);
(wr.reject)(JsValue::String("fail".to_string().into()));
assert!(wr.promise.is_rejected());
assert_eq!(
wr.promise.reason(),
Some(JsValue::String("fail".to_string().into()))
);
}
#[test]
fn test_microtask_ordering_in_chain() {
let queue = MicrotaskQueue::new();
let log: Rc<RefCell<Vec<i32>>> = Rc::new(RefCell::new(Vec::new()));
let p = promise_resolve(JsValue::Smi(0), &queue);
let log1 = Rc::clone(&log);
let p2 = promise_then(
&p,
Some(Box::new(move |v| {
log1.borrow_mut().push(1);
Ok(v)
})),
None,
&queue,
);
let log2 = Rc::clone(&log);
promise_then(
&p2,
Some(Box::new(move |v| {
log2.borrow_mut().push(2);
Ok(v)
})),
None,
&queue,
);
assert!(log.borrow().is_empty());
queue.drain();
assert_eq!(*log.borrow(), vec![1, 2]);
}
#[test]
fn test_resolve_then_fires_asynchronously() {
let queue = MicrotaskQueue::new();
let wr = promise_with_resolvers(&queue);
let fired: Rc<RefCell<bool>> = Rc::new(RefCell::new(false));
let fired2 = Rc::clone(&fired);
promise_then(
&wr.promise,
Some(Box::new(move |_| {
*fired2.borrow_mut() = true;
Ok(JsValue::Undefined)
})),
None,
&queue,
);
(wr.resolve)(JsValue::Smi(1));
assert!(!*fired.borrow());
queue.drain();
assert!(*fired.borrow());
}
#[test]
fn test_promise_state_variants() {
let queue = MicrotaskQueue::new();
let p = promise_resolve(JsValue::Smi(5), &queue);
assert_eq!(p.state(), PromiseState::Fulfilled(JsValue::Smi(5)));
let p2 = promise_reject(JsValue::String("err".to_string().into()), &queue);
assert_eq!(
p2.state(),
PromiseState::Rejected(JsValue::String("err".to_string().into()))
);
let p3 = promise_new(|_, _| {}, &queue);
assert_eq!(p3.state(), PromiseState::Pending);
}
#[test]
fn test_unhandled_rejection_detected() {
let queue = MicrotaskQueue::new();
let mut tracker = UnhandledRejectionTracker::new();
let p = promise_reject(JsValue::String("oops".into()), &queue);
tracker.track(p);
queue.drain();
let unhandled = tracker.collect_unhandled();
assert_eq!(unhandled.len(), 1);
assert_eq!(unhandled[0].reason(), Some(JsValue::String("oops".into())));
}
#[test]
fn test_handled_rejection_not_reported() {
let queue = MicrotaskQueue::new();
let mut tracker = UnhandledRejectionTracker::new();
let p = promise_reject(JsValue::String("caught".into()), &queue);
promise_catch(&p, Box::new(|_| Ok(JsValue::Undefined)), &queue);
tracker.track(p);
queue.drain();
let unhandled = tracker.collect_unhandled();
assert!(unhandled.is_empty());
}
#[test]
fn test_tracker_ignores_fulfilled_promises() {
let queue = MicrotaskQueue::new();
let mut tracker = UnhandledRejectionTracker::new();
let p = promise_resolve(JsValue::Smi(1), &queue);
tracker.track(p);
queue.drain();
let unhandled = tracker.collect_unhandled();
assert!(unhandled.is_empty());
}
#[test]
fn test_tracker_default() {
let tracker = UnhandledRejectionTracker::default();
assert_eq!(tracker.promises.len(), 0);
}
#[test]
fn test_promise_identity_equality() {
let queue = MicrotaskQueue::new();
let p1 = promise_resolve(JsValue::Smi(1), &queue);
let p1_clone = p1.clone();
let p2 = promise_resolve(JsValue::Smi(1), &queue);
assert_eq!(p1, p1_clone);
assert_ne!(p1, p2);
}
#[test]
fn test_promise_race_empty_stays_pending() {
let queue = MicrotaskQueue::new();
let p = promise_race(vec![], &queue);
queue.drain();
assert!(p.is_pending());
}
#[test]
fn test_promise_race_reject_wins_when_first() {
let queue = MicrotaskQueue::new();
let p = promise_race(
vec![
promise_reject(JsValue::Smi(99), &queue),
promise_resolve(JsValue::Smi(1), &queue),
],
&queue,
);
queue.drain();
assert!(p.is_rejected());
assert_eq!(p.reason(), Some(JsValue::Smi(99)));
}
#[test]
fn test_promise_any_single_fulfilled() {
let queue = MicrotaskQueue::new();
let p = promise_any(vec![promise_resolve(JsValue::Smi(42), &queue)], &queue);
queue.drain();
assert!(p.is_fulfilled());
assert_eq!(p.value(), Some(JsValue::Smi(42)));
}
#[test]
fn test_promise_any_aggregate_error_message() {
let queue = MicrotaskQueue::new();
let p = promise_any(vec![promise_reject(JsValue::Smi(1), &queue)], &queue);
queue.drain();
if let Some(JsValue::Error(err)) = p.reason() {
assert_eq!(err.message(), "All promises were rejected");
} else {
panic!("expected AggregateError");
}
}
#[test]
fn test_promise_new_resolve_then_reject_only_first_counts() {
let queue = MicrotaskQueue::new();
let resolve_fn: Rc<RefCell<Option<Box<dyn FnOnce(JsValue)>>>> = Rc::new(RefCell::new(None));
let reject_fn: Rc<RefCell<Option<Box<dyn FnOnce(JsValue)>>>> = Rc::new(RefCell::new(None));
let rf = Rc::clone(&resolve_fn);
let rj = Rc::clone(&reject_fn);
let p = promise_new(
move |resolve, reject| {
*rf.borrow_mut() = Some(resolve);
*rj.borrow_mut() = Some(reject);
},
&queue,
);
if let Some(f) = resolve_fn.borrow_mut().take() {
f(JsValue::Smi(1));
}
if let Some(f) = reject_fn.borrow_mut().take() {
f(JsValue::Smi(2));
}
queue.drain();
assert!(p.is_fulfilled());
assert_eq!(p.value(), Some(JsValue::Smi(1)));
}
#[test]
fn test_promise_new_reject_then_resolve_only_first_counts() {
let queue = MicrotaskQueue::new();
let resolve_fn: Rc<RefCell<Option<Box<dyn FnOnce(JsValue)>>>> = Rc::new(RefCell::new(None));
let reject_fn: Rc<RefCell<Option<Box<dyn FnOnce(JsValue)>>>> = Rc::new(RefCell::new(None));
let rf = Rc::clone(&resolve_fn);
let rj = Rc::clone(&reject_fn);
let p = promise_new(
move |resolve, reject| {
*rf.borrow_mut() = Some(resolve);
*rj.borrow_mut() = Some(reject);
},
&queue,
);
if let Some(f) = reject_fn.borrow_mut().take() {
f(JsValue::String("error".into()));
}
if let Some(f) = resolve_fn.borrow_mut().take() {
f(JsValue::Smi(1));
}
queue.drain();
assert!(p.is_rejected());
assert_eq!(p.reason(), Some(JsValue::String("error".into())));
}
#[test]
fn test_promise_finally_passes_through_fulfilled_value() {
let queue = MicrotaskQueue::new();
let p = promise_resolve(JsValue::Smi(42), &queue);
let p2 = promise_finally(&p, Box::new(|| Ok(JsValue::Undefined)), &queue);
queue.drain();
assert!(p2.is_fulfilled());
assert_eq!(p2.value(), Some(JsValue::Smi(42)));
}
#[test]
fn test_promise_finally_passes_through_rejected_reason() {
let queue = MicrotaskQueue::new();
let p = promise_reject(JsValue::String("fail".into()), &queue);
let p2 = promise_finally(&p, Box::new(|| Ok(JsValue::Undefined)), &queue);
queue.drain();
assert!(p2.is_rejected());
assert_eq!(p2.reason(), Some(JsValue::String("fail".into())));
}
#[test]
fn test_promise_try_resolves_return_value() {
let queue = MicrotaskQueue::new();
let p = promise_try(
|args| {
assert_eq!(args, vec![JsValue::Smi(2), JsValue::Smi(3)]);
Ok(JsValue::Smi(5))
},
vec![JsValue::Smi(2), JsValue::Smi(3)],
&queue,
);
queue.drain();
assert!(p.is_fulfilled());
assert_eq!(p.value(), Some(JsValue::Smi(5)));
}
#[test]
fn test_promise_try_rejects_thrown_reason() {
let queue = MicrotaskQueue::new();
let p = promise_try(
|_args| Err(JsValue::String("boom".into())),
vec![JsValue::Smi(1)],
&queue,
);
queue.drain();
assert!(p.is_rejected());
assert_eq!(p.reason(), Some(JsValue::String("boom".into())));
}
#[test]
fn test_promise_finally_waits_for_returned_promise_before_fulfill() {
let queue = MicrotaskQueue::new();
let cleanup = promise_with_resolvers(&queue);
let observed: Rc<RefCell<Vec<JsValue>>> = Rc::new(RefCell::new(Vec::new()));
let observed_cleanup = Rc::clone(&observed);
let observed_value = Rc::clone(&observed);
let result = promise_finally(
&promise_resolve(JsValue::Smi(7), &queue),
Box::new(move || {
observed_cleanup
.borrow_mut()
.push(JsValue::String("cleanup".into()));
Ok(JsValue::Promise(cleanup.promise.clone()))
}),
&queue,
);
promise_then(
&result,
Some(Box::new(move |value| {
observed_value.borrow_mut().push(value.clone());
Ok(value)
})),
None,
&queue,
);
queue.drain();
assert_eq!(*observed.borrow(), vec![JsValue::String("cleanup".into())]);
assert!(result.is_pending());
(cleanup.resolve)(JsValue::Undefined);
queue.drain();
assert_eq!(
*observed.borrow(),
vec![JsValue::String("cleanup".into()), JsValue::Smi(7)]
);
assert_eq!(result.value(), Some(JsValue::Smi(7)));
}
#[test]
fn test_promise_finally_waits_for_returned_promise_before_reject() {
let queue = MicrotaskQueue::new();
let cleanup = promise_with_resolvers(&queue);
let result = promise_finally(
&promise_reject(JsValue::String("fail".into()), &queue),
Box::new(move || Ok(JsValue::Promise(cleanup.promise.clone()))),
&queue,
);
queue.drain();
assert!(result.is_pending());
(cleanup.resolve)(JsValue::Undefined);
queue.drain();
assert!(result.is_rejected());
assert_eq!(result.reason(), Some(JsValue::String("fail".into())));
}
#[test]
fn test_promise_finally_rejects_with_returned_thenable_reason() {
let queue = MicrotaskQueue::new();
let cleanup = promise_with_resolvers(&queue);
let result = promise_finally(
&promise_resolve(JsValue::Smi(1), &queue),
Box::new(move || Ok(JsValue::Promise(cleanup.promise.clone()))),
&queue,
);
queue.drain();
assert!(result.is_pending());
(cleanup.reject)(JsValue::String("cleanup failed".into()));
queue.drain();
assert!(result.is_rejected());
assert_eq!(
result.reason(),
Some(JsValue::String("cleanup failed".into()))
);
}
#[test]
fn test_promise_all_single_element() {
let queue = MicrotaskQueue::new();
let p = promise_all(vec![promise_resolve(JsValue::Smi(7), &queue)], &queue);
queue.drain();
if let Some(JsValue::Array(arr)) = p.value() {
assert_eq!(*arr.borrow(), vec![JsValue::Smi(7)]);
} else {
panic!("expected Array");
}
}
}