//! Timer APIs: setTimeout, setInterval, clearTimeout, clearInterval, //! requestAnimationFrame, cancelAnimationFrame. //! //! Timers are stored in a thread-local registry. The VM drains due timers //! after each top-level execution (alongside microtasks). use std::cell::RefCell; use std::time::{Duration, Instant}; use crate::gc::GcRef; use crate::vm::{NativeContext, Value}; /// A pending timer (timeout, interval, or animation frame). struct Timer { id: u32, callback: GcRef, /// When this timer is due to fire. fire_at: Instant, /// For intervals: the repeat delay. `None` for one-shot timers. interval: Option, /// Whether this timer has been cancelled. cancelled: bool, } /// Thread-local timer state. struct TimerState { timers: Vec, next_id: u32, /// Epoch for `requestAnimationFrame` timestamps (millis since page load). epoch: Instant, } impl TimerState { fn new() -> Self { Self { timers: Vec::new(), next_id: 1, epoch: Instant::now(), } } } thread_local! { static TIMER_STATE: RefCell = RefCell::new(TimerState::new()); } /// Reset timer state (useful for tests to avoid leaking state). pub fn reset_timers() { TIMER_STATE.with(|s| { let mut state = s.borrow_mut(); state.timers.clear(); state.next_id = 1; state.epoch = Instant::now(); }); } /// Schedule a one-shot timer. Returns the timer ID. fn schedule_timeout(callback: GcRef, delay_ms: f64) -> u32 { let delay = Duration::from_millis(delay_ms.max(0.0) as u64); TIMER_STATE.with(|s| { let mut state = s.borrow_mut(); let id = state.next_id; state.next_id += 1; state.timers.push(Timer { id, callback, fire_at: Instant::now() + delay, interval: None, cancelled: false, }); id }) } /// Schedule a repeating interval timer. Returns the timer ID. fn schedule_interval(callback: GcRef, delay_ms: f64) -> u32 { let delay = Duration::from_millis(delay_ms.max(0.0).max(1.0) as u64); TIMER_STATE.with(|s| { let mut state = s.borrow_mut(); let id = state.next_id; state.next_id += 1; state.timers.push(Timer { id, callback, fire_at: Instant::now() + delay, interval: Some(delay), cancelled: false, }); id }) } /// Schedule an animation frame callback. Returns the ID. fn schedule_animation_frame(callback: GcRef) -> u32 { // requestAnimationFrame fires before the next repaint; for our purposes // treat it like setTimeout(cb, 0) — it fires on the next event loop tick. TIMER_STATE.with(|s| { let mut state = s.borrow_mut(); let id = state.next_id; state.next_id += 1; state.timers.push(Timer { id, callback, fire_at: Instant::now(), interval: None, cancelled: false, }); id }) } /// Cancel a timer by ID (works for timeouts, intervals, and animation frames). fn cancel_timer(id: u32) { TIMER_STATE.with(|s| { let mut state = s.borrow_mut(); if let Some(timer) = state.timers.iter_mut().find(|t| t.id == id) { timer.cancelled = true; } }); } /// Collect all GcRefs held by pending (non-cancelled) timers so the GC /// does not collect their callbacks. pub fn timer_gc_roots() -> Vec { TIMER_STATE.with(|s| { let state = s.borrow(); state .timers .iter() .filter(|t| !t.cancelled) .map(|t| t.callback) .collect() }) } /// A timer that is ready to fire. pub struct DueTimer { pub callback: GcRef, /// For `requestAnimationFrame`: timestamp in ms since epoch. pub raf_timestamp: Option, } /// Take all due timers from the queue. For intervals, reschedules the next /// occurrence. Removes cancelled and fired one-shot timers. pub fn take_due_timers() -> Vec { TIMER_STATE.with(|s| { let mut state = s.borrow_mut(); let now = Instant::now(); let epoch = state.epoch; let mut due = Vec::new(); for timer in &mut state.timers { if timer.cancelled { continue; } if timer.fire_at <= now { let raf_timestamp = if timer.interval.is_none() { // Could be a raf or a timeout; pass timestamp for raf. Some(now.duration_since(epoch).as_secs_f64() * 1000.0) } else { None }; due.push(DueTimer { callback: timer.callback, raf_timestamp, }); if let Some(delay) = timer.interval { // Reschedule interval. timer.fire_at = now + delay; } else { // One-shot: mark cancelled so it gets cleaned up. timer.cancelled = true; } } } // Remove cancelled timers. state.timers.retain(|t| !t.cancelled); due }) } /// Returns true if there are any pending (non-cancelled) timers. pub fn has_pending_timers() -> bool { TIMER_STATE.with(|s| { let state = s.borrow(); state.timers.iter().any(|t| !t.cancelled) }) } // ── Native function implementations ───────────────────────── use crate::vm::RuntimeError; /// `setTimeout(callback, delay)` — returns timer ID. pub fn set_timeout(args: &[Value], _ctx: &mut NativeContext) -> Result { let callback_ref = match args.first() { Some(Value::Function(r)) => *r, _ => return Ok(Value::Undefined), }; let delay = args.get(1).map(|v| v.to_number()).unwrap_or(0.0); let id = schedule_timeout(callback_ref, delay); Ok(Value::Number(id as f64)) } /// `clearTimeout(id)` — cancel a pending timeout. pub fn clear_timeout(args: &[Value], _ctx: &mut NativeContext) -> Result { if let Some(id) = args.first().map(|v| v.to_number() as u32) { cancel_timer(id); } Ok(Value::Undefined) } /// `setInterval(callback, delay)` — returns timer ID. pub fn set_interval(args: &[Value], _ctx: &mut NativeContext) -> Result { let callback_ref = match args.first() { Some(Value::Function(r)) => *r, _ => return Ok(Value::Undefined), }; let delay = args.get(1).map(|v| v.to_number()).unwrap_or(0.0); let id = schedule_interval(callback_ref, delay); Ok(Value::Number(id as f64)) } /// `clearInterval(id)` — cancel a repeating timer. pub fn clear_interval(args: &[Value], _ctx: &mut NativeContext) -> Result { if let Some(id) = args.first().map(|v| v.to_number() as u32) { cancel_timer(id); } Ok(Value::Undefined) } /// `requestAnimationFrame(callback)` — returns ID. pub fn request_animation_frame( args: &[Value], _ctx: &mut NativeContext, ) -> Result { let callback_ref = match args.first() { Some(Value::Function(r)) => *r, _ => return Ok(Value::Undefined), }; let id = schedule_animation_frame(callback_ref); Ok(Value::Number(id as f64)) } /// `cancelAnimationFrame(id)` — cancel pending animation frame. pub fn cancel_animation_frame( args: &[Value], _ctx: &mut NativeContext, ) -> Result { if let Some(id) = args.first().map(|v| v.to_number() as u32) { cancel_timer(id); } Ok(Value::Undefined) } // ── Tests ─────────────────────────────────────────────────── #[cfg(test)] mod tests { use super::*; use crate::compiler; use crate::parser::Parser; use crate::vm::{ConsoleOutput, Vm}; use std::cell::RefCell; use std::rc::Rc; struct CapturedConsole { log_messages: RefCell>, } impl CapturedConsole { fn new() -> Self { Self { log_messages: RefCell::new(Vec::new()), } } } impl ConsoleOutput for CapturedConsole { fn log(&self, message: &str) { self.log_messages.borrow_mut().push(message.to_string()); } fn error(&self, _message: &str) {} fn warn(&self, _message: &str) {} } struct RcConsole(Rc); impl ConsoleOutput for RcConsole { fn log(&self, message: &str) { self.0.log(message); } fn error(&self, message: &str) { self.0.error(message); } fn warn(&self, message: &str) { self.0.warn(message); } } /// Execute JS, pump the event loop, and return captured console output. fn eval_with_timers(source: &str, max_iterations: usize) -> Vec { reset_timers(); let console = Rc::new(CapturedConsole::new()); let program = Parser::parse(source).expect("parse failed"); let func = compiler::compile(&program).expect("compile failed"); let mut vm = Vm::new(); vm.set_console_output(Box::new(RcConsole(console.clone()))); vm.execute(&func).expect("execute failed"); vm.run_event_loop(max_iterations) .expect("event loop failed"); let result = console.log_messages.borrow().clone(); result } #[test] fn test_set_timeout_returns_id() { reset_timers(); let program = Parser::parse("var id = setTimeout(function(){}, 0); id").expect("parse failed"); let func = compiler::compile(&program).expect("compile failed"); let mut vm = Vm::new(); let result = vm.execute(&func).expect("execute failed"); match result { Value::Number(n) => assert!(n >= 1.0, "timer ID should be >= 1, got {n}"), other => panic!("expected Number, got {other:?}"), } } #[test] fn test_set_timeout_unique_ids() { reset_timers(); let source = r#" var id1 = setTimeout(function(){}, 0); var id2 = setTimeout(function(){}, 0); var id3 = setTimeout(function(){}, 0); id1 + ',' + id2 + ',' + id3 "#; let program = Parser::parse(source).expect("parse failed"); let func = compiler::compile(&program).expect("compile failed"); let mut vm = Vm::new(); let result = vm.execute(&func).expect("execute failed"); let s = result.to_js_string(&vm.gc); let ids: Vec = s.split(',').map(|x| x.parse().unwrap()).collect(); assert!( ids[0] < ids[1] && ids[1] < ids[2], "IDs should be monotonically increasing" ); } #[test] fn test_set_timeout_fires_callback() { let logs = eval_with_timers( r#"setTimeout(function() { console.log("fired"); }, 0);"#, 10, ); assert_eq!(logs, vec!["fired"]); } #[test] fn test_set_timeout_deferred() { // setTimeout(fn, 0) should NOT fire synchronously — only after the // current script finishes and the event loop is pumped. let logs = eval_with_timers( r#" var result = []; setTimeout(function() { result.push("timer"); console.log(result.join(",")); }, 0); result.push("sync"); "#, 10, ); assert_eq!(logs, vec!["sync,timer"]); } #[test] fn test_clear_timeout_prevents_execution() { let logs = eval_with_timers( r#" var id = setTimeout(function() { console.log("should not fire"); }, 0); clearTimeout(id); "#, 10, ); assert!(logs.is_empty(), "cleared timeout should not fire"); } #[test] fn test_set_interval_fires_multiple_times() { let logs = eval_with_timers( r#" var count = 0; var id = setInterval(function() { count++; console.log("tick " + count); if (count >= 3) clearInterval(id); }, 1); "#, 100, ); assert_eq!(logs, vec!["tick 1", "tick 2", "tick 3"]); } #[test] fn test_clear_interval_stops_repetition() { let logs = eval_with_timers( r#" var count = 0; var id = setInterval(function() { count++; console.log("tick"); }, 1); setTimeout(function() { clearInterval(id); }, 5); "#, 100, ); // Should have fired some ticks but not infinitely. assert!(!logs.is_empty(), "interval should have fired at least once"); assert!(logs.len() < 50, "interval should have been cleared"); } #[test] fn test_request_animation_frame_fires() { let logs = eval_with_timers( r#"requestAnimationFrame(function(ts) { console.log("raf " + typeof ts); });"#, 10, ); assert_eq!(logs, vec!["raf number"]); } #[test] fn test_cancel_animation_frame() { let logs = eval_with_timers( r#" var id = requestAnimationFrame(function() { console.log("should not fire"); }); cancelAnimationFrame(id); "#, 10, ); assert!(logs.is_empty(), "cancelled raf should not fire"); } #[test] fn test_set_timeout_with_delay() { // A timeout with a small delay should still fire when the event loop runs. let logs = eval_with_timers( r#"setTimeout(function() { console.log("delayed"); }, 10);"#, 100, ); assert_eq!(logs, vec!["delayed"]); } #[test] fn test_multiple_timeouts_ordering() { // Two zero-delay timeouts should fire in registration order. let logs = eval_with_timers( r#" setTimeout(function() { console.log("first"); }, 0); setTimeout(function() { console.log("second"); }, 0); "#, 10, ); assert_eq!(logs, vec!["first", "second"]); } #[test] fn test_set_timeout_non_function_arg() { // Passing a non-function should return undefined, no crash. reset_timers(); let program = Parser::parse("setTimeout(42, 0)").expect("parse failed"); let func = compiler::compile(&program).expect("compile failed"); let mut vm = Vm::new(); let result = vm.execute(&func).expect("execute failed"); assert!(matches!(result, Value::Undefined)); } #[test] fn test_clear_timeout_invalid_id() { // Clearing a non-existent ID should not crash. reset_timers(); let program = Parser::parse("clearTimeout(9999)").expect("parse failed"); let func = compiler::compile(&program).expect("compile failed"); let mut vm = Vm::new(); let result = vm.execute(&func).expect("execute failed"); assert!(matches!(result, Value::Undefined)); } #[test] fn test_timer_gc_roots_kept_alive() { // Timer callbacks should survive GC. let logs = eval_with_timers( r#" (function() { setTimeout(function() { console.log("survived gc"); }, 10); })(); "#, 100, ); assert_eq!(logs, vec!["survived gc"]); } #[test] fn test_timer_and_promise_interaction() { // Promise microtasks should drain between timer callbacks. let logs = eval_with_timers( r#" setTimeout(function() { console.log("timer"); Promise.resolve().then(function() { console.log("microtask after timer"); }); }, 0); "#, 10, ); assert_eq!(logs, vec!["timer", "microtask after timer"]); } }