we (web engine): Experimental web browser project to understand the limits of Claude
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}