use std::cell::RefCell;
use std::collections::BTreeMap;
use std::marker::PhantomData;
use std::panic::{self, AssertUnwindSafe, Location};
use std::rc::Rc;
use std::time::{Duration, Instant};
use crabshell_reactive::{Owner, Signal, untrack};
use crate::cx::Cx;
use crate::input::panic_message;
const MIN_PERIOD: Duration = Duration::from_millis(1);
const MAX_PERIOD: Duration = Duration::from_secs(u32::MAX as u64);
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Timer {
id: u64,
_not_send: PhantomData<*const ()>,
}
struct Entry {
owner: Owner,
label: Rc<str>,
period: Duration,
repeats: bool,
deadline: Option<Instant>,
running: Signal<bool>,
callback: Option<Box<dyn FnMut()>>,
}
thread_local! {
static TIMERS: RefCell<(u64, BTreeMap<u64, Entry>)> = const { RefCell::new((0, BTreeMap::new())) };
}
impl Cx {
#[track_caller]
pub fn after(&self, delay: Duration, f: impl FnMut() + 'static) -> Timer {
let label = format!("cx.after at {}", Location::caller());
add(self, label, delay.min(MAX_PERIOD), false, Box::new(f))
}
#[track_caller]
pub fn interval(&self, period: Duration, f: impl FnMut() + 'static) -> Timer {
let label = format!("cx.interval at {}", Location::caller());
let period = period.clamp(MIN_PERIOD, MAX_PERIOD);
add(self, label, period, true, Box::new(f))
}
}
fn add(cx: &Cx, label: String, period: Duration, repeats: bool, f: Box<dyn FnMut()>) -> Timer {
let running = cx.signal(true);
let entry = Entry {
owner: cx.owner(),
label: label.into(),
period,
repeats,
deadline: Some(Instant::now() + period),
running,
callback: Some(f),
};
let id = TIMERS.with(|timers| {
let (next, table) = &mut *timers.borrow_mut();
*next += 1;
table.insert(*next, entry);
*next
});
Timer {
id,
_not_send: PhantomData,
}
}
impl Timer {
pub fn stop(&self) {
self.set_deadline(|_| None);
}
pub fn restart(&self) {
self.set_deadline(|period| Some(Instant::now() + period));
}
pub fn running(&self) -> bool {
let running = TIMERS.with(|timers| timers.borrow().1.get(&self.id).map(|entry| entry.running));
running
.and_then(|running| running.try_get())
.unwrap_or(false)
}
fn set_deadline(&self, deadline: impl FnOnce(Duration) -> Option<Instant>) {
let changed = TIMERS.with(|timers| {
let mut timers = timers.borrow_mut();
let entry = timers.1.get_mut(&self.id)?;
if entry.owner.is_disposed() {
return None;
}
entry.deadline = deadline(entry.period);
Some((entry.running, entry.deadline.is_some()))
});
if let Some((signal, running)) = changed {
set_running(signal, running);
}
}
}
fn set_running(signal: Signal<bool>, running: bool) {
if untrack(|| signal.try_with(|&was| was != running)) == Some(true) {
signal.try_set(running);
}
}
pub fn next_timer() -> Option<Instant> {
sweep();
TIMERS.with(|timers| {
let timers = timers.borrow();
timers.1.values().filter_map(|entry| entry.deadline).min()
})
}
pub fn fire_timers(now: Instant) {
sweep();
let mut due: Vec<(Instant, u64)> = TIMERS.with(|timers| {
let timers = timers.borrow();
let due = timers.1.iter().filter_map(|(&id, entry)| {
let deadline = entry.deadline.filter(|&deadline| deadline <= now)?;
Some((deadline, id))
});
due.collect()
});
due.sort_unstable();
for (_, id) in due {
fire(id, now);
}
}
fn fire(id: u64, now: Instant) {
let taken = TIMERS.with(|timers| {
let mut timers = timers.borrow_mut();
let entry = timers.1.get_mut(&id)?;
let deadline = entry.deadline.filter(|&deadline| deadline <= now)?;
if entry.owner.is_disposed() {
return None;
}
let callback = entry.callback.take()?;
entry.deadline = entry.repeats.then(|| {
let next = deadline + entry.period;
if next > now { next } else { now + entry.period }
});
let entry = &*entry;
Some((
callback,
Rc::clone(&entry.label),
entry.owner,
entry.running,
entry.deadline.is_some(),
))
});
let Some((mut callback, label, owner, running, again)) = taken else {
return;
};
if !again {
set_running(running, false);
}
let panicked = panic::catch_unwind(AssertUnwindSafe(&mut callback)).err();
if let Some(payload) = &panicked {
let place = owner
.name()
.map(|name| format!(" in {name}"))
.unwrap_or_default();
tracing::error!(
"timer {label}{place} panicked and stops: {}",
panic_message(payload.as_ref())
);
}
let orphan = TIMERS.with(|timers| {
let mut timers = timers.borrow_mut();
let Some(entry) = timers.1.get_mut(&id) else {
return Some(callback);
};
if panicked.is_some() {
entry.deadline = None;
}
entry.callback = Some(callback);
None
});
if panicked.is_some() {
set_running(running, false);
}
if let Some(callback) = orphan {
drop_isolated(&label, callback);
}
}
fn sweep() {
let gone: Vec<Entry> = TIMERS.with(|timers| {
let mut timers = timers.borrow_mut();
let ids: Vec<u64> = timers
.1
.iter()
.filter(|(_, entry)| entry.owner.is_disposed())
.map(|(&id, _)| id)
.collect();
ids.iter().filter_map(|id| timers.1.remove(id)).collect()
});
for entry in gone {
if let Some(callback) = entry.callback {
drop_isolated(&entry.label, callback);
}
}
}
fn drop_isolated(label: &str, callback: Box<dyn FnMut()>) {
if let Err(payload) = panic::catch_unwind(AssertUnwindSafe(move || drop(callback))) {
tracing::error!(
"dropping timer {label} panicked: {}",
panic_message(payload.as_ref())
);
}
}