use crate::nbexec::{ExecError, Interp};
pub fn install(interp: &mut Interp<'_>) {
#[cfg(feature = "std")]
imp::install(interp);
#[cfg(not(feature = "std"))]
let _ = interp;
}
pub fn run_event_loop(interp: &mut Interp<'_>) -> Result<(), ExecError> {
#[cfg(feature = "std")]
{
imp::run_event_loop(interp)
}
#[cfg(not(feature = "std"))]
{
let _ = interp;
Ok(())
}
}
#[cfg(feature = "std")]
mod imp {
use super::{ExecError, Interp};
use crate::NanBox;
use crate::nbexec::Ctx;
use crate::parser::Parser;
use alloc::collections::VecDeque;
use alloc::vec::Vec;
use core::cell::RefCell;
const SAFETY_CAP: u64 = 50_000_000;
struct TimerEntry {
id: u64,
due: f64,
seq: u64,
cb: u32,
args: Vec<u32>,
period: Option<f64>,
}
struct Tick {
cb: u32,
args: Vec<u32>,
}
#[derive(Default)]
struct TimerStore {
next_id: u64,
seq: u64,
clock: f64,
timers: Vec<TimerEntry>,
ticks: VecDeque<Tick>,
}
std::thread_local! {
static STORE: RefCell<TimerStore> = RefCell::new(TimerStore::default());
}
const PRELUDE: &str = r#"
(function () {
var g = globalThis;
// process.nextTick — additive: extend an existing `process`, never clobber it.
var proc = g.process;
if (!proc || typeof proc !== 'object') { proc = {}; g.process = proc; }
proc.nextTick = g.__kataan_next_tick;
function makeError(msg, name) {
var e = new Error(msg);
e.name = name;
return e;
}
class AbortSignal {
constructor() {
this.aborted = false;
this.reason = undefined;
this.onabort = null;
this._listeners = [];
}
addEventListener(type, cb) {
if (type === 'abort' && typeof cb === 'function') this._listeners.push(cb);
}
removeEventListener(type, cb) {
if (type === 'abort') {
var i = this._listeners.indexOf(cb);
if (i >= 0) this._listeners.splice(i, 1);
}
}
dispatchEvent(ev) { return true; }
throwIfAborted() { if (this.aborted) throw this.reason; }
_signalAbort(reason) {
if (this.aborted) return;
this.aborted = true;
this.reason = (reason !== undefined)
? reason
: makeError('This operation was aborted', 'AbortError');
var ev = { type: 'abort', target: this, currentTarget: this };
if (typeof this.onabort === 'function') { try { this.onabort(ev); } catch (e) {} }
var ls = this._listeners.slice();
for (var i = 0; i < ls.length; i++) { try { ls[i].call(this, ev); } catch (e) {} }
}
static abort(reason) {
var s = new AbortSignal();
s._signalAbort((reason !== undefined)
? reason
: makeError('This operation was aborted', 'AbortError'));
return s;
}
static timeout(ms) {
var s = new AbortSignal();
g.setTimeout(function () {
s._signalAbort(makeError('The operation was aborted due to timeout', 'TimeoutError'));
}, ms);
return s;
}
}
class AbortController {
constructor() { this.signal = new AbortSignal(); }
abort(reason) { this.signal._signalAbort(reason); }
}
g.AbortSignal = AbortSignal;
g.AbortController = AbortController;
try { delete g.__kataan_next_tick; } catch (e) {}
})();
"#;
pub(super) fn install(interp: &mut Interp<'_>) {
STORE.with(|s| *s.borrow_mut() = TimerStore::default());
interp.register_global_fn("setTimeout", 1, |cx, _this, args| {
schedule_timer(cx, args, false)
});
interp.register_global_fn("setInterval", 1, |cx, _this, args| {
schedule_timer(cx, args, true)
});
interp.register_global_fn("setImmediate", 1, |cx, _this, args| {
schedule_immediate(cx, args)
});
interp.register_global_fn("clearTimeout", 1, |cx, _this, args| clear_timer(cx, args));
interp.register_global_fn("clearInterval", 1, |cx, _this, args| clear_timer(cx, args));
interp.register_global_fn("clearImmediate", 1, |cx, _this, args| clear_timer(cx, args));
interp.register_global_fn("queueMicrotask", 1, |cx, _this, args| {
queue_microtask(cx, args)
});
interp.register_global_fn("__kataan_next_tick", 1, |cx, _this, args| {
next_tick(cx, args)
});
let boxed = alloc::boxed::Box::new(
Parser::parse_program(PRELUDE).expect("kataan timers prelude must parse"),
);
let leaked = alloc::boxed::Box::leak(boxed);
interp
.run(leaked)
.expect("kataan timers prelude must evaluate");
}
fn schedule_timer(
cx: &mut Ctx<'_, '_>,
args: &[NanBox],
is_interval: bool,
) -> Result<NanBox, NanBox> {
let cb = args.first().copied().unwrap_or_else(|| cx.undefined());
let delay = match args.get(1).copied() {
Some(v) => cx.to_number(v)?,
None => 0.0,
};
let delay = if delay.is_finite() && delay > 0.0 {
delay
} else {
0.0
};
let period = if is_interval {
Some(delay.max(1.0))
} else {
None
};
let extra: Vec<u32> = args.iter().skip(2).map(|v| cx.persist(*v)).collect();
let cb_idx = cx.persist(cb);
let id = STORE.with(|s| {
let mut s = s.borrow_mut();
s.next_id += 1;
let id = s.next_id;
let seq = s.seq;
s.seq += 1;
let due = s.clock + delay;
s.timers.push(TimerEntry {
id,
due,
seq,
cb: cb_idx,
args: extra,
period,
});
id
});
Ok(cx.number(id as f64))
}
fn schedule_immediate(cx: &mut Ctx<'_, '_>, args: &[NanBox]) -> Result<NanBox, NanBox> {
let cb = args.first().copied().unwrap_or_else(|| cx.undefined());
let extra: Vec<u32> = args.iter().skip(1).map(|v| cx.persist(*v)).collect();
let cb_idx = cx.persist(cb);
let id = STORE.with(|s| {
let mut s = s.borrow_mut();
s.next_id += 1;
let id = s.next_id;
let seq = s.seq;
s.seq += 1;
let due = s.clock;
s.timers.push(TimerEntry {
id,
due,
seq,
cb: cb_idx,
args: extra,
period: None,
});
id
});
Ok(cx.number(id as f64))
}
fn clear_timer(cx: &mut Ctx<'_, '_>, args: &[NanBox]) -> Result<NanBox, NanBox> {
let Some(id) = args.first().and_then(|v| v.as_number()) else {
return Ok(cx.undefined());
};
let removed = STORE.with(|s| {
let mut s = s.borrow_mut();
s.timers
.iter()
.position(|t| (t.id as f64) == id)
.map(|pos| s.timers.remove(pos))
});
if let Some(t) = removed {
cx.release_persistent(t.cb);
for a in t.args {
cx.release_persistent(a);
}
}
Ok(cx.undefined())
}
fn queue_microtask(cx: &mut Ctx<'_, '_>, args: &[NanBox]) -> Result<NanBox, NanBox> {
let cb = args.first().copied().unwrap_or_else(|| cx.undefined());
if !cx.is_callable(cb) {
return Err(cx.type_error("queueMicrotask requires a callable callback"));
}
let undef = cx.undefined();
let promise = cx.resolved_promise(undef);
let then = cx.get(promise, "then")?;
cx.call(then, promise, &[cb])?;
Ok(cx.undefined())
}
fn next_tick(cx: &mut Ctx<'_, '_>, args: &[NanBox]) -> Result<NanBox, NanBox> {
let cb = args.first().copied().unwrap_or_else(|| cx.undefined());
if !cx.is_callable(cb) {
return Err(cx.type_error("process.nextTick requires a callable callback"));
}
let extra: Vec<u32> = args.iter().skip(1).map(|v| cx.persist(*v)).collect();
let cb_idx = cx.persist(cb);
STORE.with(|s| {
s.borrow_mut().ticks.push_back(Tick {
cb: cb_idx,
args: extra,
});
});
Ok(cx.undefined())
}
pub(super) fn run_event_loop(interp: &mut Interp<'_>) -> Result<(), ExecError> {
let mut budget = 0u64;
loop {
drain_jobs(interp)?;
let Some(idx) = STORE.with(|s| {
let s = s.borrow();
s.timers
.iter()
.enumerate()
.min_by(|(_, a), (_, b)| a.due.total_cmp(&b.due).then(a.seq.cmp(&b.seq)))
.map(|(i, _)| i)
}) else {
break;
};
let (cb, arg_idxs, one_shot) = STORE.with(|s| {
let mut s = s.borrow_mut();
let due = s.timers[idx].due;
if s.clock < due {
s.clock = due;
}
let cb = s.timers[idx].cb;
let arg_idxs = s.timers[idx].args.clone();
match s.timers[idx].period {
Some(period) => {
let seq = s.seq;
s.seq += 1;
s.timers[idx].due = due + period;
s.timers[idx].seq = seq;
(cb, arg_idxs, false)
}
None => {
let entry = s.timers.remove(idx);
(entry.cb, entry.args, true)
}
}
});
let callee = interp.persistent(cb).unwrap_or_else(NanBox::undefined);
let call_args: Vec<NanBox> = arg_idxs
.iter()
.map(|i| interp.persistent(*i).unwrap_or_else(NanBox::undefined))
.collect();
let outcome = interp.call_with_this(callee, NanBox::undefined(), &call_args);
if one_shot {
interp.release_persistent(cb);
for i in &arg_idxs {
interp.release_persistent(*i);
}
}
outcome?;
budget += 1;
if budget >= SAFETY_CAP {
break;
}
}
Ok(())
}
fn drain_jobs(interp: &mut Interp<'_>) -> Result<(), ExecError> {
loop {
while let Some(tick) = STORE.with(|s| s.borrow_mut().ticks.pop_front()) {
let callee = interp.persistent(tick.cb).unwrap_or_else(NanBox::undefined);
let call_args: Vec<NanBox> = tick
.args
.iter()
.map(|i| interp.persistent(*i).unwrap_or_else(NanBox::undefined))
.collect();
let outcome = interp.call_with_this(callee, NanBox::undefined(), &call_args);
interp.release_persistent(tick.cb);
for i in &tick.args {
interp.release_persistent(*i);
}
outcome?;
}
interp.drain_microtasks()?;
if STORE.with(|s| s.borrow().ticks.is_empty()) {
break;
}
}
Ok(())
}
}