we (web engine): Experimental web browser project to understand the limits of Claude
at encoding-sniffing 519 lines 16 kB view raw
1//! Timer APIs: setTimeout, setInterval, clearTimeout, clearInterval, 2//! requestAnimationFrame, cancelAnimationFrame. 3//! 4//! Timers are stored in a thread-local registry. The VM drains due timers 5//! after each top-level execution (alongside microtasks). 6 7use std::cell::RefCell; 8use std::time::{Duration, Instant}; 9 10use crate::gc::GcRef; 11use crate::vm::{NativeContext, Value}; 12 13/// A pending timer (timeout, interval, or animation frame). 14struct Timer { 15 id: u32, 16 callback: GcRef, 17 /// When this timer is due to fire. 18 fire_at: Instant, 19 /// For intervals: the repeat delay. `None` for one-shot timers. 20 interval: Option<Duration>, 21 /// Whether this timer has been cancelled. 22 cancelled: bool, 23} 24 25/// Thread-local timer state. 26struct TimerState { 27 timers: Vec<Timer>, 28 next_id: u32, 29 /// Epoch for `requestAnimationFrame` timestamps (millis since page load). 30 epoch: Instant, 31} 32 33impl TimerState { 34 fn new() -> Self { 35 Self { 36 timers: Vec::new(), 37 next_id: 1, 38 epoch: Instant::now(), 39 } 40 } 41} 42 43thread_local! { 44 static TIMER_STATE: RefCell<TimerState> = RefCell::new(TimerState::new()); 45} 46 47/// Reset timer state (useful for tests to avoid leaking state). 48pub fn reset_timers() { 49 TIMER_STATE.with(|s| { 50 let mut state = s.borrow_mut(); 51 state.timers.clear(); 52 state.next_id = 1; 53 state.epoch = Instant::now(); 54 }); 55} 56 57/// Schedule a one-shot timer. Returns the timer ID. 58fn schedule_timeout(callback: GcRef, delay_ms: f64) -> u32 { 59 let delay = Duration::from_millis(delay_ms.max(0.0) as u64); 60 TIMER_STATE.with(|s| { 61 let mut state = s.borrow_mut(); 62 let id = state.next_id; 63 state.next_id += 1; 64 state.timers.push(Timer { 65 id, 66 callback, 67 fire_at: Instant::now() + delay, 68 interval: None, 69 cancelled: false, 70 }); 71 id 72 }) 73} 74 75/// Schedule a repeating interval timer. Returns the timer ID. 76fn schedule_interval(callback: GcRef, delay_ms: f64) -> u32 { 77 let delay = Duration::from_millis(delay_ms.max(0.0).max(1.0) as u64); 78 TIMER_STATE.with(|s| { 79 let mut state = s.borrow_mut(); 80 let id = state.next_id; 81 state.next_id += 1; 82 state.timers.push(Timer { 83 id, 84 callback, 85 fire_at: Instant::now() + delay, 86 interval: Some(delay), 87 cancelled: false, 88 }); 89 id 90 }) 91} 92 93/// Schedule an animation frame callback. Returns the ID. 94fn schedule_animation_frame(callback: GcRef) -> u32 { 95 // requestAnimationFrame fires before the next repaint; for our purposes 96 // treat it like setTimeout(cb, 0) — it fires on the next event loop tick. 97 TIMER_STATE.with(|s| { 98 let mut state = s.borrow_mut(); 99 let id = state.next_id; 100 state.next_id += 1; 101 state.timers.push(Timer { 102 id, 103 callback, 104 fire_at: Instant::now(), 105 interval: None, 106 cancelled: false, 107 }); 108 id 109 }) 110} 111 112/// Cancel a timer by ID (works for timeouts, intervals, and animation frames). 113fn cancel_timer(id: u32) { 114 TIMER_STATE.with(|s| { 115 let mut state = s.borrow_mut(); 116 if let Some(timer) = state.timers.iter_mut().find(|t| t.id == id) { 117 timer.cancelled = true; 118 } 119 }); 120} 121 122/// Collect all GcRefs held by pending (non-cancelled) timers so the GC 123/// does not collect their callbacks. 124pub fn timer_gc_roots() -> Vec<GcRef> { 125 TIMER_STATE.with(|s| { 126 let state = s.borrow(); 127 state 128 .timers 129 .iter() 130 .filter(|t| !t.cancelled) 131 .map(|t| t.callback) 132 .collect() 133 }) 134} 135 136/// A timer that is ready to fire. 137pub struct DueTimer { 138 pub callback: GcRef, 139 /// For `requestAnimationFrame`: timestamp in ms since epoch. 140 pub raf_timestamp: Option<f64>, 141} 142 143/// Take all due timers from the queue. For intervals, reschedules the next 144/// occurrence. Removes cancelled and fired one-shot timers. 145pub fn take_due_timers() -> Vec<DueTimer> { 146 TIMER_STATE.with(|s| { 147 let mut state = s.borrow_mut(); 148 let now = Instant::now(); 149 let epoch = state.epoch; 150 let mut due = Vec::new(); 151 152 for timer in &mut state.timers { 153 if timer.cancelled { 154 continue; 155 } 156 if timer.fire_at <= now { 157 let raf_timestamp = if timer.interval.is_none() { 158 // Could be a raf or a timeout; pass timestamp for raf. 159 Some(now.duration_since(epoch).as_secs_f64() * 1000.0) 160 } else { 161 None 162 }; 163 due.push(DueTimer { 164 callback: timer.callback, 165 raf_timestamp, 166 }); 167 if let Some(delay) = timer.interval { 168 // Reschedule interval. 169 timer.fire_at = now + delay; 170 } else { 171 // One-shot: mark cancelled so it gets cleaned up. 172 timer.cancelled = true; 173 } 174 } 175 } 176 177 // Remove cancelled timers. 178 state.timers.retain(|t| !t.cancelled); 179 180 due 181 }) 182} 183 184/// Returns true if there are any pending (non-cancelled) timers. 185pub fn has_pending_timers() -> bool { 186 TIMER_STATE.with(|s| { 187 let state = s.borrow(); 188 state.timers.iter().any(|t| !t.cancelled) 189 }) 190} 191 192// ── Native function implementations ───────────────────────── 193 194use crate::vm::RuntimeError; 195 196/// `setTimeout(callback, delay)` — returns timer ID. 197pub fn set_timeout(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 198 let callback_ref = match args.first() { 199 Some(Value::Function(r)) => *r, 200 _ => return Ok(Value::Undefined), 201 }; 202 let delay = args.get(1).map(|v| v.to_number()).unwrap_or(0.0); 203 let id = schedule_timeout(callback_ref, delay); 204 Ok(Value::Number(id as f64)) 205} 206 207/// `clearTimeout(id)` — cancel a pending timeout. 208pub fn clear_timeout(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 209 if let Some(id) = args.first().map(|v| v.to_number() as u32) { 210 cancel_timer(id); 211 } 212 Ok(Value::Undefined) 213} 214 215/// `setInterval(callback, delay)` — returns timer ID. 216pub fn set_interval(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 217 let callback_ref = match args.first() { 218 Some(Value::Function(r)) => *r, 219 _ => return Ok(Value::Undefined), 220 }; 221 let delay = args.get(1).map(|v| v.to_number()).unwrap_or(0.0); 222 let id = schedule_interval(callback_ref, delay); 223 Ok(Value::Number(id as f64)) 224} 225 226/// `clearInterval(id)` — cancel a repeating timer. 227pub fn clear_interval(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 228 if let Some(id) = args.first().map(|v| v.to_number() as u32) { 229 cancel_timer(id); 230 } 231 Ok(Value::Undefined) 232} 233 234/// `requestAnimationFrame(callback)` — returns ID. 235pub fn request_animation_frame( 236 args: &[Value], 237 _ctx: &mut NativeContext, 238) -> Result<Value, RuntimeError> { 239 let callback_ref = match args.first() { 240 Some(Value::Function(r)) => *r, 241 _ => return Ok(Value::Undefined), 242 }; 243 let id = schedule_animation_frame(callback_ref); 244 Ok(Value::Number(id as f64)) 245} 246 247/// `cancelAnimationFrame(id)` — cancel pending animation frame. 248pub fn cancel_animation_frame( 249 args: &[Value], 250 _ctx: &mut NativeContext, 251) -> Result<Value, RuntimeError> { 252 if let Some(id) = args.first().map(|v| v.to_number() as u32) { 253 cancel_timer(id); 254 } 255 Ok(Value::Undefined) 256} 257 258// ── Tests ─────────────────────────────────────────────────── 259 260#[cfg(test)] 261mod tests { 262 use super::*; 263 use crate::compiler; 264 use crate::parser::Parser; 265 use crate::vm::{ConsoleOutput, Vm}; 266 use std::cell::RefCell; 267 use std::rc::Rc; 268 269 struct CapturedConsole { 270 log_messages: RefCell<Vec<String>>, 271 } 272 273 impl CapturedConsole { 274 fn new() -> Self { 275 Self { 276 log_messages: RefCell::new(Vec::new()), 277 } 278 } 279 } 280 281 impl ConsoleOutput for CapturedConsole { 282 fn log(&self, message: &str) { 283 self.log_messages.borrow_mut().push(message.to_string()); 284 } 285 fn error(&self, _message: &str) {} 286 fn warn(&self, _message: &str) {} 287 } 288 289 struct RcConsole(Rc<CapturedConsole>); 290 291 impl ConsoleOutput for RcConsole { 292 fn log(&self, message: &str) { 293 self.0.log(message); 294 } 295 fn error(&self, message: &str) { 296 self.0.error(message); 297 } 298 fn warn(&self, message: &str) { 299 self.0.warn(message); 300 } 301 } 302 303 /// Execute JS, pump the event loop, and return captured console output. 304 fn eval_with_timers(source: &str, max_iterations: usize) -> Vec<String> { 305 reset_timers(); 306 let console = Rc::new(CapturedConsole::new()); 307 let program = Parser::parse(source).expect("parse failed"); 308 let func = compiler::compile(&program).expect("compile failed"); 309 let mut vm = Vm::new(); 310 vm.set_console_output(Box::new(RcConsole(console.clone()))); 311 vm.execute(&func).expect("execute failed"); 312 vm.run_event_loop(max_iterations) 313 .expect("event loop failed"); 314 let result = console.log_messages.borrow().clone(); 315 result 316 } 317 318 #[test] 319 fn test_set_timeout_returns_id() { 320 reset_timers(); 321 let program = 322 Parser::parse("var id = setTimeout(function(){}, 0); id").expect("parse failed"); 323 let func = compiler::compile(&program).expect("compile failed"); 324 let mut vm = Vm::new(); 325 let result = vm.execute(&func).expect("execute failed"); 326 match result { 327 Value::Number(n) => assert!(n >= 1.0, "timer ID should be >= 1, got {n}"), 328 other => panic!("expected Number, got {other:?}"), 329 } 330 } 331 332 #[test] 333 fn test_set_timeout_unique_ids() { 334 reset_timers(); 335 let source = r#" 336 var id1 = setTimeout(function(){}, 0); 337 var id2 = setTimeout(function(){}, 0); 338 var id3 = setTimeout(function(){}, 0); 339 id1 + ',' + id2 + ',' + id3 340 "#; 341 let program = Parser::parse(source).expect("parse failed"); 342 let func = compiler::compile(&program).expect("compile failed"); 343 let mut vm = Vm::new(); 344 let result = vm.execute(&func).expect("execute failed"); 345 let s = result.to_js_string(&vm.gc); 346 let ids: Vec<u32> = s.split(',').map(|x| x.parse().unwrap()).collect(); 347 assert!( 348 ids[0] < ids[1] && ids[1] < ids[2], 349 "IDs should be monotonically increasing" 350 ); 351 } 352 353 #[test] 354 fn test_set_timeout_fires_callback() { 355 let logs = eval_with_timers( 356 r#"setTimeout(function() { console.log("fired"); }, 0);"#, 357 10, 358 ); 359 assert_eq!(logs, vec!["fired"]); 360 } 361 362 #[test] 363 fn test_set_timeout_deferred() { 364 // setTimeout(fn, 0) should NOT fire synchronously — only after the 365 // current script finishes and the event loop is pumped. 366 let logs = eval_with_timers( 367 r#" 368 var result = []; 369 setTimeout(function() { result.push("timer"); console.log(result.join(",")); }, 0); 370 result.push("sync"); 371 "#, 372 10, 373 ); 374 assert_eq!(logs, vec!["sync,timer"]); 375 } 376 377 #[test] 378 fn test_clear_timeout_prevents_execution() { 379 let logs = eval_with_timers( 380 r#" 381 var id = setTimeout(function() { console.log("should not fire"); }, 0); 382 clearTimeout(id); 383 "#, 384 10, 385 ); 386 assert!(logs.is_empty(), "cleared timeout should not fire"); 387 } 388 389 #[test] 390 fn test_set_interval_fires_multiple_times() { 391 let logs = eval_with_timers( 392 r#" 393 var count = 0; 394 var id = setInterval(function() { 395 count++; 396 console.log("tick " + count); 397 if (count >= 3) clearInterval(id); 398 }, 1); 399 "#, 400 100, 401 ); 402 assert_eq!(logs, vec!["tick 1", "tick 2", "tick 3"]); 403 } 404 405 #[test] 406 fn test_clear_interval_stops_repetition() { 407 let logs = eval_with_timers( 408 r#" 409 var count = 0; 410 var id = setInterval(function() { 411 count++; 412 console.log("tick"); 413 }, 1); 414 setTimeout(function() { clearInterval(id); }, 5); 415 "#, 416 100, 417 ); 418 // Should have fired some ticks but not infinitely. 419 assert!(!logs.is_empty(), "interval should have fired at least once"); 420 assert!(logs.len() < 50, "interval should have been cleared"); 421 } 422 423 #[test] 424 fn test_request_animation_frame_fires() { 425 let logs = eval_with_timers( 426 r#"requestAnimationFrame(function(ts) { console.log("raf " + typeof ts); });"#, 427 10, 428 ); 429 assert_eq!(logs, vec!["raf number"]); 430 } 431 432 #[test] 433 fn test_cancel_animation_frame() { 434 let logs = eval_with_timers( 435 r#" 436 var id = requestAnimationFrame(function() { console.log("should not fire"); }); 437 cancelAnimationFrame(id); 438 "#, 439 10, 440 ); 441 assert!(logs.is_empty(), "cancelled raf should not fire"); 442 } 443 444 #[test] 445 fn test_set_timeout_with_delay() { 446 // A timeout with a small delay should still fire when the event loop runs. 447 let logs = eval_with_timers( 448 r#"setTimeout(function() { console.log("delayed"); }, 10);"#, 449 100, 450 ); 451 assert_eq!(logs, vec!["delayed"]); 452 } 453 454 #[test] 455 fn test_multiple_timeouts_ordering() { 456 // Two zero-delay timeouts should fire in registration order. 457 let logs = eval_with_timers( 458 r#" 459 setTimeout(function() { console.log("first"); }, 0); 460 setTimeout(function() { console.log("second"); }, 0); 461 "#, 462 10, 463 ); 464 assert_eq!(logs, vec!["first", "second"]); 465 } 466 467 #[test] 468 fn test_set_timeout_non_function_arg() { 469 // Passing a non-function should return undefined, no crash. 470 reset_timers(); 471 let program = Parser::parse("setTimeout(42, 0)").expect("parse failed"); 472 let func = compiler::compile(&program).expect("compile failed"); 473 let mut vm = Vm::new(); 474 let result = vm.execute(&func).expect("execute failed"); 475 assert!(matches!(result, Value::Undefined)); 476 } 477 478 #[test] 479 fn test_clear_timeout_invalid_id() { 480 // Clearing a non-existent ID should not crash. 481 reset_timers(); 482 let program = Parser::parse("clearTimeout(9999)").expect("parse failed"); 483 let func = compiler::compile(&program).expect("compile failed"); 484 let mut vm = Vm::new(); 485 let result = vm.execute(&func).expect("execute failed"); 486 assert!(matches!(result, Value::Undefined)); 487 } 488 489 #[test] 490 fn test_timer_gc_roots_kept_alive() { 491 // Timer callbacks should survive GC. 492 let logs = eval_with_timers( 493 r#" 494 (function() { 495 setTimeout(function() { console.log("survived gc"); }, 10); 496 })(); 497 "#, 498 100, 499 ); 500 assert_eq!(logs, vec!["survived gc"]); 501 } 502 503 #[test] 504 fn test_timer_and_promise_interaction() { 505 // Promise microtasks should drain between timer callbacks. 506 let logs = eval_with_timers( 507 r#" 508 setTimeout(function() { 509 console.log("timer"); 510 Promise.resolve().then(function() { 511 console.log("microtask after timer"); 512 }); 513 }, 0); 514 "#, 515 10, 516 ); 517 assert_eq!(logs, vec!["timer", "microtask after timer"]); 518 } 519}