use boa_engine::interop::JsRest;
use boa_engine::job::{NativeJob, TimeoutJob};
use boa_engine::object::builtins::JsFunction;
use boa_engine::value::{IntegerOrInfinity, Nullable};
use boa_engine::{
Context, Finalize, IntoJsFunctionCopied, JsData, JsResult, JsValue, Trace, js_error, js_string,
};
use boa_gc::{Gc, GcRefCell};
use std::collections::HashSet;
#[cfg(test)]
mod tests;
#[derive(Default, Trace, Finalize, JsData)]
struct IntervalInnerState {
active_map: HashSet<u32>,
next_id: u32,
}
impl IntervalInnerState {
fn from_context(context: &mut Context) -> Gc<GcRefCell<Self>> {
if !context.has_data::<Gc<GcRefCell<IntervalInnerState>>>() {
context.insert_data(Gc::new(GcRefCell::new(Self::default())));
}
context
.get_data::<Gc<GcRefCell<Self>>>()
.expect("Should have inserted.")
.clone()
}
#[inline]
fn is_interval_valid(&self, id: u32) -> bool {
self.active_map.contains(&id)
}
fn new_interval(&mut self) -> JsResult<u32> {
if self.next_id == u32::MAX {
return Err(js_error!(Error: "Interval ID overflow"));
}
self.next_id += 1;
self.active_map.insert(self.next_id);
Ok(self.next_id)
}
fn clear_interval(&mut self, id: u32) {
self.active_map.remove(&id);
}
}
#[allow(clippy::too_many_arguments)]
fn handle(
handler_map: Gc<GcRefCell<IntervalInnerState>>,
id: u32,
function_ref: JsFunction,
args: Vec<JsValue>,
reschedule: Option<u64>,
context: &mut Context,
) -> JsResult<JsValue> {
if !handler_map.borrow().is_interval_valid(id) {
return Ok(JsValue::undefined());
}
let result = function_ref.call(&JsValue::undefined(), &args, context);
if let Some(delay) = reschedule {
if handler_map.borrow().is_interval_valid(id) {
let job = TimeoutJob::recurring(
NativeJob::new(move |context| {
handle(handler_map, id, function_ref, args, reschedule, context)
}),
delay,
);
context.enqueue_job(job.into());
}
return result;
}
handler_map.borrow_mut().clear_interval(id);
result
}
pub fn set_timeout(
function_ref: Option<JsFunction>,
delay_in_msec: Option<JsValue>,
rest: JsRest<'_>,
context: &mut Context,
) -> JsResult<u32> {
let Some(function_ref) = function_ref else {
return Ok(0);
};
let handler_map = IntervalInnerState::from_context(context);
let id = handler_map.borrow_mut().new_interval()?;
let delay = delay_in_msec
.unwrap_or_default()
.to_integer_or_infinity(context)
.unwrap_or(IntegerOrInfinity::Integer(0));
let delay = u64::from(delay.clamp_finite(0, u32::MAX));
let rest = rest.to_vec();
let job = TimeoutJob::new(
NativeJob::new(move |context| handle(handler_map, id, function_ref, rest, None, context)),
delay,
);
context.enqueue_job(job.into());
Ok(id)
}
pub fn set_interval(
function_ref: Option<JsFunction>,
delay_in_msec: Option<JsValue>,
rest: JsRest<'_>,
context: &mut Context,
) -> JsResult<u32> {
let Some(function_ref) = function_ref else {
return Ok(0);
};
let handler_map = IntervalInnerState::from_context(context);
let id = handler_map.borrow_mut().new_interval()?;
let delay = delay_in_msec
.unwrap_or_default()
.to_integer_or_infinity(context)
.unwrap_or(IntegerOrInfinity::Integer(0));
let delay = u64::from(delay.clamp_finite(0, u32::MAX));
let rest = rest.to_vec();
let job = TimeoutJob::new(
NativeJob::new(move |context| {
handle(handler_map, id, function_ref, rest, Some(delay), context)
}),
delay,
);
context.enqueue_job(job.into());
Ok(id)
}
pub fn clear_timeout(id: Nullable<Option<u32>>, context: &mut Context) {
let Some(id) = id.flatten() else {
return;
};
let handler_map = IntervalInnerState::from_context(context);
handler_map.borrow_mut().clear_interval(id);
}
pub fn register(context: &mut Context) -> JsResult<()> {
register_functions(context)
}
pub fn register_functions(context: &mut Context) -> JsResult<()> {
let set_timeout_ = set_timeout.into_js_function_copied(context);
context.register_global_callable(js_string!("setTimeout"), 1, set_timeout_)?;
let set_interval_ = set_interval.into_js_function_copied(context);
context.register_global_callable(js_string!("setInterval"), 1, set_interval_)?;
let clear_timeout_ = clear_timeout.into_js_function_copied(context);
context.register_global_callable(js_string!("clearTimeout"), 1, clear_timeout_.clone())?;
context.register_global_callable(js_string!("clearInterval"), 1, clear_timeout_)?;
Ok(())
}