we (web engine): Experimental web browser project to understand the limits of Claude
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Implement timer APIs: setTimeout, setInterval, requestAnimationFrame

- Add timers.rs module with thread-local timer registry, ID generation,
and timer lifecycle management (schedule, cancel, take due timers)
- Register global functions: setTimeout, clearTimeout, setInterval,
clearInterval, requestAnimationFrame, cancelAnimationFrame
- Integrate timer execution with VM event loop: drain_due_timers runs
after microtasks, pump_event_loop and run_event_loop for external use
- Timer callbacks registered as GC roots to survive garbage collection
- Microtask queue drains after each timer callback (Promise integration)
- 15 tests covering: ID generation, deferred execution, cancellation,
interval repetition, requestAnimationFrame timestamps, GC survival,
Promise-timer interaction, edge cases

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+581
+14
crates/js/src/builtins.rs
··· 6511 6511 vm.define_native("parseFloat", parse_float); 6512 6512 vm.define_native("isNaN", is_nan); 6513 6513 vm.define_native("isFinite", is_finite); 6514 + 6515 + // Timer APIs. 6516 + vm.define_native("setTimeout", crate::timers::set_timeout); 6517 + vm.define_native("clearTimeout", crate::timers::clear_timeout); 6518 + vm.define_native("setInterval", crate::timers::set_interval); 6519 + vm.define_native("clearInterval", crate::timers::clear_interval); 6520 + vm.define_native( 6521 + "requestAnimationFrame", 6522 + crate::timers::request_animation_frame, 6523 + ); 6524 + vm.define_native( 6525 + "cancelAnimationFrame", 6526 + crate::timers::cancel_animation_frame, 6527 + ); 6514 6528 } 6515 6529 6516 6530 fn parse_int(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
+1
crates/js/src/lib.rs
··· 9 9 pub mod lexer; 10 10 pub mod parser; 11 11 pub mod regex; 12 + pub mod timers; 12 13 pub mod vm; 13 14 14 15 use std::fmt;
+519
crates/js/src/timers.rs
··· 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 + 7 + use std::cell::RefCell; 8 + use std::time::{Duration, Instant}; 9 + 10 + use crate::gc::GcRef; 11 + use crate::vm::{NativeContext, Value}; 12 + 13 + /// A pending timer (timeout, interval, or animation frame). 14 + struct 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. 26 + struct TimerState { 27 + timers: Vec<Timer>, 28 + next_id: u32, 29 + /// Epoch for `requestAnimationFrame` timestamps (millis since page load). 30 + epoch: Instant, 31 + } 32 + 33 + impl TimerState { 34 + fn new() -> Self { 35 + Self { 36 + timers: Vec::new(), 37 + next_id: 1, 38 + epoch: Instant::now(), 39 + } 40 + } 41 + } 42 + 43 + thread_local! { 44 + static TIMER_STATE: RefCell<TimerState> = RefCell::new(TimerState::new()); 45 + } 46 + 47 + /// Reset timer state (useful for tests to avoid leaking state). 48 + pub 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. 58 + fn 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. 76 + fn 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. 94 + fn 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). 113 + fn 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. 124 + pub 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. 137 + pub 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. 145 + pub 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. 185 + pub 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 + 194 + use crate::vm::RuntimeError; 195 + 196 + /// `setTimeout(callback, delay)` — returns timer ID. 197 + pub 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. 208 + pub 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. 216 + pub 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. 227 + pub 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. 235 + pub 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. 248 + pub 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)] 261 + mod 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 + }
+47
crates/js/src/vm.rs
··· 1260 1260 Ok(()) 1261 1261 } 1262 1262 1263 + /// Execute all due timer callbacks. After each callback, drain the 1264 + /// microtask queue (so Promise `.then()` runs between timer callbacks). 1265 + fn drain_due_timers(&mut self) -> Result<(), RuntimeError> { 1266 + let due = crate::timers::take_due_timers(); 1267 + for timer in due { 1268 + // For requestAnimationFrame, pass the timestamp as an argument. 1269 + let args = match timer.raf_timestamp { 1270 + Some(ts) => vec![Value::Number(ts)], 1271 + None => vec![], 1272 + }; 1273 + let _ = self.call_function(timer.callback, &args); 1274 + self.drain_microtasks()?; 1275 + } 1276 + Ok(()) 1277 + } 1278 + 1279 + /// Pump the event loop: drain due timers and microtasks. 1280 + /// 1281 + /// Call this from tests or the platform event loop to fire any pending 1282 + /// timers whose delay has elapsed. Each timer callback is followed by 1283 + /// a microtask drain. 1284 + pub fn pump_event_loop(&mut self) -> Result<(), RuntimeError> { 1285 + self.drain_due_timers() 1286 + } 1287 + 1288 + /// Run the event loop until all pending timers have fired. 1289 + /// Useful in tests to deterministically execute all scheduled work. 1290 + /// `max_iterations` caps the loop to prevent infinite loops with 1291 + /// recurring intervals; pass 0 for unlimited. 1292 + pub fn run_event_loop(&mut self, max_iterations: usize) -> Result<(), RuntimeError> { 1293 + let mut iterations = 0; 1294 + while crate::timers::has_pending_timers() { 1295 + if max_iterations > 0 && iterations >= max_iterations { 1296 + break; 1297 + } 1298 + self.drain_due_timers()?; 1299 + iterations += 1; 1300 + // If timers are not yet due, sleep briefly to avoid spinning. 1301 + if crate::timers::has_pending_timers() { 1302 + std::thread::sleep(std::time::Duration::from_millis(1)); 1303 + } 1304 + } 1305 + Ok(()) 1306 + } 1307 + 1263 1308 /// Ensure the register file has at least `needed` slots. 1264 1309 fn ensure_registers(&mut self, needed: usize) { 1265 1310 if needed > self.registers.len() { ··· 2061 2106 } 2062 2107 } 2063 2108 } 2109 + // Pending timer callbacks must be GC roots. 2110 + roots.extend(crate::timers::timer_gc_roots()); 2064 2111 roots 2065 2112 } 2066 2113