use anyhow::bail; use piccolo::{Context, IntoValue, Table, Value}; use wasm_bindgen::{closure::Closure, JsCast}; use crate::{ command_handler::execute_queue, parsing::{parse_commands, ParsedCommand}, GlobalMemoCell, TermFrame, }; pub struct TimerHost { timers: Vec, frame_id: TermFrame, next_id: u64, } impl Drop for TimerHost { fn drop(&mut self) { if let Some(window) = web_sys::window() { for timer in &self.timers { if timer.recurring { window.clear_interval_with_handle(timer.js_id); } else { window.clear_timeout_with_handle(timer.js_id); } } } } } impl TimerHost { pub fn new(frame_id: TermFrame) -> Self { Self { timers: vec![], frame_id, next_id: 1, } } pub fn add_record( &mut self, access_context: AC, global: &GlobalMemoCell, recurring: bool, interval_millis: u32, commands: String, timer_name: Option, ) -> anyhow::Result where AC: TimerHostAccessContext + 'static, { let window = web_sys::window().ok_or_else(|| anyhow::Error::msg("Can't fetch window"))?; let new_id = self.next_id; self.next_id += 1; let global = global.clone(); if timer_name .as_ref() .map(|tn| tn.starts_with('#')) .unwrap_or(false) { bail!( "Timer name can't start with #, it is reserved for referencing timers by number." ); } let frame = self.frame_id.clone(); let callback_commands: Vec = parse_commands(&commands).commands; let callback: Closure = Closure::new(move || { let mut cq = global.command_queue.borrow_mut(); for cmd in &callback_commands { cq.push_back((frame.clone(), cmd.clone())); } drop(cq); execute_queue(&global); if !recurring { access_context.with_ref(|timer_host| { timer_host.timers.retain(|t| t.id != new_id); }); } }); let js_id = if recurring { window .set_interval_with_callback_and_timeout_and_arguments_0( callback.as_ref().unchecked_ref(), interval_millis as i32, ) .map_err(|_| anyhow::Error::msg("setInterval failed"))? } else { window .set_timeout_with_callback_and_timeout_and_arguments_0( callback.as_ref().unchecked_ref(), interval_millis as i32, ) .map_err(|_| anyhow::Error::msg("setTimeout failed"))? }; self.timers.push(TimerRecord { id: new_id, js_id, timer_name, recurring, interval_millis, commands, retained_closure: callback, }); Ok(format!("#{}", new_id)) } pub fn remove_timer(&mut self, timer_name: String) -> anyhow::Result<()> { let timer = if let Some(suffix) = timer_name.strip_prefix('#') { let id = suffix .parse::() .map_err(|_| anyhow::Error::msg("Should follow # with a number"))?; self.timers.iter().enumerate().find(|t| t.1.id == id) } else { self.timers .iter() .enumerate() .find(|t| t.1.timer_name.as_ref() == Some(&timer_name)) }; let timer = timer.ok_or_else(|| anyhow::Error::msg("No matching timer found."))?; let window = web_sys::window().ok_or_else(|| anyhow::Error::msg("Can't fetch window"))?; if timer.1.recurring { window.clear_interval_with_handle(timer.1.js_id); } else { window.clear_timeout_with_handle(timer.1.js_id); } self.timers.remove(timer.0); Ok(()) } pub fn to_value<'gc>(&self, ctx: Context<'gc>) -> anyhow::Result> { let tbl = Table::new(&ctx); for timer in self.timers.iter() { let timer_tbl = Table::new(&ctx); timer_tbl.set( ctx, "name", timer .timer_name .clone() .unwrap_or_else(|| format!("#{}", timer.id)), )?; timer_tbl.set(ctx, "commands", timer.commands.clone())?; timer_tbl.set(ctx, "recurring", timer.recurring)?; timer_tbl.set(ctx, "interval_millis", timer.interval_millis)?; tbl.set(ctx, timer.id as i64, timer_tbl)?; } Ok(tbl.into_value(ctx)) } } pub trait TimerHostAccessContext { fn with_ref(&self, f: F) where F: Fn(&mut TimerHost); } pub struct TimerRecord { id: u64, js_id: i32, timer_name: Option, recurring: bool, interval_millis: u32, commands: String, #[allow(dead_code)] retained_closure: Closure, }