use ::std::cell::{Cell, RefCell};
use ::std::time::Duration;
use mozjs::jsapi::*;
use mozjs::jsval::{Int32Value, JSVal, ObjectValue, UndefinedValue};
use mozjs::rooted;
use mozjs::rust::wrappers2::JS_DefineFunction;
use crate::gc_store::{gc_store_get_ns, gc_store_insert_ns, gc_store_remove_ns};
thread_local! {
static NEXT_ID: Cell<u32> = const { Cell::new(1) };
static BAO_RUNTIME_LOOP: Cell<*mut bun_event_loop::MiniEventLoop::MiniEventLoop<'static>> =
const { Cell::new(::std::ptr::null_mut()) };
static CURRENT_CX: Cell<*mut JSContext> = const { Cell::new(::std::ptr::null_mut()) };
static BAO_REGISTRY: RefCell<BaoTimerRegistry> = RefCell::new(BaoTimerRegistry::new());
static NEXT_EPOCH: Cell<u32> = const { Cell::new(1) };
static CURRENT_FIRING_TIMER: Cell<u32> = const { Cell::new(0) };
static CLEARED_DURING_FIRE: Cell<bool> = const { Cell::new(false) };
}
pub unsafe fn register_current_cx(cx: *mut JSContext) {
CURRENT_CX.with(|cell| cell.set(cx));
}
#[inline]
pub fn current_cx() -> *mut JSContext {
CURRENT_CX.with(|cell| cell.get())
}
struct CxGuard;
impl CxGuard {
fn new() -> Self {
Self
}
}
impl Drop for CxGuard {
fn drop(&mut self) {
unsafe {
register_current_cx(::std::ptr::null_mut());
}
}
}
pub fn with_event_loop<F, R>(f: F) -> R
where
F: FnOnce(&mut bun_event_loop::MiniEventLoop::MiniEventLoop<'static>) -> R,
{
BAO_RUNTIME_LOOP.with(|cell| {
let mut ptr = cell.get();
if ptr.is_null() {
bao_uloop::force_link();
let boxed =
::std::boxed::Box::new(bun_event_loop::MiniEventLoop::MiniEventLoop::init());
ptr = bun_core::heap::into_raw(boxed);
cell.set(ptr);
bun_event_loop::ConcurrentWakeup::register_thread_loop(ptr);
}
let loop_: &mut bun_event_loop::MiniEventLoop::MiniEventLoop<'static> =
unsafe { &mut *ptr };
f(loop_)
})
}
pub fn init() {}
pub fn install_timer_globals(
cx: &mut mozjs::context::JSContext,
global: mozjs::rust::Handle<*mut JSObject>,
) {
init();
unsafe {
JS_DefineFunction(
cx,
global,
c"setTimeout".as_ptr(),
::std::option::Option::Some(set_timeout),
2,
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx,
global,
c"clearTimeout".as_ptr(),
::std::option::Option::Some(clear_timeout),
1,
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx,
global,
c"setInterval".as_ptr(),
::std::option::Option::Some(set_interval),
2,
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx,
global,
c"clearInterval".as_ptr(),
::std::option::Option::Some(clear_interval),
1,
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx,
global,
c"setImmediate".as_ptr(),
::std::option::Option::Some(set_immediate),
1,
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx,
global,
c"clearImmediate".as_ptr(),
::std::option::Option::Some(clear_timeout),
1,
JSPROP_ENUMERATE as u32,
);
}
}
fn wait_for_timer_deadline() {
if bun_core::util::mock_time::get().is_some() {
::std::thread::sleep(Duration::from_millis(1));
return;
}
let now = Timespec::now_allow_mocked_time();
let Some(deadline) = BAO_REGISTRY.with(|r| r.borrow().next_deadline()) else {
return;
};
if deadline.order(&now) != core::cmp::Ordering::Greater {
return;
}
let delta = deadline.duration(&now);
let delta_ns = delta.ns() as i64;
let start = ::std::time::Instant::now();
with_event_loop(|loop_| {
unsafe { (*loop_.loop_ptr()).tick_with_timeout(Some(&delta)) };
});
let elapsed_ns = start.elapsed().as_nanos() as i64;
if elapsed_ns < delta_ns {
let residual = (delta_ns - elapsed_ns).min(1_000_000);
::std::thread::sleep(Duration::from_nanos(residual as u64));
}
}
pub fn drain_and_check(cx: &mut mozjs::context::JSContext) -> bool {
if crate::should_exit() {
return false;
}
unsafe {
register_current_cx(cx.raw_cx());
}
let _cx_guard = CxGuard::new();
let has_http_before_tick = crate::node_http::has_active_servers();
let has_pending_before_tick = bao_has_pending_timers();
let has_pending_async_fetch = crate::fetch_async::has_pending();
if has_http_before_tick {
with_event_loop(|loop_| {
loop_.tick_without_idle(core::ptr::null_mut());
});
} else if has_pending_before_tick {
wait_for_timer_deadline();
}
let has_http = crate::node_http::has_active_servers();
let raw_cx = unsafe { cx.raw_cx() };
drain_bao_timers(raw_cx);
bao_engine::job_queue::JobQueue::drain(cx);
crate::web_api::ws_pump_all(raw_cx);
crate::node_fs::fs_watch_pump_all(raw_cx);
crate::node_cluster::cluster_pump_all(raw_cx);
crate::bun_api::spawn_watch_pump_all(raw_cx);
bao_engine::job_queue::JobQueue::drain(cx);
bao_has_pending_timers()
|| crate::node_http::has_active_servers()
|| crate::fetch_async::has_pending()
|| crate::web_api::ws_has_pending()
|| crate::node_fs::fs_watch_loop_alive()
|| crate::node_cluster::cluster_loop_alive()
|| crate::bun_api::spawn_watch_loop_alive()
}
pub unsafe fn drain_one_pass(raw_cx: *mut JSContext) -> bool {
if crate::should_exit() {
return false;
}
unsafe {
register_current_cx(raw_cx);
}
let _cx_guard = CxGuard::new();
let has_http = crate::node_http::has_active_servers();
if has_http {
with_event_loop(|loop_| {
loop_.tick_without_idle(core::ptr::null_mut());
});
} else if bao_has_pending_timers() {
wait_for_timer_deadline();
}
let fired = drain_bao_timers(raw_cx);
mozjs_sys::jsapi::js::RunJobs(raw_cx);
crate::web_api::ws_pump_all(raw_cx);
crate::node_fs::fs_watch_pump_all(raw_cx);
crate::node_cluster::cluster_pump_all(raw_cx);
crate::bun_api::spawn_watch_pump_all(raw_cx);
mozjs_sys::jsapi::js::RunJobs(raw_cx);
fired
}
pub fn has_pending_work() -> bool {
bao_has_pending_timers()
|| crate::node_http::has_active_servers()
|| crate::fetch_async::has_pending()
|| crate::node_fs::fs_watch_loop_alive()
}
pub unsafe fn fire_js_callback_raw(
raw_cx: *mut JSContext,
callback: *mut JSObject,
args: &[JSVal],
) {
unsafe {
let global = CurrentGlobalOrNull(raw_cx);
let global = if global.is_null() {
match bao_engine::context::thread_realm_global() {
::std::option::Option::Some(g) if !g.is_null() => g,
_ => return,
}
} else {
global
};
let cx_ref =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(raw_cx));
rooted!(&in(cx_ref) let global_root = global);
rooted!(&in(cx_ref) let fval_root = ObjectValue(callback));
let args_array = if args.is_empty() {
HandleValueArray::empty()
} else {
HandleValueArray {
length_: args.len(),
elements_: args.as_ptr(),
}
};
let mut rval = UndefinedValue();
let rval_handle = MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut rval,
};
let ok = JS_CallFunctionValue(
raw_cx,
global_root.handle().into(),
fval_root.handle().into(),
&args_array,
rval_handle,
);
if !ok {
let mut exn = UndefinedValue();
JS_GetPendingException(
raw_cx,
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut exn,
},
);
JS_ClearPendingException(raw_cx);
rooted!(&in(cx_ref) let reason_root = exn);
if !exn.is_undefined() {
crate::uncaught::route_uncaught_exception(raw_cx, exn);
}
}
}
}
pub fn has_pending_timers() -> bool {
BAO_REGISTRY.with(|r| !r.borrow().is_empty())
}
fn bao_has_pending_timers() -> bool {
has_pending_timers()
}
fn drain_bao_timers(raw_cx: *mut JSContext) -> bool {
let now_ts = Timespec::now_allow_mocked_time();
let mut fired = false;
loop {
let should_fire = BAO_REGISTRY.with(|r| {
let reg = r.borrow();
match reg.next_deadline() {
Some(dl) => dl.order(&now_ts) != core::cmp::Ordering::Greater,
None => false,
}
});
if !should_fire {
break;
}
let obj_box = BAO_REGISTRY.with(|r| {
let mut reg = r.borrow_mut();
let peeked = reg.heap.peek();
if peeked.is_null() {
return None;
}
let timeout = unsafe { BaoTimeoutObject::from_timer_ptr(peeked) };
let id = unsafe { (*timeout).timer_id };
reg.remove(id)
});
let Some(mut obj) = obj_box else {
break;
};
fired = true;
let firing_id = obj.timer_id;
CURRENT_FIRING_TIMER.with(|c| c.set(firing_id));
CLEARED_DURING_FIRE.with(|c| c.set(false));
unsafe {
obj.fire_js(raw_cx, &now_ts);
}
CURRENT_FIRING_TIMER.with(|c| c.set(0));
let cleared_during_fire = CLEARED_DURING_FIRE.with(|c| c.get());
if obj.interval.is_some() && !cleared_during_fire {
let interval = obj.interval.expect("checked Some above");
let interval_ms = interval.as_millis() as i64;
let mut next_ts = obj.event_loop_timer.next;
while next_ts.order(&now_ts) != core::cmp::Ordering::Greater {
next_ts = next_ts.add_ms(interval_ms);
}
obj.event_loop_timer.next = next_ts;
obj.event_loop_timer.state = TimerState::PENDING;
obj.epoch = obj.epoch.wrapping_add(1);
BAO_REGISTRY.with(|r| r.borrow_mut().insert(obj));
} else {
obj.cleanup_callback(raw_cx);
obj.event_loop_timer.state = TimerState::CANCELLED;
}
}
CURRENT_FIRING_TIMER.with(|c| c.set(0));
fired
}
pub fn schedule_raw(
cx: *mut JSContext,
callback: *mut JSObject,
delay_ms: u64,
repeating: bool,
_args: &[JSVal],
) -> u32 {
let id = NEXT_ID.with(|n| {
let val = n.get();
n.set(val + 1);
val
});
let interval = if repeating {
Some(Duration::from_millis(delay_ms.max(1)))
} else {
None
};
let effective_delay = if delay_ms == 0 && repeating {
1
} else {
delay_ms
};
let callback_key = format!("cb_{}", id);
if !cx.is_null() {
gc_store_insert_ns(cx, "timer", &callback_key, callback);
}
let mut bao_obj = Box::new(BaoTimeoutObject::new_paused());
bao_obj.timer_id = id;
bao_obj.event_loop_timer.next =
bun_core::Timespec::now_allow_mocked_time().add_ms(effective_delay as i64);
bao_obj.interval = interval;
bao_obj.callback_key = ::std::option::Option::Some(callback_key);
bao_obj.args = _args.to_vec();
bao_obj.epoch = NEXT_EPOCH.with(|c| {
let v = c.get();
c.set(v.wrapping_add(1));
v
});
BAO_REGISTRY.with(|r| r.borrow_mut().insert(bao_obj));
id
}
pub fn cancel_raw(id: u32) {
BAO_REGISTRY.with(|r| {
if let ::std::option::Option::Some(obj) = r.borrow_mut().remove(id) {
let cx = current_cx();
if !cx.is_null() {
obj.cleanup_callback(cx);
}
return;
}
});
let firing = CURRENT_FIRING_TIMER.with(|c| c.get());
if firing != 0 && firing == id {
CLEARED_DURING_FIRE.with(|c| c.set(true));
}
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn set_timeout(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
register_timer(cx, argc, vp, false)
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn set_interval(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
register_timer(cx, argc, vp, true)
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn clear_timeout(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
if argc > 0 {
let v = *args.get(0).ptr;
if v.is_int32() {
let id = v.to_int32() as u32;
let removed = BAO_REGISTRY.with(|r| {
r.borrow_mut().remove(id).map(|obj| {
obj.cleanup_callback(cx);
})
});
if removed.is_none() {
let firing = CURRENT_FIRING_TIMER.with(|c| c.get());
if firing != 0 && firing == id {
CLEARED_DURING_FIRE.with(|c| c.set(true));
}
}
}
}
args.rval().set(UndefinedValue());
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn clear_interval(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
clear_timeout(cx, argc, vp)
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn set_immediate(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
if argc == 0 || !(*args.get(0).ptr).is_object() {
args.rval().set(Int32Value(0));
return true;
}
let cb_val = *args.get(0).ptr;
let cx_ref = mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
rooted!(&in(cx_ref) let cb = cb_val.to_object());
let cb_args = if argc > 1 {
(1..argc).map(|i| *args.get(i).ptr).collect()
} else {
Vec::new()
};
let id = schedule_raw(cx, cb.get(), 0, false, &cb_args);
args.rval().set(Int32Value(id as i32));
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn register_timer(_cx: *mut JSContext, argc: u32, vp: *mut JSVal, repeating: bool) -> bool {
let args = CallArgs::from_vp(vp, argc);
if argc == 0 {
args.rval().set(Int32Value(0));
return true;
}
let first = *args.get(0).ptr;
if !first.is_object() {
args.rval().set(Int32Value(0));
return true;
}
let cx_ref = mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(_cx));
rooted!(&in(cx_ref) let callback = first.to_object());
let delay_ms = if argc > 1 {
let v = *args.get(1).ptr;
if v.is_int32() {
v.to_int32().max(0) as u64
} else if v.is_double() {
v.to_double().max(0.0) as u64
} else {
0
}
} else {
0
};
let extra_args: Vec<JSVal> = if argc > 2 {
(2..argc).map(|i| *args.get(i).ptr).collect()
} else {
Vec::new()
};
let id = schedule_raw(_cx, callback.get(), delay_ms, repeating, &extra_args);
args.rval().set(Int32Value(id as i32));
true
}
use bun_core::Timespec;
use bun_event_loop::EventLoopTimer::{EventLoopTimer, State as TimerState};
#[repr(C)]
pub struct BaoTimeoutObject {
pub event_loop_timer: EventLoopTimer,
pub epoch: u32,
pub callback_key: ::std::option::Option<String>,
pub args: ::std::vec::Vec<JSVal>,
pub interval: ::std::option::Option<Duration>,
pub timer_id: u32,
}
impl BaoTimeoutObject {
pub fn new_paused() -> Self {
Self {
event_loop_timer: EventLoopTimer::init_paused(
bun_event_loop::EventLoopTimer::Tag::TimeoutObject,
),
epoch: 0,
callback_key: ::std::option::Option::None,
args: ::std::vec::Vec::new(),
interval: ::std::option::Option::None,
timer_id: 0,
}
}
pub unsafe fn from_timer_ptr(t: *mut EventLoopTimer) -> *mut Self {
let offset = core::mem::offset_of!(Self, event_loop_timer);
(t as *mut u8).wrapping_sub(offset) as *mut Self
}
pub fn fire(&mut self, _now: &Timespec) {
self.event_loop_timer.state = TimerState::FIRED;
self.epoch = self.epoch.wrapping_add(1);
}
pub unsafe fn fire_js(&mut self, raw_cx: *mut JSContext, now: &Timespec) {
self.fire(now);
if let ::std::option::Option::Some(ref key) = self.callback_key {
if let ::std::option::Option::Some(cb) = gc_store_get_ns(raw_cx, "timer", key) {
if !cb.is_null() {
unsafe { fire_js_callback_raw(raw_cx, cb, &self.args) };
}
}
}
}
fn cleanup_callback(&self, cx: *mut JSContext) {
if let ::std::option::Option::Some(ref key) = self.callback_key {
gc_store_remove_ns(cx, "timer", key);
}
}
}
#[derive(::std::default::Default)]
pub struct BaoTimerHeapCtx;
impl bun_io::heap::HeapContext<EventLoopTimer> for BaoTimerHeapCtx {
unsafe fn less(&self, a: *mut EventLoopTimer, b: *mut EventLoopTimer) -> bool {
unsafe { EventLoopTimer::less((), &*a, &*b) }
}
}
pub type BaoTimerHeap = bun_io::heap::Intrusive<EventLoopTimer, BaoTimerHeapCtx>;
pub struct BaoTimerRegistry {
heap: BaoTimerHeap,
owned: ::std::collections::HashMap<u32, ::std::boxed::Box<BaoTimeoutObject>>,
}
#[allow(clippy::derivable_impls)]
impl ::std::default::Default for BaoTimerRegistry {
fn default() -> Self {
Self {
heap: ::std::default::Default::default(),
owned: ::std::default::Default::default(),
}
}
}
impl BaoTimerRegistry {
pub fn new() -> Self {
::std::default::Default::default()
}
pub fn len(&self) -> usize {
self.owned.len()
}
pub fn is_empty(&self) -> bool {
self.owned.is_empty()
}
pub fn insert(&mut self, mut obj: ::std::boxed::Box<BaoTimeoutObject>) -> u32 {
let id = obj.timer_id;
assert!(
!self.owned.contains_key(&id),
"duplicate timer_id {id} in BaoTimerRegistry"
);
let timer_ptr: *mut EventLoopTimer = &mut obj.event_loop_timer;
unsafe {
self.heap.insert(timer_ptr);
}
self.owned.insert(id, obj);
id
}
pub fn remove(
&mut self,
id: u32,
) -> ::std::option::Option<::std::boxed::Box<BaoTimeoutObject>> {
let mut obj = self.owned.remove(&id)?;
let timer_ptr: *mut EventLoopTimer = &mut obj.event_loop_timer;
unsafe {
self.heap.remove(timer_ptr);
}
::std::option::Option::Some(obj)
}
pub fn next_deadline(&self) -> ::std::option::Option<bun_core::Timespec> {
let ptr = self.heap.peek();
if ptr.is_null() {
return ::std::option::Option::None;
}
::std::option::Option::Some(unsafe { (*ptr).next })
}
}
#[cfg(test)]
mod bao_timeout_tests {
use super::*;
#[test]
fn bao_timeout_object_offset_zero() {
let obj = BaoTimeoutObject::new_paused();
let base = &obj as *const _ as usize;
let timer = &obj.event_loop_timer as *const _ as usize;
assert_eq!(timer - base, 0, "event_loop_timer must be at offset 0");
}
#[test]
fn bao_timeout_object_from_timer_ptr_roundtrip() {
let obj = Box::new(BaoTimeoutObject::new_paused());
let obj_ptr = Box::into_raw(obj);
let timer_ptr = unsafe { core::ptr::addr_of_mut!((*obj_ptr).event_loop_timer) };
let recovered = unsafe { BaoTimeoutObject::from_timer_ptr(timer_ptr) };
assert_eq!(recovered, obj_ptr, "from_timer_ptr must recover the parent");
unsafe {
drop(Box::from_raw(obj_ptr));
}
}
#[test]
fn bao_timeout_object_fire_transitions_state() {
let mut obj = BaoTimeoutObject::new_paused();
assert!(
obj.event_loop_timer.state == TimerState::PENDING,
"initial state is PENDING"
);
assert_eq!(obj.epoch, 0);
let now = Timespec {
sec: 1_700_000_000,
nsec: 0,
};
obj.fire(&now);
assert!(
obj.event_loop_timer.state == TimerState::FIRED,
"fire transitions to FIRED"
);
assert_eq!(
obj.epoch, 1,
"fire bumps epoch for stable re-queue ordering"
);
}
#[test]
fn bao_timeout_object_tag_is_timeout_object() {
let obj = BaoTimeoutObject::new_paused();
assert!(
obj.event_loop_timer.tag == bun_event_loop::EventLoopTimer::Tag::TimeoutObject,
"tag must be TimeoutObject for FFI dispatch",
);
}
#[test]
fn bao_timeout_object_new_paused_has_no_callback() {
let obj = BaoTimeoutObject::new_paused();
assert!(
obj.callback_key.is_none(),
"new_paused must start with no callback_key"
);
assert!(obj.args.is_empty(), "new_paused must start with empty args");
}
#[test]
fn bao_timeout_object_fire_js_no_callback_key_is_noop() {
let mut obj = BaoTimeoutObject::new_paused();
let now = Timespec {
sec: 1_700_000_000,
nsec: 0,
};
unsafe {
obj.fire_js(::std::ptr::null_mut(), &now);
}
assert!(
obj.event_loop_timer.state == TimerState::FIRED,
"fire_js transitions state even with no callback_key"
);
assert_eq!(obj.epoch, 1, "fire_js bumps epoch");
}
#[test]
fn bao_timeout_object_callback_key_field_roundtrip() {
let mut obj = BaoTimeoutObject::new_paused();
obj.callback_key = ::std::option::Option::Some("cb_42".to_string());
obj.args = vec![mozjs::jsval::Int32Value(42)];
assert_eq!(
obj.callback_key,
::std::option::Option::Some("cb_42".to_string()),
"callback_key stores string"
);
assert_eq!(obj.args.len(), 1, "args field stores JSVal vec");
assert_eq!(obj.args[0].to_int32(), 42, "JSVal roundtrips intact");
}
#[test]
fn with_event_loop_lazily_materializes_mini_event_loop() {
let ptr1 = with_event_loop(|loop_| loop_.loop_ptr() as usize);
let ptr2 = with_event_loop(|loop_| loop_.loop_ptr() as usize);
assert!(ptr1 != 0, "MiniEventLoop loop_ptr must be non-null");
assert_eq!(
ptr1, ptr2,
"with_event_loop must return the same loop on repeated calls"
);
}
#[test]
fn current_cx_roundtrip_via_thread_local() {
let sentinel: *mut JSContext = 0x12345678 as *mut JSContext;
unsafe {
register_current_cx(sentinel);
}
assert_eq!(
current_cx(),
sentinel,
"register_current_cx stores cx in thread_local"
);
unsafe {
register_current_cx(::std::ptr::null_mut());
}
assert!(
current_cx().is_null(),
"register_current_cx(null) clears the slot"
);
}
#[test]
fn bao_timeout_object_new_paused_has_no_interval() {
let obj = BaoTimeoutObject::new_paused();
assert!(
obj.interval.is_none(),
"new_paused must start with no interval (one-shot)"
);
assert_eq!(obj.timer_id, 0, "new_paused must start with timer_id 0");
}
#[test]
fn bao_timer_heap_ctx_default_compiles() {
let _ctx = BaoTimerHeapCtx::default();
let _heap: BaoTimerHeap = ::std::default::Default::default();
}
#[test]
fn bao_timer_heap_insert_then_peek_orders_by_deadline() {
let mut earlier = Box::new(BaoTimeoutObject::new_paused());
earlier.event_loop_timer.next = bun_core::Timespec { sec: 100, nsec: 0 };
let mut later = Box::new(BaoTimeoutObject::new_paused());
later.event_loop_timer.next = bun_core::Timespec { sec: 200, nsec: 0 };
let earlier_ptr = (&mut earlier.event_loop_timer) as *mut EventLoopTimer;
let later_ptr = (&mut later.event_loop_timer) as *mut EventLoopTimer;
let mut heap: BaoTimerHeap = ::std::default::Default::default();
unsafe {
heap.insert(later_ptr);
heap.insert(earlier_ptr);
}
let peeked = heap.peek();
assert_eq!(
peeked, earlier_ptr,
"heap.peek must return earliest deadline (Bun's less ordering)"
);
unsafe {
let _ = heap.delete_min();
let _ = heap.delete_min();
}
drop(earlier);
drop(later);
}
#[test]
fn bao_timer_registry_insert_and_len() {
let mut reg = BaoTimerRegistry::new();
assert!(reg.is_empty());
assert_eq!(reg.len(), 0);
let mut obj = Box::new(BaoTimeoutObject::new_paused());
obj.timer_id = 1;
obj.event_loop_timer.next = bun_core::Timespec { sec: 100, nsec: 0 };
let id = reg.insert(obj);
assert_eq!(id, 1);
assert_eq!(reg.len(), 1);
assert!(!reg.is_empty());
}
#[test]
fn bao_timer_registry_next_deadline_returns_min() {
let mut reg = BaoTimerRegistry::new();
for (id, sec) in [(1, 300), (2, 100), (3, 200)] {
let mut obj = Box::new(BaoTimeoutObject::new_paused());
obj.timer_id = id;
obj.event_loop_timer.next = bun_core::Timespec { sec, nsec: 0 };
reg.insert(obj);
}
let dl = reg.next_deadline().expect("heap non-empty");
assert_eq!(dl.sec, 100, "next_deadline must return earliest (sec=100)");
}
#[test]
fn bao_timer_registry_remove_clears_ownership() {
let mut reg = BaoTimerRegistry::new();
let mut obj = Box::new(BaoTimeoutObject::new_paused());
obj.timer_id = 42;
obj.event_loop_timer.next = bun_core::Timespec { sec: 500, nsec: 0 };
reg.insert(obj);
assert_eq!(reg.len(), 1);
let removed = reg.remove(42);
assert!(
removed.is_some(),
"remove returns Some(Box<..>) for known id"
);
assert_eq!(reg.len(), 0);
assert!(reg.is_empty());
assert!(
reg.next_deadline().is_none(),
"heap must be empty after remove"
);
let again = reg.remove(42);
assert!(again.is_none(), "remove returns None for unknown id");
}
#[test]
fn bao_timer_registry_insert_multiple_orders_by_deadline() {
let mut reg = BaoTimerRegistry::new();
let deadlines = [(1, 500), (2, 100), (3, 300), (4, 50), (5, 200)];
for (id, sec) in deadlines {
let mut obj = Box::new(BaoTimeoutObject::new_paused());
obj.timer_id = id;
obj.event_loop_timer.next = bun_core::Timespec { sec, nsec: 0 };
reg.insert(obj);
}
assert_eq!(reg.len(), 5);
let dl = reg.next_deadline().expect("heap non-empty");
assert_eq!(dl.sec, 50, "next_deadline must return earliest deadline");
}
#[test]
fn bao_timer_registry_remove_middle_preserves_heap_order() {
let mut reg = BaoTimerRegistry::new();
for (id, sec) in [(1, 100), (2, 200), (3, 300), (4, 400)] {
let mut obj = Box::new(BaoTimeoutObject::new_paused());
obj.timer_id = id;
obj.event_loop_timer.next = bun_core::Timespec { sec, nsec: 0 };
reg.insert(obj);
}
let removed = reg.remove(2);
assert!(removed.is_some());
assert_eq!(removed.unwrap().timer_id, 2);
assert_eq!(reg.len(), 3);
let dl = reg.next_deadline().expect("heap non-empty");
assert_eq!(dl.sec, 100);
}
#[test]
fn bao_timer_registry_remove_earliest_updates_next_deadline() {
let mut reg = BaoTimerRegistry::new();
for (id, sec) in [(10, 1000), (20, 100), (30, 500)] {
let mut obj = Box::new(BaoTimeoutObject::new_paused());
obj.timer_id = id;
obj.event_loop_timer.next = bun_core::Timespec { sec, nsec: 0 };
reg.insert(obj);
}
reg.remove(20);
let dl = reg.next_deadline().expect("heap non-empty");
assert_eq!(dl.sec, 500);
}
#[test]
fn bao_timer_registry_remove_all_makes_empty() {
let mut reg = BaoTimerRegistry::new();
let ids: Vec<u32> = (1..=10).collect();
for &id in &ids {
let mut obj = Box::new(BaoTimeoutObject::new_paused());
obj.timer_id = id;
obj.event_loop_timer.next = bun_core::Timespec {
sec: id as i64 * 100,
nsec: 0,
};
reg.insert(obj);
}
assert_eq!(reg.len(), 10);
for &id in &ids {
let removed = reg.remove(id);
assert!(removed.is_some(), "remove({id}) should succeed");
}
assert!(reg.is_empty());
assert!(reg.next_deadline().is_none());
}
#[test]
fn bao_timer_registry_insert_duplicate_panics() {
let mut reg = BaoTimerRegistry::new();
let mut obj1 = Box::new(BaoTimeoutObject::new_paused());
obj1.timer_id = 123;
obj1.event_loop_timer.next = bun_core::Timespec { sec: 100, nsec: 0 };
reg.insert(obj1);
let mut obj2 = Box::new(BaoTimeoutObject::new_paused());
obj2.timer_id = 123; obj2.event_loop_timer.next = bun_core::Timespec { sec: 200, nsec: 0 };
let result = ::std::panic::catch_unwind(::std::panic::AssertUnwindSafe(|| {
reg.insert(obj2);
}));
assert!(result.is_err(), "insert with duplicate timer_id must panic");
}
#[test]
fn bao_timer_registry_next_deadline_equal_deadlines_uses_epoch() {
let mut reg = BaoTimerRegistry::new();
let mut obj1 = Box::new(BaoTimeoutObject::new_paused());
obj1.timer_id = 1;
obj1.epoch = 10; obj1.event_loop_timer.next = bun_core::Timespec { sec: 100, nsec: 0 };
reg.insert(obj1);
let mut obj2 = Box::new(BaoTimeoutObject::new_paused());
obj2.timer_id = 2;
obj2.epoch = 20; obj2.event_loop_timer.next = bun_core::Timespec { sec: 100, nsec: 0 };
reg.insert(obj2);
let peeked = reg.heap.peek();
let recovered = unsafe { BaoTimeoutObject::from_timer_ptr(peeked) };
assert_eq!(
unsafe { (*recovered).timer_id },
1,
"earlier epoch should be at heap root"
);
}
#[test]
fn bao_timeout_object_epoch_wrapping_add() {
let mut obj = BaoTimeoutObject::new_paused();
obj.epoch = u32::MAX - 1;
obj.epoch = obj.epoch.wrapping_add(1);
assert_eq!(obj.epoch, u32::MAX);
obj.epoch = obj.epoch.wrapping_add(1);
assert_eq!(obj.epoch, 0, "epoch should wrap to 0 on overflow");
}
#[test]
fn bao_timeout_object_fire_bumps_epoch_multiple_times() {
let mut obj = BaoTimeoutObject::new_paused();
let now = Timespec {
sec: 1_700_000_000,
nsec: 0,
};
for i in 1..=5 {
obj.fire(&now);
assert_eq!(obj.epoch, i, "epoch should increment each fire");
}
}
#[test]
fn bao_timeout_object_interval_field_roundtrip() {
let mut obj = BaoTimeoutObject::new_paused();
assert!(obj.interval.is_none());
obj.interval = Some(Duration::from_millis(250));
assert_eq!(obj.interval, Some(Duration::from_millis(250)));
obj.interval = None;
assert!(obj.interval.is_none());
}
#[test]
fn bao_timeout_object_timer_id_field_roundtrip() {
let mut obj = BaoTimeoutObject::new_paused();
assert_eq!(obj.timer_id, 0);
obj.timer_id = 999_999;
assert_eq!(obj.timer_id, 999_999);
}
#[test]
fn bao_timeout_object_args_field_multiple_values() {
let mut obj = BaoTimeoutObject::new_paused();
obj.args = vec![
mozjs::jsval::Int32Value(1),
mozjs::jsval::Int32Value(2),
mozjs::jsval::Int32Value(3),
];
assert_eq!(obj.args.len(), 3);
assert_eq!(obj.args[0].to_int32(), 1);
assert_eq!(obj.args[1].to_int32(), 2);
assert_eq!(obj.args[2].to_int32(), 3);
}
#[test]
fn bao_timer_heap_ctx_is_zst() {
assert_eq!(
::std::mem::size_of::<BaoTimerHeapCtx>(),
0,
"BaoTimerHeapCtx must be ZST"
);
}
#[test]
fn bao_timer_heap_default_is_empty() {
let heap: BaoTimerHeap = ::std::default::Default::default();
assert!(heap.peek().is_null(), "default heap peek must return null");
}
#[test]
fn bao_timer_heap_insert_single_peek_returns_same() {
let mut obj = Box::new(BaoTimeoutObject::new_paused());
obj.event_loop_timer.next = bun_core::Timespec { sec: 100, nsec: 0 };
let obj_ptr = Box::into_raw(obj);
let timer_ptr = unsafe { core::ptr::addr_of_mut!((*obj_ptr).event_loop_timer) };
let mut heap: BaoTimerHeap = ::std::default::Default::default();
unsafe {
heap.insert(timer_ptr);
}
assert_eq!(
heap.peek(),
timer_ptr,
"peek must return the only inserted node"
);
unsafe {
let _ = heap.delete_min();
drop(Box::from_raw(obj_ptr));
}
}
#[test]
fn bao_timer_heap_delete_min_returns_null_when_empty() {
let mut heap: BaoTimerHeap = ::std::default::Default::default();
let result = unsafe { heap.delete_min() };
assert!(
result.is_null(),
"delete_min on empty heap must return null"
);
}
#[test]
fn bao_timer_heap_count_empty_is_zero() {
let heap: BaoTimerHeap = ::std::default::Default::default();
assert_eq!(unsafe { heap.count() }, 0);
}
#[test]
fn bao_timer_heap_count_single_is_one() {
let mut obj = Box::new(BaoTimeoutObject::new_paused());
obj.event_loop_timer.next = bun_core::Timespec { sec: 100, nsec: 0 };
let obj_ptr = Box::into_raw(obj);
let timer_ptr = unsafe { core::ptr::addr_of_mut!((*obj_ptr).event_loop_timer) };
let mut heap: BaoTimerHeap = ::std::default::Default::default();
unsafe {
heap.insert(timer_ptr);
}
assert_eq!(unsafe { heap.count() }, 1);
unsafe {
let _ = heap.delete_min();
drop(Box::from_raw(obj_ptr));
}
}
#[test]
fn bao_timer_heap_remove_middle_node() {
let mut objs: Vec<Box<BaoTimeoutObject>> = (0..3)
.map(|i| {
let mut obj = Box::new(BaoTimeoutObject::new_paused());
obj.event_loop_timer.next = bun_core::Timespec {
sec: (i + 1) as i64 * 100,
nsec: 0,
};
obj
})
.collect();
let ptrs: Vec<*mut EventLoopTimer> = objs
.iter_mut()
.map(|obj| &mut obj.event_loop_timer as *mut _)
.collect();
let mut heap: BaoTimerHeap = ::std::default::Default::default();
unsafe {
for &p in &ptrs {
heap.insert(p);
}
heap.remove(ptrs[1]);
assert_eq!(heap.count(), 2);
assert_eq!(heap.peek(), ptrs[0]);
let _ = heap.delete_min();
let _ = heap.delete_min();
}
drop(objs);
}
#[test]
fn schedule_raw_returns_monotonic_ids() {
let sentinel: *mut JSObject = 0xdeadbeef as *mut JSObject;
let null_cx: *mut JSContext = ::std::ptr::null_mut();
let id1 = schedule_raw(null_cx, sentinel, 100, false, &[]);
let id2 = schedule_raw(null_cx, sentinel, 200, false, &[]);
let id3 = schedule_raw(null_cx, sentinel, 300, false, &[]);
assert!(id2 > id1, "schedule_raw IDs must be monotonic");
assert!(id3 > id2, "schedule_raw IDs must be monotonic");
cancel_raw(id1);
cancel_raw(id2);
cancel_raw(id3);
}
#[test]
fn schedule_raw_one_shot_has_no_interval() {
let sentinel: *mut JSObject = 0xdeadbeef as *mut JSObject;
let null_cx: *mut JSContext = ::std::ptr::null_mut();
let id = schedule_raw(null_cx, sentinel, 100, false, &[]);
let interval = BAO_REGISTRY.with(|r| r.borrow().owned.get(&id).map(|obj| obj.interval));
assert_eq!(
interval,
Some(None),
"one-shot timer must have interval=None"
);
cancel_raw(id);
}
#[test]
fn schedule_raw_interval_has_interval_set() {
let sentinel: *mut JSObject = 0xdeadbeef as *mut JSObject;
let null_cx: *mut JSContext = ::std::ptr::null_mut();
let id = schedule_raw(null_cx, sentinel, 100, true, &[]);
let interval = BAO_REGISTRY.with(|r| r.borrow().owned.get(&id).map(|obj| obj.interval));
assert_eq!(
interval,
Some(Some(Duration::from_millis(100))),
"interval timer must have interval set"
);
cancel_raw(id);
}
#[test]
fn schedule_raw_zero_delay_interval_uses_minimum_one_ms() {
let sentinel: *mut JSObject = 0xdeadbeef as *mut JSObject;
let null_cx: *mut JSContext = ::std::ptr::null_mut();
let id = schedule_raw(null_cx, sentinel, 0, true, &[]);
let interval = BAO_REGISTRY.with(|r| r.borrow().owned.get(&id).map(|obj| obj.interval));
assert_eq!(
interval,
Some(Some(Duration::from_millis(1))),
"interval with delay=0 must use 1ms minimum"
);
cancel_raw(id);
}
#[test]
fn cancel_raw_removes_timer_from_registry() {
let sentinel: *mut JSObject = 0xdeadbeef as *mut JSObject;
let null_cx: *mut JSContext = ::std::ptr::null_mut();
let id = schedule_raw(null_cx, sentinel, 1000, false, &[]);
assert!(
BAO_REGISTRY.with(|r| r.borrow().owned.contains_key(&id)),
"timer should be in registry"
);
cancel_raw(id);
assert!(
!BAO_REGISTRY.with(|r| r.borrow().owned.contains_key(&id)),
"timer should be removed after cancel_raw"
);
}
#[test]
fn cancel_raw_unknown_id_is_noop() {
cancel_raw(999999); }
#[test]
fn next_id_thread_local_isolation() {
let initial = NEXT_ID.with(|n| n.get());
NEXT_ID.with(|n| n.set(initial + 100));
let updated = NEXT_ID.with(|n| n.get());
assert_eq!(updated, initial + 100);
NEXT_ID.with(|n| n.set(initial));
}
#[test]
fn next_epoch_thread_local_isolation() {
let initial = NEXT_EPOCH.with(|n| n.get());
NEXT_EPOCH.with(|n| n.set(initial + 50));
let updated = NEXT_EPOCH.with(|n| n.get());
assert_eq!(updated, initial + 50);
NEXT_EPOCH.with(|n| n.set(initial));
}
#[test]
fn next_epoch_wrapping_behavior() {
let initial = NEXT_EPOCH.with(|n| n.get());
NEXT_EPOCH.with(|n| n.set(u32::MAX));
let next = NEXT_EPOCH.with(|n| {
let v = n.get();
n.set(v.wrapping_add(1));
n.get()
});
assert_eq!(next, 0, "NEXT_EPOCH should wrap to 0");
NEXT_EPOCH.with(|n| n.set(initial));
}
#[test]
fn with_event_loop_returns_same_instance_on_multiple_calls() {
let ptr1 = with_event_loop(|loop_| loop_.loop_ptr() as usize);
let ptr2 = with_event_loop(|loop_| loop_.loop_ptr() as usize);
let ptr3 = with_event_loop(|loop_| loop_.loop_ptr() as usize);
assert_eq!(ptr1, ptr2);
assert_eq!(ptr2, ptr3);
}
#[test]
fn current_cx_initially_null() {
unsafe {
register_current_cx(::std::ptr::null_mut());
}
assert!(
current_cx().is_null(),
"current_cx should be null after clearing"
);
}
#[test]
fn register_current_cx_overwrites_previous() {
let sentinel1: *mut JSContext = 0x11111111 as *mut JSContext;
let sentinel2: *mut JSContext = 0x22222222 as *mut JSContext;
unsafe {
register_current_cx(sentinel1);
assert_eq!(current_cx(), sentinel1);
register_current_cx(sentinel2);
assert_eq!(current_cx(), sentinel2);
register_current_cx(::std::ptr::null_mut());
}
}
#[test]
fn bao_timer_registry_default_is_empty() {
let reg = BaoTimerRegistry::default();
assert!(reg.is_empty());
assert_eq!(reg.len(), 0);
assert!(reg.next_deadline().is_none());
}
#[test]
fn bao_timeout_object_event_loop_timer_state_initial() {
let obj = BaoTimeoutObject::new_paused();
assert!(
obj.event_loop_timer.state == TimerState::PENDING,
"initial state should be PENDING"
);
}
#[test]
fn bao_timeout_object_event_loop_timer_next_initial() {
let obj = BaoTimeoutObject::new_paused();
assert_eq!(obj.event_loop_timer.next.sec, 0);
assert_eq!(obj.event_loop_timer.next.nsec, 0);
}
#[test]
fn bao_timeout_object_from_timer_ptr_null_is_unsound() {
let result = unsafe { BaoTimeoutObject::from_timer_ptr(::std::ptr::null_mut()) };
assert_eq!(
result as usize, 0,
"from_timer_ptr(null) should return null when offset is 0"
);
}
#[test]
fn bao_timer_registry_insert_returns_timer_id() {
let mut reg = BaoTimerRegistry::new();
let mut obj = Box::new(BaoTimeoutObject::new_paused());
obj.timer_id = 42;
obj.event_loop_timer.next = bun_core::Timespec { sec: 100, nsec: 0 };
let id = reg.insert(obj);
assert_eq!(id, 42);
}
#[test]
fn bao_timer_registry_remove_returns_correct_object() {
let mut reg = BaoTimerRegistry::new();
let mut obj = Box::new(BaoTimeoutObject::new_paused());
obj.timer_id = 777;
obj.epoch = 12345;
obj.event_loop_timer.next = bun_core::Timespec { sec: 100, nsec: 0 };
reg.insert(obj);
let removed = reg.remove(777).expect("should remove");
assert_eq!(removed.timer_id, 777);
assert_eq!(removed.epoch, 12345);
}
#[test]
fn bao_timer_heap_insert_out_of_order_still_orders() {
let mut reg = BaoTimerRegistry::new();
for (id, sec) in [(1, 500), (2, 400), (3, 300), (4, 200), (5, 100)] {
let mut obj = Box::new(BaoTimeoutObject::new_paused());
obj.timer_id = id;
obj.event_loop_timer.next = bun_core::Timespec { sec, nsec: 0 };
reg.insert(obj);
}
let dl = reg.next_deadline().expect("non-empty");
assert_eq!(dl.sec, 100);
}
#[test]
fn bao_timer_registry_insert_zero_timer_id() {
let mut reg = BaoTimerRegistry::new();
let mut obj = Box::new(BaoTimeoutObject::new_paused());
obj.timer_id = 0;
obj.event_loop_timer.next = bun_core::Timespec { sec: 100, nsec: 0 };
let id = reg.insert(obj);
assert_eq!(id, 0);
assert!(reg.remove(0).is_some());
}
#[test]
fn bao_timeout_object_fire_does_not_change_deadline() {
let mut obj = BaoTimeoutObject::new_paused();
obj.event_loop_timer.next = bun_core::Timespec {
sec: 12345,
nsec: 67890,
};
let now = Timespec {
sec: 1_700_000_000,
nsec: 0,
};
obj.fire(&now);
assert_eq!(obj.event_loop_timer.next.sec, 12345);
assert_eq!(obj.event_loop_timer.next.nsec, 67890);
}
#[test]
fn bao_timeout_object_fire_js_none_callback_key_skips_dispatch_with_null_cx() {
let mut obj = BaoTimeoutObject::new_paused();
let now = Timespec {
sec: 1_700_000_000,
nsec: 0,
};
unsafe {
obj.fire_js(::std::ptr::null_mut(), &now);
}
assert!(
obj.event_loop_timer.state == TimerState::FIRED,
"fire_js transitions state with None callback_key"
);
assert_eq!(obj.epoch, 1);
}
#[test]
fn has_pending_timers_reflects_registry_state() {
let ids: Vec<u32> = BAO_REGISTRY.with(|r| r.borrow().owned.keys().copied().collect());
for id in ids {
cancel_raw(id);
}
assert!(!has_pending_timers(), "should be empty after clearing");
let sentinel: *mut JSObject = 0xdeadbeef as *mut JSObject;
let null_cx: *mut JSContext = ::std::ptr::null_mut();
let id = schedule_raw(null_cx, sentinel, 1000, false, &[]);
assert!(has_pending_timers(), "should have pending timer");
cancel_raw(id);
assert!(!has_pending_timers(), "should be empty after cancel");
}
#[test]
fn cancel_raw_during_fire_latches_cleared_flag_for_matching_id() {
let sentinel: *mut JSObject = 0xdeadbeef as *mut JSObject;
let null_cx: *mut JSContext = ::std::ptr::null_mut();
let id = schedule_raw(null_cx, sentinel, 1000, true, &[]); let _obj = BAO_REGISTRY.with(|r| r.borrow_mut().remove(id));
assert!(!BAO_REGISTRY.with(|r| r.borrow().owned.contains_key(&id)));
CURRENT_FIRING_TIMER.with(|c| c.set(id));
CLEARED_DURING_FIRE.with(|c| c.set(false));
cancel_raw(id);
assert!(
CLEARED_DURING_FIRE.with(|c| c.get()),
"cancel_raw during fire of matching id must set CLEARED_DURING_FIRE"
);
CURRENT_FIRING_TIMER.with(|c| c.set(0));
CLEARED_DURING_FIRE.with(|c| c.set(false));
}
#[test]
fn cancel_raw_during_fire_ignores_non_matching_id() {
let sentinel: *mut JSObject = 0xdeadbeef as *mut JSObject;
let null_cx: *mut JSContext = ::std::ptr::null_mut();
let firing_id = schedule_raw(null_cx, sentinel, 1000, true, &[]);
let other_id = schedule_raw(null_cx, sentinel, 1000, true, &[]);
let _o = BAO_REGISTRY.with(|r| r.borrow_mut().remove(firing_id));
CURRENT_FIRING_TIMER.with(|c| c.set(firing_id));
CLEARED_DURING_FIRE.with(|c| c.set(false));
cancel_raw(other_id);
assert!(
!CLEARED_DURING_FIRE.with(|c| c.get()),
"cancel of non-firing id must NOT latch CLEARED_DURING_FIRE"
);
CURRENT_FIRING_TIMER.with(|c| c.set(0));
}
#[test]
fn cancel_raw_during_fire_with_no_firing_timer_is_noop() {
CLEARED_DURING_FIRE.with(|c| c.set(false));
CURRENT_FIRING_TIMER.with(|c| c.set(0));
cancel_raw(95123); assert!(
!CLEARED_DURING_FIRE.with(|c| c.get()),
"cancel with no firing timer must not latch flag"
);
}
}