use std::cell::{Cell, RefCell};
use std::ops::DerefMut;
use std::ptr;
use std::rc::Rc;
use dom_struct::dom_struct;
use js::context::JSContext;
use js::conversions::{ConversionResult, FromJSValConvertibleRc};
use js::jsapi::{
AddRawValueRoot, CallArgs, GetFunctionNativeReserved, Heap, JS_ClearPendingException,
JS_GetFunctionObject, JS_NewFunction, JSAutoRealm, JSContext as RawJSContext, JSObject,
PromiseState, PromiseUserInputEventHandlingState, RemoveRawValueRoot,
SetFunctionNativeReserved,
};
use js::jsval::{Int32Value, JSVal, NullValue, ObjectValue, UndefinedValue};
use js::realm::{AutoRealm, CurrentRealm};
use js::rust::wrappers::{
CallOriginalPromiseReject, CallOriginalPromiseResolve, GetPromiseIsHandled, GetPromiseState,
IsPromiseObject, NewPromiseObject, RejectPromise, ResolvePromise, SetAnyPromiseIsHandled,
SetPromiseUserInputEventHandlingState,
};
use js::rust::wrappers2::{AddPromiseReactions, NewFunctionWithReserved};
use js::rust::{HandleObject, HandleValue, MutableHandleObject, Runtime};
use script_bindings::conversions::SafeToJSValConvertible;
use script_bindings::reflector::{DomObject, MutDomObject, Reflector};
use script_bindings::settings_stack::run_a_script;
use crate::DomTypeHolder;
use crate::dom::bindings::conversions::root_from_object;
use crate::dom::bindings::error::{Error, ErrorToJsval};
use crate::dom::bindings::reflector::DomGlobal;
use crate::dom::bindings::root::{AsHandleValue, DomRoot};
use crate::dom::globalscope::GlobalScope;
use crate::dom::promisenativehandler::{Callback, PromiseNativeHandler};
use crate::microtask::{Microtask, MicrotaskRunnable};
use crate::realms::{InRealm, enter_auto_realm, enter_realm};
use crate::script_runtime::{CanGc, JSContext as SafeJSContext};
use crate::script_thread::ScriptThread;
#[dom_struct]
#[cfg_attr(crown, crown::unrooted_must_root_lint::allow_unrooted_in_rc)]
pub(crate) struct Promise {
reflector: Reflector,
#[ignore_malloc_size_of = "SM handles JS values"]
permanent_js_root: Heap<JSVal>,
}
trait PromiseHelper {
fn initialize(&self, cx: SafeJSContext);
}
impl PromiseHelper for Rc<Promise> {
#[expect(unsafe_code)]
fn initialize(&self, cx: SafeJSContext) {
let obj = self.reflector().get_jsobject();
self.permanent_js_root.set(ObjectValue(*obj));
unsafe {
assert!(AddRawValueRoot(
*cx,
self.permanent_js_root.get_unsafe(),
c"Promise::root".as_ptr(),
));
}
}
}
impl Drop for Promise {
#[expect(unsafe_code)]
fn drop(&mut self) {
unsafe {
let object = self.permanent_js_root.get().to_object();
assert!(!object.is_null());
if let Some(cx) = Runtime::get() {
RemoveRawValueRoot(cx.as_ptr(), self.permanent_js_root.get_unsafe());
}
}
}
}
impl Promise {
pub(crate) fn new(global: &GlobalScope, can_gc: CanGc) -> Rc<Promise> {
let realm = enter_realm(global);
let comp = InRealm::Entered(&realm);
Promise::new_in_current_realm(comp, can_gc)
}
pub(crate) fn new_in_current_realm(_comp: InRealm, can_gc: CanGc) -> Rc<Promise> {
let cx = GlobalScope::get_cx();
rooted!(in(*cx) let mut obj = ptr::null_mut::<JSObject>());
Promise::create_js_promise(cx, obj.handle_mut(), can_gc);
Promise::new_with_js_promise(obj.handle(), cx)
}
pub(crate) fn new2(cx: &mut js::context::JSContext, global: &GlobalScope) -> Rc<Promise> {
let mut realm = AutoRealm::new(
cx,
std::ptr::NonNull::new(global.reflector().get_jsobject().get()).unwrap(),
);
let mut current_realm = realm.current_realm();
Promise::new_in_realm(&mut current_realm)
}
pub(crate) fn new_in_realm(current_realm: &mut CurrentRealm) -> Rc<Promise> {
let cx = current_realm.deref_mut();
rooted!(&in(cx) let mut obj = ptr::null_mut::<JSObject>());
Promise::create_js_promise(cx.into(), obj.handle_mut(), CanGc::from_cx(cx));
Promise::new_with_js_promise(obj.handle(), cx.into())
}
pub(crate) fn duplicate(&self) -> Rc<Promise> {
let cx = GlobalScope::get_cx();
Promise::new_with_js_promise(self.reflector().get_jsobject(), cx)
}
#[expect(unsafe_code)]
#[cfg_attr(crown, expect(crown::unrooted_must_root))]
pub(crate) fn new_with_js_promise(obj: HandleObject, cx: SafeJSContext) -> Rc<Promise> {
unsafe {
assert!(IsPromiseObject(obj));
let promise = Promise {
reflector: Reflector::new(),
permanent_js_root: Heap::default(),
};
let promise = Rc::new(promise);
promise.init_reflector_without_associated_memory(obj.get());
promise.initialize(cx);
promise
}
}
#[expect(unsafe_code)]
fn create_js_promise(cx: SafeJSContext, mut obj: MutableHandleObject, _can_gc: CanGc) {
unsafe {
let do_nothing_func = JS_NewFunction(
*cx,
Some(do_nothing_promise_executor),
2,
0,
ptr::null(),
);
assert!(!do_nothing_func.is_null());
rooted!(in(*cx) let do_nothing_obj = JS_GetFunctionObject(do_nothing_func));
assert!(!do_nothing_obj.is_null());
obj.set(NewPromiseObject(*cx, do_nothing_obj.handle()));
assert!(!obj.is_null());
let is_user_interacting = if ScriptThread::is_user_interacting() {
PromiseUserInputEventHandlingState::HadUserInteractionAtCreation
} else {
PromiseUserInputEventHandlingState::DidntHaveUserInteractionAtCreation
};
SetPromiseUserInputEventHandlingState(obj.handle(), is_user_interacting);
}
}
#[expect(unsafe_code)]
pub(crate) fn new_resolved(
global: &GlobalScope,
cx: SafeJSContext,
value: impl SafeToJSValConvertible,
can_gc: CanGc,
) -> Rc<Promise> {
let _ac = JSAutoRealm::new(*cx, global.reflector().get_jsobject().get());
rooted!(in(*cx) let mut rval = UndefinedValue());
value.safe_to_jsval(cx, rval.handle_mut(), can_gc);
unsafe {
rooted!(in(*cx) let p = CallOriginalPromiseResolve(*cx, rval.handle()));
assert!(!p.handle().is_null());
Promise::new_with_js_promise(p.handle(), cx)
}
}
#[expect(unsafe_code)]
pub(crate) fn new_rejected(
global: &GlobalScope,
cx: SafeJSContext,
value: impl SafeToJSValConvertible,
can_gc: CanGc,
) -> Rc<Promise> {
let _ac = JSAutoRealm::new(*cx, global.reflector().get_jsobject().get());
rooted!(in(*cx) let mut rval = UndefinedValue());
value.safe_to_jsval(cx, rval.handle_mut(), can_gc);
unsafe {
rooted!(in(*cx) let p = CallOriginalPromiseReject(*cx, rval.handle()));
assert!(!p.handle().is_null());
Promise::new_with_js_promise(p.handle(), cx)
}
}
pub(crate) fn resolve_native<T>(&self, val: &T, can_gc: CanGc)
where
T: SafeToJSValConvertible,
{
let cx = GlobalScope::get_cx();
let _ac = enter_realm(self);
rooted!(in(*cx) let mut v = UndefinedValue());
val.safe_to_jsval(cx, v.handle_mut(), can_gc);
self.resolve(cx, v.handle(), can_gc);
}
#[expect(unsafe_code)]
pub(crate) fn resolve(&self, cx: SafeJSContext, value: HandleValue, _can_gc: CanGc) {
unsafe {
if !ResolvePromise(*cx, self.promise_obj(), value) {
JS_ClearPendingException(*cx);
}
}
}
pub(crate) fn reject_native<T>(&self, val: &T, can_gc: CanGc)
where
T: SafeToJSValConvertible,
{
let cx = GlobalScope::get_cx();
let _ac = enter_realm(self);
rooted!(in(*cx) let mut v = UndefinedValue());
val.safe_to_jsval(cx, v.handle_mut(), can_gc);
self.reject(cx, v.handle(), can_gc);
}
pub(crate) fn reject_error(&self, error: Error, can_gc: CanGc) {
let cx = GlobalScope::get_cx();
let _ac = enter_realm(self);
rooted!(in(*cx) let mut v = UndefinedValue());
error.to_jsval(cx, &self.global(), v.handle_mut(), can_gc);
self.reject(cx, v.handle(), can_gc);
}
#[expect(unsafe_code)]
pub(crate) fn reject(&self, cx: SafeJSContext, value: HandleValue, _can_gc: CanGc) {
unsafe {
if !RejectPromise(*cx, self.promise_obj(), value) {
JS_ClearPendingException(*cx);
}
}
}
#[expect(unsafe_code)]
pub(crate) fn is_fulfilled(&self) -> bool {
let state = unsafe { GetPromiseState(self.promise_obj()) };
matches!(state, PromiseState::Rejected | PromiseState::Fulfilled)
}
#[expect(unsafe_code)]
pub(crate) fn is_rejected(&self) -> bool {
let state = unsafe { GetPromiseState(self.promise_obj()) };
matches!(state, PromiseState::Rejected)
}
#[expect(unsafe_code)]
pub(crate) fn is_pending(&self) -> bool {
let state = unsafe { GetPromiseState(self.promise_obj()) };
matches!(state, PromiseState::Pending)
}
#[expect(unsafe_code)]
pub(crate) fn promise_obj(&self) -> HandleObject<'_> {
let obj = self.reflector().get_jsobject();
unsafe {
assert!(IsPromiseObject(obj));
}
obj
}
#[expect(unsafe_code)]
pub(crate) fn append_native_handler(
&self,
cx: &mut CurrentRealm,
handler: &PromiseNativeHandler,
) {
let in_realm_proof = cx.into();
let realm = InRealm::Already(&in_realm_proof);
run_a_script::<DomTypeHolder, _>(&handler.global_(realm), || {
rooted!(&in(cx) let resolve_func =
create_native_handler_function(cx,
handler.reflector().get_jsobject(),
NativeHandlerTask::Resolve));
rooted!(&in(cx) let reject_func =
create_native_handler_function(cx,
handler.reflector().get_jsobject(),
NativeHandlerTask::Reject));
unsafe {
let ok = AddPromiseReactions(
cx,
self.promise_obj(),
resolve_func.handle(),
reject_func.handle(),
);
assert!(ok);
}
})
}
#[expect(unsafe_code)]
pub(crate) fn get_promise_is_handled(&self) -> bool {
unsafe { GetPromiseIsHandled(self.reflector().get_jsobject()) }
}
#[expect(unsafe_code)]
pub(crate) fn set_promise_is_handled(&self) -> bool {
let cx = GlobalScope::get_cx();
unsafe { SetAnyPromiseIsHandled(*cx, self.reflector().get_jsobject()) }
}
}
#[expect(unsafe_code)]
unsafe extern "C" fn do_nothing_promise_executor(
_cx: *mut RawJSContext,
argc: u32,
vp: *mut JSVal,
) -> bool {
let args = unsafe { CallArgs::from_vp(vp, argc) };
args.rval().set(UndefinedValue());
true
}
const SLOT_NATIVEHANDLER: usize = 0;
const SLOT_NATIVEHANDLER_TASK: usize = 1;
#[derive(PartialEq)]
enum NativeHandlerTask {
Resolve = 0,
Reject = 1,
}
#[expect(unsafe_code)]
unsafe extern "C" fn native_handler_callback(
cx: *mut RawJSContext,
argc: u32,
vp: *mut JSVal,
) -> bool {
let mut cx = unsafe { JSContext::from_ptr(ptr::NonNull::new(cx).unwrap()) };
let mut cx = CurrentRealm::assert(&mut cx);
let cx = &mut cx;
let args = unsafe { CallArgs::from_vp(vp, argc) };
let native_handler_value =
unsafe { *GetFunctionNativeReserved(args.callee(), SLOT_NATIVEHANDLER) };
rooted!(&in(cx) let native_handler_value = native_handler_value);
assert!(native_handler_value.get().is_object());
let handler = unsafe {
root_from_object::<PromiseNativeHandler>(native_handler_value.to_object(), cx.raw_cx())
}
.expect("unexpected value for native handler in promise native handler callback");
let native_handler_task_value =
unsafe { *GetFunctionNativeReserved(args.callee(), SLOT_NATIVEHANDLER_TASK) };
rooted!(&in(cx) let native_handler_task_value = native_handler_task_value);
match native_handler_task_value.to_int32() {
native_handler_task_value
if native_handler_task_value == NativeHandlerTask::Resolve as i32 =>
{
handler.resolved_callback(cx, unsafe { HandleValue::from_raw(args.get(0)) })
},
native_handler_task_value
if native_handler_task_value == NativeHandlerTask::Reject as i32 =>
{
handler.rejected_callback(cx, unsafe { HandleValue::from_raw(args.get(0)) })
},
_ => panic!("unexpected native handler task value"),
};
true
}
#[expect(unsafe_code)]
fn create_native_handler_function(
cx: &mut JSContext,
holder: HandleObject,
task: NativeHandlerTask,
) -> *mut JSObject {
unsafe {
let func = NewFunctionWithReserved(cx, Some(native_handler_callback), 1, 0, ptr::null());
assert!(!func.is_null());
rooted!(&in(cx) let obj = JS_GetFunctionObject(func));
assert!(!obj.is_null());
SetFunctionNativeReserved(obj.get(), SLOT_NATIVEHANDLER, &ObjectValue(*holder));
SetFunctionNativeReserved(obj.get(), SLOT_NATIVEHANDLER_TASK, &Int32Value(task as i32));
obj.get()
}
}
impl FromJSValConvertibleRc for Promise {
#[expect(unsafe_code)]
unsafe fn from_jsval(
_cx: *mut RawJSContext,
value: HandleValue,
) -> Result<ConversionResult<Rc<Promise>>, ()> {
let mut cx = unsafe { script_bindings::script_runtime::temp_cx() };
Self::safe_from_jsval(&mut cx, value)
}
fn safe_from_jsval(
cx: &mut JSContext,
value: HandleValue,
) -> Result<ConversionResult<Rc<Promise>>, ()> {
if value.get().is_null() {
return Ok(ConversionResult::Failure(c"null not allowed".into()));
}
let realm = CurrentRealm::assert(cx);
let global_scope = GlobalScope::from_current_realm(&realm);
let promise = Promise::new_resolved(&global_scope, cx.into(), value, CanGc::from_cx(cx));
Ok(ConversionResult::Success(promise))
}
}
type WaitForAllSuccessSteps = Rc<dyn Fn(&mut JSContext, Vec<HandleValue>)>;
type WaitForAllFailureSteps = Rc<dyn Fn(&mut JSContext, HandleValue)>;
#[derive(JSTraceable, MallocSizeOf)]
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
struct WaitForAllFulfillmentHandler {
#[ignore_malloc_size_of = "callbacks are hard"]
#[no_trace]
success_steps: WaitForAllSuccessSteps,
#[ignore_malloc_size_of = "mozjs"]
#[expect(clippy::vec_box)]
result: Rc<RefCell<Vec<Box<Heap<JSVal>>>>>,
promise_index: usize,
#[conditional_malloc_size_of]
fulfilled_count: Rc<RefCell<usize>>,
}
impl Callback for WaitForAllFulfillmentHandler {
fn callback(&self, cx: &mut CurrentRealm, v: HandleValue) {
let equals_total = {
let result = self.result.borrow_mut();
result[self.promise_index].set(v.get());
let mut fulfilled_count = self.fulfilled_count.borrow_mut();
*fulfilled_count += 1;
*fulfilled_count == result.len()
};
if equals_total {
let result_ref = self.result.borrow();
let result_handles: Vec<HandleValue> =
result_ref.iter().map(|v| v.as_handle_value()).collect();
(self.success_steps)(cx, result_handles);
}
}
}
#[derive(Clone, JSTraceable, MallocSizeOf)]
struct WaitForAllRejectionHandler {
#[ignore_malloc_size_of = "callbacks are hard"]
#[no_trace]
failure_steps: WaitForAllFailureSteps,
rejected: Cell<bool>,
}
impl Callback for WaitForAllRejectionHandler {
fn callback(&self, cx: &mut CurrentRealm, v: HandleValue) {
if self.rejected.replace(true) {
return;
}
(self.failure_steps)(cx, v);
}
}
#[derive(JSTraceable, MallocSizeOf)]
pub(crate) struct WaitForAllSuccessStepsMicrotask {
global: DomRoot<GlobalScope>,
#[ignore_malloc_size_of = "Closure is hard"]
#[no_trace]
success_steps: WaitForAllSuccessSteps,
}
impl MicrotaskRunnable for WaitForAllSuccessStepsMicrotask {
fn handler(&self, cx: &mut JSContext) {
(self.success_steps)(cx, vec![]);
}
fn enter_realm<'cx>(&self, cx: &'cx mut JSContext) -> AutoRealm<'cx> {
enter_auto_realm(cx, &*self.global)
}
}
#[cfg_attr(crown, expect(crown::unrooted_must_root))]
fn wait_for_all(
cx: &mut CurrentRealm,
global: &GlobalScope,
promises: Vec<Rc<Promise>>,
success_steps: WaitForAllSuccessSteps,
failure_steps: WaitForAllFailureSteps,
) {
let fulfilled_count: Rc<RefCell<usize>> = Default::default();
let rejection_handler = WaitForAllRejectionHandler {
failure_steps,
rejected: Default::default(),
};
if promises.is_empty() {
global.enqueue_microtask(Microtask::WaitForAllSuccessSteps(
WaitForAllSuccessStepsMicrotask {
global: DomRoot::from_ref(global),
success_steps,
},
));
return;
}
let result: Rc<RefCell<Vec<Box<Heap<JSVal>>>>> = Default::default();
for (promise_index, promise) in promises.into_iter().enumerate() {
let result = result.clone();
{
let mut result_list = result.borrow_mut();
rooted!(&in(cx) let null_value = NullValue());
result_list.push(Heap::boxed(null_value.get()));
}
let handler = PromiseNativeHandler::new(
global,
Some(Box::new(WaitForAllFulfillmentHandler {
success_steps: success_steps.clone(),
result,
promise_index,
fulfilled_count: fulfilled_count.clone(),
})),
Some(Box::new(rejection_handler.clone())),
CanGc::from_cx(cx),
);
promise.append_native_handler(cx, &handler);
}
}
pub(crate) fn wait_for_all_promise(
cx: &mut CurrentRealm,
global: &GlobalScope,
promises: Vec<Rc<Promise>>,
) -> Rc<Promise> {
let promise = Promise::new2(cx, global);
let success_promise = promise.clone();
let failure_promise = promise.clone();
let success_steps = Rc::new(move |cx: &mut JSContext, results: Vec<HandleValue>| {
success_promise.resolve_native(&results, CanGc::from_cx(cx));
});
let failure_steps = Rc::new(move |cx: &mut JSContext, reason: HandleValue| {
failure_promise.reject_native(&reason, CanGc::from_cx(cx));
});
wait_for_all(cx, global, promises, success_steps, failure_steps);
promise
}