use std::{
cell::{Ref, RefCell, RefMut},
fmt::Debug,
sync::Arc,
time::Duration,
};
use jiff::{Span, Timestamp, Zoned};
use nautilus_core::{
DurationNanos, UnixNanos,
datetime::{NANOSECONDS_IN_SECOND, try_datetime_to_unix_nanos},
};
use ustr::Ustr;
use super::{Clock, duration_to_nanos};
use crate::{
calendar::CalendarSchedule,
component::ComponentAccessError,
timer::{TimeEventCallback, TimerSchedule},
};
#[derive(Debug)]
pub struct ClockApi<'a> {
backing: ClockApiBacking<'a>,
}
impl<'a> ClockApi<'a> {
pub(crate) fn new(clock: &'a RefCell<dyn Clock>) -> Self {
Self {
backing: ClockApiBacking::Native(clock),
}
}
#[doc(hidden)]
#[must_use]
#[expect(
clippy::too_many_arguments,
reason = "clock API backing mirrors the full ClockApi surface"
)]
pub fn from_handlers<
TimestampNs,
SetTimeAlertNs,
SetTimerNs,
TimerNames,
TimerCount,
TimerExists,
NextTimeNs,
CancelTimer,
CancelTimers,
>(
timestamp_ns: TimestampNs,
set_time_alert_ns: SetTimeAlertNs,
set_timer_ns: SetTimerNs,
timer_names: TimerNames,
timer_count: TimerCount,
timer_exists: TimerExists,
next_time_ns: NextTimeNs,
cancel_timer: CancelTimer,
cancel_timers: CancelTimers,
) -> Self
where
TimestampNs: Fn() -> UnixNanos + 'a,
SetTimeAlertNs:
Fn(&str, UnixNanos, Option<TimeEventCallback>, Option<bool>) -> anyhow::Result<()> + 'a,
SetTimerNs: Fn(
&str,
DurationNanos,
Option<UnixNanos>,
Option<UnixNanos>,
Option<TimeEventCallback>,
Option<bool>,
Option<bool>,
) -> anyhow::Result<()>
+ 'a,
TimerNames: Fn() -> Vec<String> + 'a,
TimerCount: Fn() -> usize + 'a,
TimerExists: Fn(&str) -> bool + 'a,
NextTimeNs: Fn(&str) -> Option<UnixNanos> + 'a,
CancelTimer: Fn(&str) + 'a,
CancelTimers: Fn() + 'a,
{
Self {
backing: ClockApiBacking::Handlers(ClockApiHandlers {
timestamp_ns: Box::new(timestamp_ns),
set_time_alert_ns: Box::new(set_time_alert_ns),
set_timer_ns: Box::new(set_timer_ns),
timer_names: Box::new(timer_names),
timer_count: Box::new(timer_count),
timer_exists: Box::new(timer_exists),
next_time_ns: Box::new(next_time_ns),
cancel_timer: Box::new(cancel_timer),
cancel_timers: Box::new(cancel_timers),
}),
}
}
#[must_use]
pub fn timestamp_ns(&self) -> UnixNanos {
match &self.backing {
ClockApiBacking::Native(clock) => clock_ref(clock, "timestamp_ns").timestamp_ns(),
ClockApiBacking::Handlers(handlers) => (handlers.timestamp_ns)(),
}
}
#[must_use]
pub fn timestamp_us(&self) -> u64 {
match &self.backing {
ClockApiBacking::Native(clock) => clock_ref(clock, "timestamp_us").timestamp_us(),
ClockApiBacking::Handlers(handlers) => (handlers.timestamp_ns)().as_micros(),
}
}
#[must_use]
pub fn timestamp_ms(&self) -> u64 {
match &self.backing {
ClockApiBacking::Native(clock) => clock_ref(clock, "timestamp_ms").timestamp_ms(),
ClockApiBacking::Handlers(handlers) => (handlers.timestamp_ns)().as_millis(),
}
}
#[must_use]
pub fn timestamp(&self) -> f64 {
match &self.backing {
ClockApiBacking::Native(clock) => clock_ref(clock, "timestamp").timestamp(),
ClockApiBacking::Handlers(handlers) => {
(handlers.timestamp_ns)().as_f64() / (NANOSECONDS_IN_SECOND as f64)
}
}
}
#[must_use]
pub fn utc_now(&self) -> Timestamp {
match &self.backing {
ClockApiBacking::Native(clock) => clock_ref(clock, "utc_now").utc_now(),
ClockApiBacking::Handlers(handlers) => (handlers.timestamp_ns)().to_datetime_utc(),
}
}
pub fn set_time_alert(
&self,
name: &str,
alert_time: Timestamp,
callback: Option<TimeEventCallback>,
allow_past: Option<bool>,
) -> anyhow::Result<()> {
match &self.backing {
ClockApiBacking::Native(clock) => clock_mut(clock, "set_time_alert")?
.set_time_alert(name, alert_time, callback, allow_past),
ClockApiBacking::Handlers(handlers) => (handlers.set_time_alert_ns)(
name,
try_datetime_to_unix_nanos(alert_time)?,
callback,
allow_past,
),
}
}
pub fn set_time_alert_ns(
&self,
name: &str,
alert_time_ns: UnixNanos,
callback: Option<TimeEventCallback>,
allow_past: Option<bool>,
) -> anyhow::Result<()> {
match &self.backing {
ClockApiBacking::Native(clock) => clock_mut(clock, "set_time_alert_ns")?
.set_time_alert_ns(name, alert_time_ns, callback, allow_past),
ClockApiBacking::Handlers(handlers) => {
(handlers.set_time_alert_ns)(name, alert_time_ns, callback, allow_past)
}
}
}
#[expect(clippy::too_many_arguments, reason = "timer scheduling mirrors Clock")]
pub fn set_timer(
&self,
name: &str,
interval: Duration,
start_time: Option<Timestamp>,
stop_time: Option<Timestamp>,
callback: Option<TimeEventCallback>,
allow_past: Option<bool>,
fire_immediately: Option<bool>,
) -> anyhow::Result<()> {
match &self.backing {
ClockApiBacking::Native(clock) => clock_mut(clock, "set_timer")?.set_timer(
name,
interval,
start_time,
stop_time,
callback,
allow_past,
fire_immediately,
),
ClockApiBacking::Handlers(handlers) => (handlers.set_timer_ns)(
name,
duration_to_nanos(interval)?,
start_time.map(try_datetime_to_unix_nanos).transpose()?,
stop_time.map(try_datetime_to_unix_nanos).transpose()?,
callback,
allow_past,
fire_immediately,
),
}
}
#[expect(clippy::too_many_arguments, reason = "timer scheduling mirrors Clock")]
pub fn set_timer_ns(
&self,
name: &str,
interval_ns: DurationNanos,
start_time_ns: Option<UnixNanos>,
stop_time_ns: Option<UnixNanos>,
callback: Option<TimeEventCallback>,
allow_past: Option<bool>,
fire_immediately: Option<bool>,
) -> anyhow::Result<()> {
match &self.backing {
ClockApiBacking::Native(clock) => clock_mut(clock, "set_timer_ns")?.set_timer_ns(
name,
interval_ns,
start_time_ns,
stop_time_ns,
callback,
allow_past,
fire_immediately,
),
ClockApiBacking::Handlers(handlers) => (handlers.set_timer_ns)(
name,
interval_ns,
start_time_ns,
stop_time_ns,
callback,
allow_past,
fire_immediately,
),
}
}
pub fn set_timer_schedule(
&self,
name: &str,
schedule: Arc<dyn TimerSchedule>,
stop_time_ns: Option<UnixNanos>,
callback: Option<TimeEventCallback>,
allow_past: Option<bool>,
fire_immediately: Option<bool>,
) -> anyhow::Result<()> {
match &self.backing {
ClockApiBacking::Native(clock) => clock_mut(clock, "set_timer_schedule")?
.set_timer_schedule(
name,
schedule,
stop_time_ns,
callback,
allow_past,
fire_immediately,
),
ClockApiBacking::Handlers(_) => {
anyhow::bail!("Timer '{name}' needs a native clock for a deadline schedule")
}
}
}
#[expect(clippy::too_many_arguments, reason = "timer scheduling mirrors Clock")]
pub fn set_timer_zoned(
&self,
name: &str,
interval: Span,
start_time: &Zoned,
stop_time: Option<Timestamp>,
callback: Option<TimeEventCallback>,
allow_past: Option<bool>,
fire_immediately: Option<bool>,
) -> anyhow::Result<()> {
let schedule = CalendarSchedule::new(start_time, interval)?;
self.set_timer_schedule(
name,
Arc::new(schedule),
stop_time.map(try_datetime_to_unix_nanos).transpose()?,
callback,
allow_past,
fire_immediately,
)
}
#[must_use]
pub fn timer_names(&self) -> Vec<String> {
match &self.backing {
ClockApiBacking::Native(clock) => clock_ref(clock, "timer_names")
.timer_names()
.into_iter()
.map(str::to_string)
.collect(),
ClockApiBacking::Handlers(handlers) => (handlers.timer_names)(),
}
}
#[must_use]
pub fn timer_count(&self) -> usize {
match &self.backing {
ClockApiBacking::Native(clock) => clock_ref(clock, "timer_count").timer_count(),
ClockApiBacking::Handlers(handlers) => (handlers.timer_count)(),
}
}
#[must_use]
pub fn timer_exists(&self, name: &str) -> bool {
match &self.backing {
ClockApiBacking::Native(clock) => {
clock_ref(clock, "timer_exists").timer_exists(&Ustr::from(name))
}
ClockApiBacking::Handlers(handlers) => (handlers.timer_exists)(name),
}
}
#[must_use]
pub fn next_time_ns(&self, name: &str) -> Option<UnixNanos> {
match &self.backing {
ClockApiBacking::Native(clock) => clock_ref(clock, "next_time_ns").next_time_ns(name),
ClockApiBacking::Handlers(handlers) => (handlers.next_time_ns)(name),
}
}
pub fn cancel_timer(&self, name: &str) {
match &self.backing {
ClockApiBacking::Native(clock) => clock_mut(clock, "cancel_timer")
.unwrap_or_else(|e| panic!("{e}"))
.cancel_timer(name),
ClockApiBacking::Handlers(handlers) => (handlers.cancel_timer)(name),
}
}
pub fn cancel_timers(&self) {
match &self.backing {
ClockApiBacking::Native(clock) => clock_mut(clock, "cancel_timers")
.unwrap_or_else(|e| panic!("{e}"))
.cancel_timers(),
ClockApiBacking::Handlers(handlers) => (handlers.cancel_timers)(),
}
}
}
enum ClockApiBacking<'a> {
Native(&'a RefCell<dyn Clock>),
Handlers(ClockApiHandlers<'a>),
}
struct ClockApiHandlers<'a> {
timestamp_ns: Box<dyn Fn() -> UnixNanos + 'a>,
set_time_alert_ns: Box<SetTimeAlertNsHandler<'a>>,
set_timer_ns: Box<SetTimerNsHandler<'a>>,
timer_names: Box<dyn Fn() -> Vec<String> + 'a>,
timer_count: Box<dyn Fn() -> usize + 'a>,
timer_exists: Box<dyn Fn(&str) -> bool + 'a>,
next_time_ns: Box<NextTimeNsHandler<'a>>,
cancel_timer: Box<dyn Fn(&str) + 'a>,
cancel_timers: Box<dyn Fn() + 'a>,
}
impl Debug for ClockApiBacking<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Native(_) => f.write_str("Native"),
Self::Handlers(_) => f.write_str("Handlers"),
}
}
}
type SetTimeAlertNsHandler<'a> =
dyn Fn(&str, UnixNanos, Option<TimeEventCallback>, Option<bool>) -> anyhow::Result<()> + 'a;
type NextTimeNsHandler<'a> = dyn Fn(&str) -> Option<UnixNanos> + 'a;
type SetTimerNsHandler<'a> = dyn Fn(
&str,
DurationNanos,
Option<UnixNanos>,
Option<UnixNanos>,
Option<TimeEventCallback>,
Option<bool>,
Option<bool>,
) -> anyhow::Result<()>
+ 'a;
fn clock_ref<'a>(clock: &'a RefCell<dyn Clock>, operation: &'static str) -> Ref<'a, dyn Clock> {
clock.try_borrow().unwrap_or_else(|_| {
panic!(
"{}",
ComponentAccessError::ReadConflict {
resource: "clock",
operation,
}
)
})
}
fn clock_mut<'a>(
clock: &'a RefCell<dyn Clock>,
operation: &'static str,
) -> Result<RefMut<'a, dyn Clock>, ComponentAccessError> {
clock
.try_borrow_mut()
.map_err(|_| ComponentAccessError::WriteConflict {
resource: "clock",
operation,
})
}