use std::convert::Infallible; use std::io; use std::rc::Rc; use std::sync::{Arc, Condvar, Mutex}; use std::thread::JoinHandle; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use avril_core::{Extension, Op}; use futures::channel::oneshot; #[derive(Default)] struct State { pending: Option<(Instant, oneshot::Sender<()>)>, stopped: bool, } struct Timers { origin: Instant, time_origin: f64, shared: Arc<(Mutex, Condvar)>, worker: Option>, } impl Timers { fn new() -> io::Result { let origin = Instant::now(); let time_origin = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_secs_f64() * 1000.0; let shared = Arc::new((Mutex::new(State::default()), Condvar::new())); let worker = shared.clone(); let worker = std::thread::Builder::new() .name("avril:timers".into()) .spawn(move || { let (lock, wake) = &*worker; let mut state = lock.lock().unwrap(); loop { if state.stopped { return; } if let Some((deadline, _)) = &state.pending { let now = Instant::now(); if *deadline <= now { let (_, reply) = state.pending.take().unwrap(); let _ = reply.send(()); } else { let delay = *deadline - now; state = wake.wait_timeout(state, delay).unwrap().0; } } else { state = wake.wait(state).unwrap(); } } })?; Ok(Self { origin, time_origin, shared, worker: Some(worker), }) } fn wait(&self, deadline: f64) -> io::Result> { let offset = Duration::try_from_secs_f64(deadline / 1000.0).map_err(io::Error::other)?; let deadline = self .origin .checked_add(offset) .ok_or_else(|| io::Error::other("Timer deadline is out of range"))?; let (reply, result) = oneshot::channel(); let mut state = self.shared.0.lock().unwrap(); state.pending.take(); if deadline <= Instant::now() { let _ = reply.send(()); } else { state.pending = Some((deadline, reply)); } self.shared.1.notify_one(); Ok(result) } fn cancel(&self) { self.shared.0.lock().unwrap().pending.take(); self.shared.1.notify_one(); } } impl Drop for Timers { fn drop(&mut self) { { let mut state = self.shared.0.lock().unwrap(); state.stopped = true; state.pending.take(); self.shared.1.notify_one(); } if let Some(worker) = self.worker.take() { let _ = worker.join(); } } } pub fn init() -> io::Result { let timers = Rc::new(Timers::new()?); let (now, time_origin, wait, wait_unreferenced, cancel) = ( timers.clone(), timers.clone(), timers.clone(), timers.clone(), timers, ); Ok(Extension { name: "ext:timers", dependencies: &[], ops: vec![ Op::new("op_timer_now", move |(): ()| { Ok::<_, Infallible>(now.origin.elapsed().as_secs_f64() * 1000.0) }), Op::new("op_timer_time_origin", move |(): ()| { Ok::<_, Infallible>(time_origin.time_origin) }), Op::asynchronous("op_timer_wait", move |deadline: f64| { let result = wait.wait(deadline); async move { let _ = result?.await; Ok::<_, io::Error>(()) } }), Op::asynchronous_unreferenced("op_timer_wait_unreferenced", move |deadline: f64| { let result = wait_unreferenced.wait(deadline); async move { let _ = result?.await; Ok::<_, io::Error>(()) } }), Op::new("op_timer_cancel", move |(): ()| { cancel.cancel(); Ok::<_, Infallible>(()) }), ], modules: &[ ( "ext:timers/main.js", include_str!(concat!(env!("OUT_DIR"), "/main.js")), ), ( "ext:timers/timers.js", include_str!(concat!(env!("OUT_DIR"), "/timers.js")), ), ], entry_point: Some("ext:timers/main.js"), }) } #[cfg(test)] mod tests { use avril_core::{Runtime, RuntimeOptions}; fn runtime() -> Runtime { Runtime::new(RuntimeOptions { extensions: vec![ avril_ext_web::with_broker(std::sync::Arc::default()), super::init().unwrap(), ], ..Default::default() }) .unwrap() } #[test] fn callback_failure_does_not_disarm_remaining_timers() { let mut runtime = runtime(); runtime .execute_script::<()>( "timers", r#" globalThis.calls = 0; setTimeout(() => {throw Error('timer failed');}); setTimeout(() => calls++); "#, ) .unwrap(); assert!( runtime .run_event_loop() .unwrap_err() .to_string() .contains("timer failed") ); runtime.run_event_loop().unwrap(); assert_eq!(runtime.execute_script::("calls", "calls").unwrap(), 1); } #[test] fn microtask_failures_drain_the_queue_without_poisoning_later_turns() { let mut runtime = runtime(); let error = runtime .execute_script::<()>( "microtasks", r#" globalThis.calls = []; queueMicrotask(() => { calls.push('first'); queueMicrotask(() => calls.push('nested')); throw Error('first failure'); }); queueMicrotask(() => { calls.push('second'); throw Error('second failure'); }); Promise.resolve().then(() => calls.push('promise')); "#, ) .unwrap_err() .to_string(); assert!(error.contains("first failure"), "{error}"); assert_eq!( runtime .execute_script::>("calls", "calls") .unwrap(), ["first", "second", "promise", "nested"] ); runtime.run_event_loop().unwrap(); runtime .execute_script::<()>( "caught", "queueMicrotask(() => {try {throw Error('caught');} catch {}});", ) .unwrap(); } #[test] fn microtasks_report_non_error_exceptions() { let mut runtime = runtime(); for value in ["undefined", "null", "42", "'failure'"] { let error = runtime .execute_script::<()>( "throw", &format!("queueMicrotask(() => {{throw {value};}});"), ) .unwrap_err() .to_string(); assert!(error.contains(value.trim_matches('\'')), "{error}"); runtime.run_event_loop().unwrap(); } } #[test] fn dropping_inside_an_async_embedder_cancels_pending_waits() { let executor = tokio::runtime::Builder::new_current_thread() .build() .unwrap(); executor.block_on(async { let mut runtime = runtime(); runtime .execute_script::<()>("pending", "setTimeout(() => {}, 1_000_000);") .unwrap(); drop(runtime); }); } }