2024-10-01 23:02:26 +10:00
|
|
|
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},
|
2024-10-05 19:28:39 +10:00
|
|
|
FrameId, GlobalMemoCell,
|
2024-10-01 23:02:26 +10:00
|
|
|
};
|
|
|
|
|
|
|
|
pub struct TimerHost {
|
|
|
|
timers: Vec<TimerRecord>,
|
2024-10-05 19:28:39 +10:00
|
|
|
frame_id: FrameId,
|
2024-10-01 23:02:26 +10:00
|
|
|
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 {
|
2024-10-05 19:28:39 +10:00
|
|
|
pub fn new(frame_id: FrameId) -> Self {
|
2024-10-01 23:02:26 +10:00
|
|
|
Self {
|
|
|
|
timers: vec![],
|
|
|
|
frame_id,
|
|
|
|
next_id: 1,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn add_record<AC>(
|
|
|
|
&mut self,
|
|
|
|
access_context: AC,
|
|
|
|
global: &GlobalMemoCell,
|
|
|
|
recurring: bool,
|
|
|
|
interval_millis: u32,
|
|
|
|
commands: String,
|
|
|
|
timer_name: Option<String>,
|
|
|
|
) -> anyhow::Result<String>
|
|
|
|
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<ParsedCommand> = parse_commands(&commands).commands;
|
|
|
|
let callback: Closure<dyn FnMut()> = 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::<u64>()
|
|
|
|
.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<Value<'gc>> {
|
|
|
|
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<F>(&self, f: F)
|
|
|
|
where
|
|
|
|
F: Fn(&mut TimerHost);
|
|
|
|
}
|
|
|
|
|
|
|
|
pub struct TimerRecord {
|
|
|
|
id: u64,
|
|
|
|
js_id: i32,
|
|
|
|
timer_name: Option<String>,
|
|
|
|
recurring: bool,
|
|
|
|
interval_millis: u32,
|
|
|
|
commands: String,
|
|
|
|
#[allow(dead_code)]
|
|
|
|
retained_closure: Closure<dyn FnMut()>,
|
|
|
|
}
|