use ::std::cell::RefCell;
use ::std::collections::VecDeque;
use ::std::ffi::CString;
use ::std::os::raw::c_void;
use ::std::ptr;
use ::std::sync::atomic::{AtomicUsize, Ordering};
use ::std::sync::OnceLock;
use mozjs::glue::{CreateJobQueue, DeleteJobQueue, JobQueueTraps};
use mozjs::jsapi::*;
use mozjs::jsval::{JSVal, UndefinedValue};
use mozjs::realm::AutoRealm;
use mozjs::rooted;
use mozjs::rust::wrappers2::{RunJobs, SetJobQueue};
static JOB_COUNTER: AtomicUsize = AtomicUsize::new(0);
pub type UncaughtExceptionHook = unsafe fn(cx: *mut JSContext, reason: JSVal);
pub type FlushRejectionsHook = unsafe fn(cx: *mut JSContext);
static UNCAUGHT_HOOK: OnceLock<UncaughtExceptionHook> = OnceLock::new();
static FLUSH_HOOK: OnceLock<FlushRejectionsHook> = OnceLock::new();
pub fn set_uncaught_hooks(uncaught: UncaughtExceptionHook, flush: FlushRejectionsHook) {
let _ = UNCAUGHT_HOOK.set(uncaught);
let _ = FLUSH_HOOK.set(flush);
}
thread_local! {
static JOB_IDS: RefCell<VecDeque<(usize, *mut mozjs::jsapi::JSObject)>> =
const { RefCell::new(VecDeque::new()) };
static QUEUE_PTR: RefCell<*mut mozjs::jsapi::JobQueue> = const { RefCell::new(ptr::null_mut()) };
}
fn job_prop_name(id: usize) -> CString {
CString::new(format!("__job_{}", id)).unwrap_or_default()
}
pub struct JobQueue;
impl JobQueue {
pub fn init(cx: &mozjs::context::JSContext) -> bool {
let traps = JobQueueTraps {
getHostDefinedData: Some(get_host_defined_data),
enqueuePromiseJob: Some(enqueue_job),
runJobs: Some(run_jobs),
empty: Some(is_empty),
pushNewInterruptQueue: None,
popInterruptQueue: None,
dropInterruptQueues: None,
};
let queue = unsafe { CreateJobQueue(&traps, ptr::null(), ptr::null_mut()) };
if queue.is_null() {
return false;
}
QUEUE_PTR.with(|p| {
*p.borrow_mut() = queue;
});
unsafe { SetJobQueue(cx, queue) }
true
}
pub fn drain(cx: &mut mozjs::context::JSContext) {
unsafe { RunJobs(cx) }
}
}
impl Drop for JobQueue {
fn drop(&mut self) {
QUEUE_PTR.with(|p| {
let ptr = *p.borrow();
if !ptr.is_null() {
unsafe { DeleteJobQueue(ptr) };
*p.borrow_mut() = ptr::null_mut();
}
});
}
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn enqueue_job(
_queue: *const c_void,
cx: *mut JSContext,
_promise: Handle<*mut JSObject>,
job: Handle<*mut JSObject>,
_allocation_site: Handle<*mut JSObject>,
_host_defined_data: Handle<*mut JSObject>,
) -> bool {
let job_obj = *job.ptr;
if job_obj.is_null() {
return true;
}
let id = JOB_COUNTER.fetch_add(1, Ordering::Relaxed);
let global = unsafe { CurrentGlobalOrNull(cx) };
if global.is_null() {
return true;
}
let prop = job_prop_name(id);
let mut wrapped_cx =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
rooted!(&in(wrapped_cx) let job_root = mozjs::jsval::ObjectValue(job_obj));
rooted!(&in(wrapped_cx) let global_root = global);
unsafe {
JS_DefineProperty(
cx,
global_root.handle().into(),
prop.as_ptr(),
job_root.handle().into(),
0,
);
}
JOB_IDS.with(|q| {
q.borrow_mut().push_back((id, global));
});
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn run_jobs(_queue: *const c_void, cx: *mut JSContext) {
loop {
let job_entry = JOB_IDS.with(|q| q.borrow_mut().pop_front());
let Some((id, global)) = job_entry else {
break;
};
if global.is_null() {
continue;
}
let prop = job_prop_name(id);
let mut wrapped_cx =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
let mut realm = AutoRealm::new(
&mut wrapped_cx,
::std::ptr::NonNull::new_unchecked(global),
);
let realm_cx: &mut mozjs::context::JSContext = &mut realm;
rooted!(&in(realm_cx) let global_root = global);
let mut job_val = UndefinedValue();
unsafe {
if !JS_GetProperty(
cx,
global_root.handle().into(),
prop.as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut job_val,
},
) {
JS_ClearPendingException(cx);
continue;
}
}
if !job_val.is_object() {
continue;
}
let mut rval = UndefinedValue();
rooted!(&in(realm_cx) let obj_root = global);
rooted!(&in(realm_cx) let fval_root = job_val);
let empty_args = HandleValueArray::empty();
let rval_handle = MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut rval,
};
unsafe {
let ok = JS_CallFunctionValue(
cx,
obj_root.handle().into(),
fval_root.handle().into(),
&empty_args,
rval_handle,
);
if !ok {
let mut exn = UndefinedValue();
JS_GetPendingException(
cx,
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut exn,
},
);
JS_ClearPendingException(cx);
rooted!(&in(realm_cx) let reason_root = exn);
if !exn.is_undefined() {
if let Some(&hook) = UNCAUGHT_HOOK.get() {
unsafe { hook(cx, exn) };
}
}
}
}
unsafe {
JS_DeleteProperty1(cx, global_root.handle().into(), prop.as_ptr());
}
}
if let Some(&hook) = FLUSH_HOOK.get() {
unsafe { hook(cx) };
}
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn get_host_defined_data(
_queue: *const c_void,
_cx: *mut JSContext,
data: MutableHandle<*mut JSObject>,
) -> bool {
data.set(ptr::null_mut());
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn is_empty(_queue: *const c_void) -> bool {
JOB_IDS.with(|q| q.borrow().is_empty())
}