diff --git a/crates/javy/src/apis/timers/mod.rs b/crates/javy/src/apis/timers/mod.rs index f824d7f8..22b90318 100644 --- a/crates/javy/src/apis/timers/mod.rs +++ b/crates/javy/src/apis/timers/mod.rs @@ -1,8 +1,7 @@ -use std::{ - collections::BinaryHeap, - sync::{Arc, Mutex, OnceLock}, - time::{SystemTime, UNIX_EPOCH}, -}; +use std::sync::{Arc, Mutex}; + +mod queue; +use queue::{TimerCallback, TimerQueue}; use crate::{ hold, hold_and_release, @@ -11,213 +10,111 @@ use crate::{ }; use anyhow::{anyhow, Result}; -/// Timer entry in the timer queue -#[derive(Debug, Clone)] -struct Timer { - id: u32, - fire_time: u64, // milliseconds since UNIX epoch - callback: String, // JavaScript code to execute - interval_ms: Option, // If Some(), this is a repeating timer +pub struct TimersRuntime { + queue: Arc>, } -impl PartialEq for Timer { - fn eq(&self, other: &Self) -> bool { - self.fire_time == other.fire_time +impl TimersRuntime { + pub fn new() -> Self { + Self { + queue: Arc::new(Mutex::new(TimerQueue::new())), + } } -} -impl Eq for Timer {} + /// Register timer functions on the global object + pub fn register_globals(&self, this: Ctx<'_>) -> Result<()> { + let globals = this.globals(); + + let queue = self.queue.clone(); + globals.set("setTimeout", Function::new(this.clone(), MutFn::new(move |cx, args| { + let (cx, args) = hold_and_release!(cx, args); + set_timeout(&queue, hold!(cx.clone(), args)) + .map_err(|e| to_js_error(cx, e)) + }))?)?; + + let queue = self.queue.clone(); + globals.set("clearTimeout",Function::new(this.clone(), MutFn::new(move |cx, args| { + let (cx, args) = hold_and_release!(cx, args); + clear_timeout(&queue, hold!(cx.clone(), args)) + .map_err(|e| to_js_error(cx, e)) + }))?)?; + + let queue = self.queue.clone(); + globals.set("setInterval", Function::new(this.clone(), MutFn::new(move |cx, args| { + let (cx, args) = hold_and_release!(cx, args); + set_interval(&queue, hold!(cx.clone(), args)) + .map_err(|e| to_js_error(cx, e)) + }))?)?; + + let queue = self.queue.clone(); + globals.set("clearInterval", Function::new(this.clone(), MutFn::new(move |cx, args| { + let (cx, args) = hold_and_release!(cx, args); + clear_interval(&queue, hold!(cx.clone(), args)) + .map_err(|e| to_js_error(cx, e)) + }))?)?; -impl PartialOrd for Timer { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) + Ok(()) } -} -impl Ord for Timer { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - // Reverse order for min-heap behavior - other.fire_time.cmp(&self.fire_time) - } -} + /// Process expired timers - should be called by the event loop + pub fn process_timers(&self, ctx: Ctx<'_>) -> Result<()> { + let mut queue = self.queue.lock().unwrap(); + let expired_timers = queue.get_expired_timers(); -/// Global timer queue -#[derive(Debug)] -struct TimerQueue { - timers: BinaryHeap, - next_id: u32, -} - -impl TimerQueue { - fn new() -> Self { - Self { - timers: BinaryHeap::new(), - next_id: 1, + // Reschedule intervals before releasing the lock + for timer in &expired_timers { + if let Some(interval_ms) = timer.interval_ms { + queue.add_timer(interval_ms, true, timer.callback.clone(), Some(timer.id)); + } } - } - fn add_timer(&mut self, delay_ms: u32, callback: String) -> u32 { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis() as u64; - - let id = self.next_id; - self.next_id += 1; - - let timer = Timer { - id, - // For delay=0, set fire_time to current time to ensure immediate availability - fire_time: if delay_ms == 0 { now } else { now + delay_ms as u64 }, - callback, - interval_ms: None, - }; - - self.timers.push(timer); - id - } - - fn add_interval(&mut self, delay_ms: u32, callback: String) -> u32 { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis() as u64; - - let id = self.next_id; - self.next_id += 1; - - let timer = Timer { - id, - // For delay=0, set fire_time to current time to ensure immediate availability - fire_time: if delay_ms == 0 { now } else { now + delay_ms as u64 }, - callback, - interval_ms: Some(delay_ms), - }; - - self.timers.push(timer); - id - } - - fn reschedule_interval(&mut self, timer: Timer) { - if let Some(interval_ms) = timer.interval_ms { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis() as u64; - - let new_timer = Timer { - id: timer.id, - fire_time: if interval_ms == 0 { now } else { now + interval_ms as u64 }, - callback: timer.callback, - interval_ms: timer.interval_ms, + drop(queue); // Release lock before executing JavaScript + + // Execute all timer callbacks (both timeouts and intervals) + for timer in &expired_timers { + match &timer.callback { + TimerCallback::Code(code) => { + if let Err(e) = ctx.eval::<(), _>(code.as_str()) { + eprintln!("Timer callback error: {}", e); + } + }, + TimerCallback::Function => { + let code = format!("globalThis.__timer_callback_{}()", timer.id); + if let Err(e) = ctx.eval::<(), _>(code.as_str()) { + eprintln!("Timer callback error: {}", e); + } + // remove the callback from the global object, unless it's an interval + if timer.interval_ms.is_none() { + ctx.globals().remove(format!("__timer_callback_{}", timer.id))?; + } + }, }; - - self.timers.push(new_timer); } - } - fn remove_timer(&mut self, timer_id: u32) -> bool { - let original_len = self.timers.len(); - self.timers.retain(|timer| timer.id != timer_id); - self.timers.len() != original_len - } - - fn get_expired_timers(&mut self) -> Vec { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis() as u64; - - let mut expired = Vec::new(); - - while let Some(timer) = self.timers.peek() { - if timer.fire_time <= now { - expired.push(self.timers.pop().unwrap()); - } else { - break; - } - } - - expired + Ok(()) } - fn has_pending_timers(&self) -> bool { - !self.timers.is_empty() + /// Check if there are pending timers + pub fn has_pending_timers(&self) -> bool { + let queue = self.queue.lock().unwrap(); + queue.has_pending_timers() } } -static TIMER_QUEUE: OnceLock>> = OnceLock::new(); - -fn get_timer_queue() -> &'static Arc> { - TIMER_QUEUE.get_or_init(|| Arc::new(Mutex::new(TimerQueue::new()))) -} - -/// Register timer functions on the global object -pub(crate) fn register(this: Ctx<'_>) -> Result<()> { - let globals = this.globals(); - - globals.set( - "setTimeout", - Function::new( - this.clone(), - MutFn::new(move |cx, args| { - let (cx, args) = hold_and_release!(cx, args); - set_timeout(hold!(cx.clone(), args)).map_err(|e| to_js_error(cx, e)) - }), - )?, - )?; - - globals.set( - "clearTimeout", - Function::new( - this.clone(), - MutFn::new(move |cx, args| { - let (cx, args) = hold_and_release!(cx, args); - clear_timeout(hold!(cx.clone(), args)).map_err(|e| to_js_error(cx, e)) - }), - )?, - )?; - - globals.set( - "setInterval", - Function::new( - this.clone(), - MutFn::new(move |cx, args| { - let (cx, args) = hold_and_release!(cx, args); - set_interval(hold!(cx.clone(), args)).map_err(|e| to_js_error(cx, e)) - }), - )?, - )?; - - globals.set( - "clearInterval", - Function::new( - this.clone(), - MutFn::new(move |cx, args| { - let (cx, args) = hold_and_release!(cx, args); - clear_interval(hold!(cx.clone(), args)).map_err(|e| to_js_error(cx, e)) - }), - )?, - )?; - - Ok(()) -} - -fn set_timeout<'js>(args: Args<'js>) -> Result> { +fn set_timeout<'js>(queue: &Arc>, args: Args<'js>) -> Result> { let (ctx, args) = args.release(); let args = args.into_inner(); - + if args.is_empty() { return Err(anyhow!("setTimeout requires at least 1 argument")); } - // Get callback (can be function or string) - let callback_str = if args[0].is_function() { - // Convert function to string representation - val_to_string(&ctx, args[0].clone())? - } else { - // Treat as string code - val_to_string(&ctx, args[0].clone())? + let callback_str = val_to_string(&ctx, args[0].clone())?; + let callback = if args[0].is_function() { + TimerCallback::Function + } + else { + TimerCallback::Code(callback_str) }; // Get delay (default to 0 if not provided) @@ -227,43 +124,52 @@ fn set_timeout<'js>(args: Args<'js>) -> Result> { 0 }; - let mut queue = get_timer_queue().lock().unwrap(); - let timer_id = queue.add_timer(delay_ms, callback_str); - + let mut queue = queue.lock().unwrap(); + let timer_id = queue.add_timer(delay_ms, false, callback, None); + drop(queue); + + if args[0].is_function() { + ctx.globals().set(format!("__timer_callback_{}", timer_id), args[0].clone())?; + } + Ok(Value::new_int(ctx, timer_id as i32)) } -fn clear_timeout<'js>(args: Args<'js>) -> Result> { +fn clear_timeout<'js>(queue: &Arc>, args: Args<'js>) -> Result> { let (ctx, args) = args.release(); let args = args.into_inner(); - + if args.is_empty() { return Ok(Value::new_undefined(ctx)); } let timer_id = args[0].as_number().unwrap_or(0.0) as u32; - - let mut queue = get_timer_queue().lock().unwrap(); - queue.remove_timer(timer_id); - + + let mut queue = queue.lock().unwrap(); + let removed = queue.remove_timer(timer_id); + drop(queue); + + if removed { + ctx.globals().remove(format!("__timer_callback_{}", timer_id))?; + } + Ok(Value::new_undefined(ctx)) } -fn set_interval<'js>(args: Args<'js>) -> Result> { +fn set_interval<'js>(queue: &Arc>, args: Args<'js>) -> Result> { let (ctx, args) = args.release(); let args = args.into_inner(); - + if args.is_empty() { return Err(anyhow!("setInterval requires at least 1 argument")); } - // Get callback (can be function or string) - let callback_str = if args[0].is_function() { - // Convert function to string representation - val_to_string(&ctx, args[0].clone())? - } else { - // Treat as string code - val_to_string(&ctx, args[0].clone())? + let callback_str = val_to_string(&ctx, args[0].clone())?; + let callback = if args[0].is_function() { + TimerCallback::Function + } + else { + TimerCallback::Code(callback_str) }; // Get interval (default to 0 if not provided) @@ -273,58 +179,36 @@ fn set_interval<'js>(args: Args<'js>) -> Result> { 0 }; - let mut queue = get_timer_queue().lock().unwrap(); - let timer_id = queue.add_interval(interval_ms, callback_str); - + let mut queue = queue.lock().unwrap(); + let timer_id = queue.add_timer(interval_ms, true, callback, None); + drop(queue); + + if args[0].is_function() { + ctx.globals().set(format!("__timer_callback_{}", timer_id), args[0].clone())?; + } + Ok(Value::new_int(ctx, timer_id as i32)) } -fn clear_interval<'js>(args: Args<'js>) -> Result> { +fn clear_interval<'js>(queue: &Arc>, args: Args<'js>) -> Result> { let (ctx, args) = args.release(); let args = args.into_inner(); - + if args.is_empty() { return Ok(Value::new_undefined(ctx)); } let timer_id = args[0].as_number().unwrap_or(0.0) as u32; - - let mut queue = get_timer_queue().lock().unwrap(); - queue.remove_timer(timer_id); - - Ok(Value::new_undefined(ctx)) -} -/// Process expired timers - should be called by the event loop -pub fn process_timers(ctx: Ctx<'_>) -> Result<()> { - let mut queue = get_timer_queue().lock().unwrap(); - let expired_timers = queue.get_expired_timers(); - - // Separate intervals that need rescheduling - let (intervals, timeouts): (Vec<_>, Vec<_>) = expired_timers.into_iter() - .partition(|timer| timer.interval_ms.is_some()); - - // Reschedule intervals before releasing the lock - for interval in &intervals { - queue.reschedule_interval(interval.clone()); - } - - drop(queue); // Release lock before executing JavaScript - - // Execute all timer callbacks (both timeouts and intervals) - for timer in timeouts.into_iter().chain(intervals.into_iter()) { - if let Err(e) = ctx.eval::<(), _>(timer.callback.as_str()) { - eprintln!("Timer callback error: {}", e); - } + let mut queue = queue.lock().unwrap(); + let removed = queue.remove_timer(timer_id); + drop(queue); + + if removed { + ctx.globals().remove(format!("__timer_callback_{}", timer_id))?; } - - Ok(()) -} -/// Check if there are pending timers -pub fn has_pending_timers() -> bool { - let queue = get_timer_queue().lock().unwrap(); - queue.has_pending_timers() + Ok(Value::new_undefined(ctx)) } #[cfg(test)] @@ -333,46 +217,17 @@ mod tests { use crate::{Config, Runtime}; use anyhow::Error; - #[test] - fn test_timer_queue() { - let mut queue = TimerQueue::new(); - - // Add some timers - let id1 = queue.add_timer(100, "console.log('timer1')".to_string()); - let id2 = queue.add_timer(50, "console.log('timer2')".to_string()); - let id3 = queue.add_timer(200, "console.log('timer3')".to_string()); - - assert_eq!(id1, 1); - assert_eq!(id2, 2); - assert_eq!(id3, 3); - - assert!(queue.has_pending_timers()); - - // Remove a timer - assert!(queue.remove_timer(id2)); - assert!(!queue.remove_timer(999)); // Non-existent timer - - assert!(queue.has_pending_timers()); - } - #[test] fn test_register() -> Result<()> { let mut config = Config::default(); config.timers(true); let runtime = Runtime::new(config)?; runtime.context().with(|cx| { - register(cx.clone())?; - - // Check that setTimeout is available - let result: Value = cx.eval("typeof setTimeout")?; - let type_str = val_to_string(&cx, result)?; - assert_eq!(type_str, "function"); - - // Check that clearTimeout is available - let result: Value = cx.eval("typeof clearTimeout")?; - let type_str = val_to_string(&cx, result)?; - assert_eq!(type_str, "function"); - + // Check that API is available + assert_eq!("function", cx.eval::("typeof setTimeout")?); + assert_eq!("function", cx.eval::("typeof clearTimeout")?); + assert_eq!("function", cx.eval::("typeof setInterval")?); + assert_eq!("function", cx.eval::("typeof clearInterval")?); Ok::<_, Error>(()) })?; Ok(()) @@ -384,18 +239,14 @@ mod tests { config.timers(true); let runtime = Runtime::new(config)?; runtime.context().with(|cx| { - register(cx.clone())?; - // Test setTimeout with string callback - let result: Value = cx.eval("setTimeout('1+1', 100)")?; - let timer_id = result.as_number().unwrap() as i32; + let timer_id: i32 = cx.eval("setTimeout('1+1', 100)")?; assert!(timer_id > 0); - - // Test setTimeout with function callback - let result: Value = cx.eval("setTimeout(function() { return 42; }, 50)")?; - let timer_id2 = result.as_number().unwrap() as i32; + + // Test setTimeout with function callback + let timer_id2: i32 = cx.eval("setTimeout(function() { return 42; }, 50)")?; assert!(timer_id2 > timer_id); - + Ok::<_, Error>(()) })?; Ok(()) @@ -407,13 +258,10 @@ mod tests { config.timers(true); let runtime = Runtime::new(config)?; runtime.context().with(|cx| { - register(cx.clone())?; - // Create a timer and clear it - let result: Value = cx.eval("const id = setTimeout('console.log(\"test\")', 1000); clearTimeout(id); id")?; - let timer_id = result.as_number().unwrap() as i32; + let code = r#"const id = setTimeout('console.log("test")', 1000); clearTimeout(id); id"#; + let timer_id: i32 = cx.eval(code)?; assert!(timer_id > 0); - Ok::<_, Error>(()) })?; Ok(()) @@ -421,32 +269,59 @@ mod tests { #[test] fn test_timer_execution() -> Result<()> { - // Clear any existing timers from other tests - { - let mut queue = get_timer_queue().lock().unwrap(); - queue.timers.clear(); - } - let mut config = Config::default(); config.timers(true); let runtime = Runtime::new(config)?; + + runtime.context().with(|cx| { + cx.eval::<(), _>("globalThis.var1 = -123; setTimeout('globalThis.var1 = 321', 0)")?; + Ok::<_, Error>(()) + })?; + + // Process timers immediately without sleep - they should be available + runtime.resolve_pending_jobs()?; + runtime.context().with(|cx| { - register(cx.clone())?; - - // Use a unique variable name to avoid interference between tests - let unique_var = format!("timerExecuted_{}", std::process::id()); - let timer_code = format!("globalThis.{} = false; setTimeout('globalThis.{} = true', 0)", unique_var, unique_var); - cx.eval::<(), _>(timer_code.as_str())?; - - // Process timers immediately without sleep - they should be available - process_timers(cx.clone())?; - // Check if timer was executed - let check_code = format!("globalThis.{}", unique_var); - let result: Value = cx.eval(check_code.as_str())?; - - assert!(result.as_bool().unwrap_or(false)); - + assert_eq!(321, cx.eval::("globalThis.var1")?); + Ok::<_, Error>(()) + })?; + Ok(()) + } + + #[test] + fn test_timer_function_callback() -> Result<()> { + let mut config = Config::default(); + config.timers(true); + let runtime = Runtime::new(config)?; + + runtime.context().with(|cx| { + // Create timeout with a closure with a mutable state + // To make sure the closure preserves the state reference + let res = cx.eval::<(), _>(" + globalThis.var1 = -123; + function createIncrementor(initialDelta) { + var delta = initialDelta; + return [() => globalThis.var1 += delta, (newDelta) => delta = newDelta]; + } + var [incrementor, setDelta] = createIncrementor(100); + incrementor(); + setTimeout(incrementor, 0); + setDelta(123); + ")?; + + // So far, only explicit call to incrementor (having delta = 100) is done + assert_eq!(-23, cx.eval::("globalThis.var1")?); + + Ok::<_, Error>(()) + })?; + + // Process timers immediately without sleep - they should be available + runtime.resolve_pending_jobs()?; + + runtime.context().with(|cx| { + // Check if closure correctly applied delta modified after its creation + assert_eq!(100, cx.eval::("globalThis.var1")?); Ok::<_, Error>(()) })?; Ok(()) @@ -454,33 +329,22 @@ mod tests { #[test] fn test_timer_with_delay() -> Result<()> { - // Clear any existing timers from other tests - { - let mut queue = get_timer_queue().lock().unwrap(); - queue.timers.clear(); - } - let mut config = Config::default(); config.timers(true); let runtime = Runtime::new(config)?; + runtime.context().with(|cx| { - register(cx.clone())?; - - // Use unique variable name to avoid interference between tests - let unique_var = format!("delayedTimer_{}", std::process::id()); - // Set a timer with a delay that shouldn't fire immediately - let timer_code = format!("globalThis.{} = false; setTimeout('globalThis.{} = true', 1000)", unique_var, unique_var); - cx.eval::<(), _>(timer_code.as_str())?; - - // Process timers immediately - should not execute - process_timers(cx.clone())?; - + cx.eval::<(), _>("globalThis.var1 = -765; setTimeout('globalThis.var1 = 567', 1000)")?; + Ok::<_, Error>(()) + })?; + + // Process timers immediately - should not execute + runtime.resolve_pending_jobs()?; + + runtime.context().with(|cx| { // Check if timer was NOT executed - let check_code = format!("globalThis.{}", unique_var); - let result: Value = cx.eval(check_code.as_str())?; - assert!(!result.as_bool().unwrap_or(true)); - + assert_eq!(-765, cx.eval::("globalThis.var1")?); Ok::<_, Error>(()) })?; Ok(()) @@ -488,43 +352,28 @@ mod tests { #[test] fn test_multiple_timers() -> Result<()> { - // Clear any existing timers from other tests - { - let mut queue = get_timer_queue().lock().unwrap(); - queue.timers.clear(); - } - let mut config = Config::default(); config.timers(true); let runtime = Runtime::new(config)?; + runtime.context().with(|cx| { - register(cx.clone())?; - - // Use unique variable names to avoid interference between tests - let unique_id = std::process::id(); - let timer1_var = format!("timer1_{}", unique_id); - let timer2_var = format!("timer2_{}", unique_id); - // Set multiple timers - let timer_code = format!(" - globalThis.{} = false; - globalThis.{} = false; - setTimeout('globalThis.{} = true', 0); - setTimeout('globalThis.{} = true', 0); - ", timer1_var, timer2_var, timer1_var, timer2_var); - cx.eval::<(), _>(timer_code.as_str())?; - - // Process timers - process_timers(cx.clone())?; - + cx.eval::<(), _>(" + globalThis.var1 = 0; + globalThis.var2 = 0; + setTimeout('globalThis.var1 = 123', 0); + setTimeout('globalThis.var2 = 321', 0); + ")?; + Ok::<_, Error>(()) + })?; + + // Process timers + runtime.resolve_pending_jobs()?; + + runtime.context().with(|cx| { // Check if both timers were executed - let check1_code = format!("globalThis.{}", timer1_var); - let check2_code = format!("globalThis.{}", timer2_var); - let result1: Value = cx.eval(check1_code.as_str())?; - let result2: Value = cx.eval(check2_code.as_str())?; - assert!(result1.as_bool().unwrap_or(false)); - assert!(result2.as_bool().unwrap_or(false)); - + assert_eq!(123, cx.eval::("globalThis.var1")?); + assert_eq!(321, cx.eval::("globalThis.var2")?); Ok::<_, Error>(()) })?; Ok(()) @@ -532,37 +381,26 @@ mod tests { #[test] fn test_clear_timeout_removes_timer() -> Result<()> { - // Clear any existing timers from other tests - { - let mut queue = get_timer_queue().lock().unwrap(); - queue.timers.clear(); - } - let mut config = Config::default(); config.timers(true); let runtime = Runtime::new(config)?; + runtime.context().with(|cx| { - register(cx.clone())?; - - // Use unique variable name to avoid interference between tests - let unique_var = format!("clearedTimer_{}", std::process::id()); - // Set a timer and immediately clear it - let timer_code = format!(" - globalThis.{} = false; - const id = setTimeout('globalThis.{} = true', 0); + cx.eval::<(), _>(" + globalThis.var1 = -432; + const id = setTimeout('globalThis.var1 = 234', 0); clearTimeout(id); - ", unique_var, unique_var); - cx.eval::<(), _>(timer_code.as_str())?; - - // Process timers - process_timers(cx.clone())?; - + ")?; + Ok::<_, Error>(()) + })?; + + // Process timers + runtime.resolve_pending_jobs()?; + + runtime.context().with(|cx| { // Check if timer was NOT executed - let check_code = format!("globalThis.{}", unique_var); - let result: Value = cx.eval(check_code.as_str())?; - assert!(!result.as_bool().unwrap_or(true)); - + assert_eq!(-432, cx.eval::("globalThis.var1")?); Ok::<_, Error>(()) })?; Ok(()) @@ -570,78 +408,53 @@ mod tests { #[test] fn test_has_pending_timers() -> Result<()> { - // Clear any existing timers from other tests and verify clean state - { - let mut queue = get_timer_queue().lock().unwrap(); - queue.timers.clear(); - } - - // Initially no pending timers - assert!(!has_pending_timers()); - let mut config = Config::default(); config.timers(true); let runtime = Runtime::new(config)?; + + // Initially no pending timers + assert!(!runtime.has_pending_timers()); + runtime.context().with(|cx| { - register(cx.clone())?; - // Add a timer - cx.eval::<(), _>("setTimeout('console.log(\"test\")', 1000)")?; - - // Should have pending timers - assert!(has_pending_timers()); - + cx.eval::<(), _>(r#"setTimeout('console.log("test")', 1000)"#)?; Ok::<_, Error>(()) })?; + + // Should have pending timers + assert!(runtime.has_pending_timers()); + Ok(()) } #[test] fn test_set_interval_basic() -> Result<()> { - // Clear any existing timers from other tests - { - let mut queue = get_timer_queue().lock().unwrap(); - queue.timers.clear(); - } - let mut config = Config::default(); config.timers(true); let runtime = Runtime::new(config)?; runtime.context().with(|cx| { - register(cx.clone())?; - // Test setInterval with string callback - let result: Value = cx.eval("setInterval('1+1', 100)")?; - let interval_id = result.as_number().unwrap() as i32; + let interval_id: i32 = cx.eval("setInterval('1+1', 100)")?; assert!(interval_id > 0); - - // Should have pending timers - assert!(has_pending_timers()); - Ok::<_, Error>(()) })?; + + // Should have pending timers + assert!(runtime.has_pending_timers()); + Ok(()) } #[test] fn test_clear_interval() -> Result<()> { - // Clear any existing timers from other tests - { - let mut queue = get_timer_queue().lock().unwrap(); - queue.timers.clear(); - } - let mut config = Config::default(); config.timers(true); let runtime = Runtime::new(config)?; runtime.context().with(|cx| { - register(cx.clone())?; - // Create an interval and clear it - let result: Value = cx.eval("const id = setInterval('console.log(\"test\")', 1000); clearInterval(id); id")?; - let interval_id = result.as_number().unwrap() as i32; + let code = r#"const id = setInterval('console.log("test")', 1000); clearInterval(id); id"#; + let interval_id: i32 = cx.eval(code)?; assert!(interval_id > 0); - Ok::<_, Error>(()) })?; Ok(()) @@ -649,36 +462,24 @@ mod tests { #[test] fn test_interval_execution_and_rescheduling() -> Result<()> { - // Clear any existing timers from other tests - { - let mut queue = get_timer_queue().lock().unwrap(); - queue.timers.clear(); - } - let mut config = Config::default(); config.timers(true); let runtime = Runtime::new(config)?; + + runtime.context().with(|cx| { + cx.eval::<(), _>("globalThis.var1 = 1000; setInterval('globalThis.var1++', 0)")?; + Ok::<_, Error>(()) + })?; + + // Process timers multiple times to test rescheduling + runtime.resolve_pending_jobs()?; + runtime.resolve_pending_jobs()?; + runtime.resolve_pending_jobs()?; + runtime.context().with(|cx| { - register(cx.clone())?; - - // Use unique variable name to avoid interference between tests - let unique_var = format!("intervalCount_{}", std::process::id()); - let interval_code = format!("globalThis.{} = 0; setInterval('globalThis.{}++', 0)", unique_var, unique_var); - cx.eval::<(), _>(interval_code.as_str())?; - - // Process timers multiple times to test rescheduling - process_timers(cx.clone())?; - process_timers(cx.clone())?; - process_timers(cx.clone())?; - - // Check if interval executed multiple times - let check_code = format!("globalThis.{}", unique_var); - let result: Value = cx.eval(check_code.as_str())?; - let count = result.as_number().unwrap_or(0.0) as i32; - - // Should have executed at least twice (showing it's repeating) - assert!(count >= 2, "Interval should have executed multiple times, got {}", count); - + // Check if interval executed multiple times (showing it's repeating) + let var1: i32 = cx.eval("globalThis.var1")?; + assert!(var1 >= 1002, "Interval should have executed multiple times, got {}", var1); Ok::<_, Error>(()) })?; Ok(()) @@ -686,40 +487,28 @@ mod tests { #[test] fn test_clear_interval_stops_repetition() -> Result<()> { - // Clear any existing timers from other tests - { - let mut queue = get_timer_queue().lock().unwrap(); - queue.timers.clear(); - } - let mut config = Config::default(); config.timers(true); let runtime = Runtime::new(config)?; + runtime.context().with(|cx| { - register(cx.clone())?; - - // Use unique variable name to avoid interference between tests - let unique_var = format!("clearedIntervalCount_{}", std::process::id()); - let interval_code = format!(" - globalThis.{} = 0; - const id = setInterval('globalThis.{}++', 0); + cx.eval::<(), _>(" + globalThis.var1 = 100; + const id = setInterval('globalThis.var1++', 0); clearInterval(id); - ", unique_var, unique_var); - cx.eval::<(), _>(interval_code.as_str())?; - - // Process timers multiple times - process_timers(cx.clone())?; - process_timers(cx.clone())?; - process_timers(cx.clone())?; - - // Check that interval was NOT executed - let check_code = format!("globalThis.{}", unique_var); - let result: Value = cx.eval(check_code.as_str())?; - let count = result.as_number().unwrap_or(-1.0) as i32; - - // Should still be 0 (not executed) - assert_eq!(count, 0, "Cleared interval should not execute, got {}", count); - + ")?; + Ok::<_, Error>(()) + })?; + + // Process timers multiple times + runtime.resolve_pending_jobs()?; + runtime.resolve_pending_jobs()?; + runtime.resolve_pending_jobs()?; + + runtime.context().with(|cx| { + // Check that interval was NOT executed (should be 0) + let var1: i32 = cx.eval("globalThis.var1")?; + assert_eq!(100, var1, "Cleared interval should not execute"); Ok::<_, Error>(()) })?; Ok(()) @@ -727,56 +516,47 @@ mod tests { #[test] fn test_interval_and_timeout_coexistence() -> Result<()> { - // Clear any existing timers from other tests - { - let mut queue = get_timer_queue().lock().unwrap(); - queue.timers.clear(); - } - let mut config = Config::default(); config.timers(true); let runtime = Runtime::new(config)?; + runtime.context().with(|cx| { - register(cx.clone())?; - - // Use unique variable names - let unique_id = std::process::id(); - let timeout_var = format!("timeoutExecuted_{}", unique_id); - let interval_var = format!("intervalCount_{}", unique_id); - // Set timeout first, then interval - both with 0 delay - let timer_code = format!(" - globalThis.{} = false; - globalThis.{} = 0; - setTimeout('globalThis.{} = true', 0); - setInterval('globalThis.{}++', 0); - ", timeout_var, interval_var, timeout_var, interval_var); - cx.eval::<(), _>(timer_code.as_str())?; - - // Process timers first time - should execute both timeout and first interval - process_timers(cx.clone())?; - - // Check both timeout and interval results - let timeout_check = format!("globalThis.{}", timeout_var); - let interval_check = format!("globalThis.{}", interval_var); - - let timeout_result: Value = cx.eval(timeout_check.as_str())?; - let interval_result: Value = cx.eval(interval_check.as_str())?; - - assert!(timeout_result.as_bool().unwrap_or(false), "Timeout should have executed"); - assert!(interval_result.as_number().unwrap_or(0.0) as i32 >= 1, "Interval should have executed at least once"); - - // Process timers again to verify interval repeats (timeout shouldn't run again) - process_timers(cx.clone())?; - let interval_result2: Value = cx.eval(interval_check.as_str())?; - let timeout_result2: Value = cx.eval(timeout_check.as_str())?; - + cx.eval::<(), _>(" + globalThis.var1 = -543; + globalThis.var2 = 100; + setTimeout('globalThis.var1 = 999', 0); + setInterval('globalThis.var2++', 0); + ")?; + Ok::<_, Error>(()) + })?; + + // Process timers first time - should execute both timeout and first interval + runtime.resolve_pending_jobs()?; + + runtime.context().with(|cx| { + let var1: i32 = cx.eval("globalThis.var1")?; + let var2: i32 = cx.eval("globalThis.var2")?; + + assert_eq!(999, var1, "Timeout should have executed"); + assert!(var2 >= 101, "Interval should have executed at least once, got {}", var2); + + Ok::<_, Error>(()) + })?; + + // Process timers again to verify interval repeats (timeout shouldn't run again) + runtime.resolve_pending_jobs()?; + + runtime.context().with(|cx| { + let var1: i32 = cx.eval("globalThis.var1")?; + let var2: i32 = cx.eval("globalThis.var2")?; + // Timeout should still be true (unchanged), interval should have incremented - assert!(timeout_result2.as_bool().unwrap_or(false), "Timeout should remain executed"); - assert!(interval_result2.as_number().unwrap_or(0.0) as i32 >= 2, "Interval should have executed multiple times"); - + assert_eq!(999, var1, "Timeout should remain executed"); + assert!(var2 >= 102, "Interval should have executed multiple times"); + Ok::<_, Error>(()) })?; Ok(()) } -} \ No newline at end of file +} diff --git a/crates/javy/src/apis/timers/queue.rs b/crates/javy/src/apis/timers/queue.rs new file mode 100644 index 00000000..86cad2a2 --- /dev/null +++ b/crates/javy/src/apis/timers/queue.rs @@ -0,0 +1,144 @@ +use std::{ + collections::BinaryHeap, + time::{SystemTime, UNIX_EPOCH}, +}; + +#[derive(Debug, Clone)] +pub(super) enum TimerCallback { + Code(String), + Function, +} + +/// Timer entry in the timer queue +#[derive(Debug)] +pub(super) struct Timer { + pub id: u32, + pub fire_time: u64, // milliseconds since UNIX epoch + pub callback: TimerCallback, + pub interval_ms: Option, // If Some(), this is a repeating timer +} + +impl PartialEq for Timer { + fn eq(&self, other: &Self) -> bool { + self.fire_time == other.fire_time + } +} + +impl Eq for Timer {} + +impl PartialOrd for Timer { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Timer { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + // Reverse order for min-heap behavior + other.fire_time.cmp(&self.fire_time) + } +} + +/// Global timer queue +#[derive(Debug)] +pub(super) struct TimerQueue { + timers: BinaryHeap, + next_id: u32, +} + +impl TimerQueue { + pub fn new() -> Self { + Self { + timers: BinaryHeap::new(), + next_id: 1, + } + } + + pub fn add_timer( + &mut self, + delay_ms: u32, + repeat: bool, + callback: TimerCallback, + reuse_id: Option, + ) -> u32 { + let now = Self::now(); + + let id = reuse_id.unwrap_or_else(|| { + let id = self.next_id; + self.next_id += 1; + id + }); + + let timer = Timer { + id, + fire_time: now + delay_ms as u64, + callback, + interval_ms: if repeat { Some(delay_ms) } else { None }, + }; + + self.timers.push(timer); + id + } + + pub fn remove_timer(&mut self, timer_id: u32) -> bool { + let original_len = self.timers.len(); + self.timers.retain(|timer| timer.id != timer_id); + self.timers.len() != original_len + } + + pub fn get_expired_timers(&mut self) -> Vec { + let now = Self::now(); + let mut expired = Vec::new(); + while let Some(timer) = self.timers.peek() { + if timer.fire_time <= now { + expired.push(self.timers.pop().unwrap()); + } else { + break; + } + } + + expired + } + + pub fn has_pending_timers(&self) -> bool { + !self.timers.is_empty() + } + + fn now() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis() as u64 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_timer_queue() { + let mut queue = TimerQueue::new(); + + fn add_timer(delay_ms: u32, callback_code: &str, queue: &mut TimerQueue) -> u32 { + queue.add_timer(delay_ms, false, TimerCallback::Code(callback_code.to_string()), None) + } + + // Add some timers + let id1 = add_timer(100, "console.log('timer1')", &mut queue); + let id2 = add_timer(50, "console.log('timer2')", &mut queue); + let id3 = add_timer(200, "console.log('timer3')", &mut queue); + + assert_eq!(id1, 1); + assert_eq!(id2, 2); + assert_eq!(id3, 3); + + assert!(queue.has_pending_timers()); + + // Remove a timer + assert!(queue.remove_timer(id2)); + assert!(!queue.remove_timer(999)); // Non-existent timer + + assert!(queue.has_pending_timers()); + } +} diff --git a/crates/javy/src/runtime.rs b/crates/javy/src/runtime.rs index 3dbb0a04..d26e8e44 100644 --- a/crates/javy/src/runtime.rs +++ b/crates/javy/src/runtime.rs @@ -3,7 +3,7 @@ use super::from_js_error; #[cfg(feature = "json")] use crate::apis::json; use crate::{ - apis::{base64, console, random, stream_io, text_encoding, timers}, + apis::{base64, console, random, stream_io, text_encoding, timers::TimersRuntime}, config::{JSIntrinsics, JavyIntrinsics}, Config, }; @@ -39,21 +39,25 @@ pub struct Runtime { /// The inner QuickJS runtime representation. // Read above on the usage of `ManuallyDrop`. inner: ManuallyDrop, - /// Whether timers are enabled - timers_enabled: bool, + /// Timers runtime state, if enabled. + timers: Option, } impl Runtime { /// Creates a new [Runtime]. pub fn new(config: Config) -> Result { let rt = ManuallyDrop::new(QRuntime::new()?); - let timers_enabled = config.intrinsics.contains(JSIntrinsics::TIMERS); + let timers = if config.intrinsics.contains(JSIntrinsics::TIMERS) { + Some(TimersRuntime::new()) + } else { + None + }; - let context = Self::build_from_config(&rt, config)?; - Ok(Self { inner: rt, context, timers_enabled }) + let context = Self::build_from_config(&rt, config, &timers)?; + Ok(Self { inner: rt, context, timers }) } - fn build_from_config(rt: &QRuntime, cfg: Config) -> Result> { + fn build_from_config(rt: &QRuntime, cfg: Config, timers: &Option) -> Result> { let cfg = cfg.validate()?; let intrinsics = &cfg.intrinsics; let javy_intrinsics = &cfg.javy_intrinsics; @@ -156,8 +160,8 @@ impl Runtime { .expect("registering StreamIO functions to succeed"); } - if intrinsics.contains(JSIntrinsics::TIMERS) { - timers::register(ctx.clone()) + if let Some(timers) = timers { + timers.register_globals(ctx.clone()) .expect("registering timer APIs to succeed"); } }); @@ -174,8 +178,8 @@ impl Runtime { pub fn resolve_pending_jobs(&self) -> Result<()> { // Process timers if enabled self.context.with(|ctx| { - if self.has_timers_enabled() { - timers::process_timers(ctx.clone()).unwrap_or_else(|e| { + if let Some(timers) = &self.timers { + timers.process_timers(ctx.clone()).unwrap_or_else(|e| { eprintln!("Timer processing error: {}", e); }); } @@ -200,18 +204,15 @@ impl Runtime { /// Returns true if there are pending jobs in the queue. pub fn has_pending_jobs(&self) -> bool { let has_js_jobs = self.inner.is_job_pending(); - let has_timer_jobs = if self.has_timers_enabled() { - timers::has_pending_timers() - } else { - false - }; - - has_js_jobs || has_timer_jobs + has_js_jobs || self.has_pending_timers() } - /// Returns true if timers are enabled for this runtime. - pub fn has_timers_enabled(&self) -> bool { - self.timers_enabled + pub fn has_pending_timers(&self) -> bool { + if let Some(timers) = &self.timers { + timers.has_pending_timers() + } else { + false + } } /// Compiles the given module to bytecode.