Skip to main content

nautilus_common/clock/
api.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! User-facing facade over clock operations.
17
18use std::{
19    cell::{Ref, RefCell, RefMut},
20    fmt::Debug,
21    time::Duration,
22};
23
24use jiff::Timestamp;
25use nautilus_core::{
26    DurationNanos, UnixNanos,
27    datetime::{NANOSECONDS_IN_SECOND, try_datetime_to_unix_nanos},
28};
29use ustr::Ustr;
30
31use super::{Clock, duration_to_nanos};
32use crate::{component::ComponentAccessError, timer::TimeEventCallback};
33
34/// Provides a user-facing facade over clock operations.
35///
36/// Calls delegate to either a borrowed [`Clock`] or a set of operation handlers.
37/// Panics from supplied operation handlers propagate to the caller.
38#[derive(Debug)]
39pub struct ClockApi<'a> {
40    backing: ClockApiBacking<'a>,
41}
42
43impl<'a> ClockApi<'a> {
44    pub(crate) fn new(clock: &'a RefCell<dyn Clock>) -> Self {
45        Self {
46            backing: ClockApiBacking::Native(clock),
47        }
48    }
49
50    /// Creates a clock API backed by the supplied operation handlers.
51    ///
52    /// The nanosecond timestamp handler also supplies the derived UTC, second, millisecond, and
53    /// microsecond values. Timestamp-based scheduling methods convert their inputs before invoking
54    /// the corresponding nanosecond handler.
55    #[doc(hidden)]
56    #[must_use]
57    #[expect(
58        clippy::too_many_arguments,
59        reason = "clock API backing mirrors the full ClockApi surface"
60    )]
61    pub fn from_handlers<
62        TimestampNs,
63        SetTimeAlertNs,
64        SetTimerNs,
65        TimerNames,
66        TimerCount,
67        TimerExists,
68        NextTimeNs,
69        CancelTimer,
70        CancelTimers,
71    >(
72        timestamp_ns: TimestampNs,
73        set_time_alert_ns: SetTimeAlertNs,
74        set_timer_ns: SetTimerNs,
75        timer_names: TimerNames,
76        timer_count: TimerCount,
77        timer_exists: TimerExists,
78        next_time_ns: NextTimeNs,
79        cancel_timer: CancelTimer,
80        cancel_timers: CancelTimers,
81    ) -> Self
82    where
83        TimestampNs: Fn() -> UnixNanos + 'a,
84        SetTimeAlertNs:
85            Fn(&str, UnixNanos, Option<TimeEventCallback>, Option<bool>) -> anyhow::Result<()> + 'a,
86        SetTimerNs: Fn(
87                &str,
88                DurationNanos,
89                Option<UnixNanos>,
90                Option<UnixNanos>,
91                Option<TimeEventCallback>,
92                Option<bool>,
93                Option<bool>,
94            ) -> anyhow::Result<()>
95            + 'a,
96        TimerNames: Fn() -> Vec<String> + 'a,
97        TimerCount: Fn() -> usize + 'a,
98        TimerExists: Fn(&str) -> bool + 'a,
99        NextTimeNs: Fn(&str) -> Option<UnixNanos> + 'a,
100        CancelTimer: Fn(&str) + 'a,
101        CancelTimers: Fn() + 'a,
102    {
103        Self {
104            backing: ClockApiBacking::Handlers(ClockApiHandlers {
105                timestamp_ns: Box::new(timestamp_ns),
106                set_time_alert_ns: Box::new(set_time_alert_ns),
107                set_timer_ns: Box::new(set_timer_ns),
108                timer_names: Box::new(timer_names),
109                timer_count: Box::new(timer_count),
110                timer_exists: Box::new(timer_exists),
111                next_time_ns: Box::new(next_time_ns),
112                cancel_timer: Box::new(cancel_timer),
113                cancel_timers: Box::new(cancel_timers),
114            }),
115        }
116    }
117
118    /// Returns the current UNIX timestamp in nanoseconds.
119    ///
120    /// # Panics
121    ///
122    /// With native backing, panics if the clock is already mutably borrowed.
123    #[must_use]
124    pub fn timestamp_ns(&self) -> UnixNanos {
125        match &self.backing {
126            ClockApiBacking::Native(clock) => clock_ref(clock, "timestamp_ns").timestamp_ns(),
127            ClockApiBacking::Handlers(handlers) => (handlers.timestamp_ns)(),
128        }
129    }
130
131    /// Returns the current UNIX timestamp in microseconds.
132    ///
133    /// # Panics
134    ///
135    /// With native backing, panics if the clock is already mutably borrowed.
136    #[must_use]
137    pub fn timestamp_us(&self) -> u64 {
138        match &self.backing {
139            ClockApiBacking::Native(clock) => clock_ref(clock, "timestamp_us").timestamp_us(),
140            ClockApiBacking::Handlers(handlers) => (handlers.timestamp_ns)().as_micros(),
141        }
142    }
143
144    /// Returns the current UNIX timestamp in milliseconds.
145    ///
146    /// # Panics
147    ///
148    /// With native backing, panics if the clock is already mutably borrowed.
149    #[must_use]
150    pub fn timestamp_ms(&self) -> u64 {
151        match &self.backing {
152            ClockApiBacking::Native(clock) => clock_ref(clock, "timestamp_ms").timestamp_ms(),
153            ClockApiBacking::Handlers(handlers) => (handlers.timestamp_ns)().as_millis(),
154        }
155    }
156
157    /// Returns the current UNIX timestamp in seconds.
158    ///
159    /// # Panics
160    ///
161    /// With native backing, panics if the clock is already mutably borrowed.
162    #[must_use]
163    pub fn timestamp(&self) -> f64 {
164        match &self.backing {
165            ClockApiBacking::Native(clock) => clock_ref(clock, "timestamp").timestamp(),
166            ClockApiBacking::Handlers(handlers) => {
167                (handlers.timestamp_ns)().as_f64() / (NANOSECONDS_IN_SECOND as f64)
168            }
169        }
170    }
171
172    /// Returns the current UTC timestamp.
173    ///
174    /// # Panics
175    ///
176    /// With native backing, panics if the clock is already mutably borrowed.
177    #[must_use]
178    pub fn utc_now(&self) -> Timestamp {
179        match &self.backing {
180            ClockApiBacking::Native(clock) => clock_ref(clock, "utc_now").utc_now(),
181            ClockApiBacking::Handlers(handlers) => (handlers.timestamp_ns)().to_datetime_utc(),
182        }
183    }
184
185    /// Sets a time alert for the specified UTC timestamp.
186    ///
187    /// See [`Clock::set_time_alert`] for timing and callback selection semantics.
188    ///
189    /// # Errors
190    ///
191    /// Returns:
192    /// - An error if the timestamp cannot be converted to [`UnixNanos`] or the backing clock
193    ///   rejects the alert.
194    /// - [`ComponentAccessError`] if the native clock is already borrowed.
195    pub fn set_time_alert(
196        &self,
197        name: &str,
198        alert_time: Timestamp,
199        callback: Option<TimeEventCallback>,
200        allow_past: Option<bool>,
201    ) -> anyhow::Result<()> {
202        match &self.backing {
203            ClockApiBacking::Native(clock) => clock_mut(clock, "set_time_alert")?
204                .set_time_alert(name, alert_time, callback, allow_past),
205            ClockApiBacking::Handlers(handlers) => (handlers.set_time_alert_ns)(
206                name,
207                try_datetime_to_unix_nanos(alert_time)?,
208                callback,
209                allow_past,
210            ),
211        }
212    }
213
214    /// Sets a time alert for the specified UNIX nanosecond timestamp.
215    ///
216    /// See [`Clock::set_time_alert_ns`] for timing and callback selection semantics.
217    ///
218    /// # Errors
219    ///
220    /// Returns:
221    /// - An error if the backing clock rejects the alert.
222    /// - [`ComponentAccessError`] if the native clock is already borrowed.
223    pub fn set_time_alert_ns(
224        &self,
225        name: &str,
226        alert_time_ns: UnixNanos,
227        callback: Option<TimeEventCallback>,
228        allow_past: Option<bool>,
229    ) -> anyhow::Result<()> {
230        match &self.backing {
231            ClockApiBacking::Native(clock) => clock_mut(clock, "set_time_alert_ns")?
232                .set_time_alert_ns(name, alert_time_ns, callback, allow_past),
233            ClockApiBacking::Handlers(handlers) => {
234                (handlers.set_time_alert_ns)(name, alert_time_ns, callback, allow_past)
235            }
236        }
237    }
238
239    /// Sets an interval timer using UTC timestamps.
240    ///
241    /// See [`Clock::set_timer`] for scheduling and callback selection semantics.
242    ///
243    /// # Errors
244    ///
245    /// Returns:
246    /// - An error if the interval exceeds `u64::MAX` nanoseconds, a timestamp cannot be
247    ///   converted to [`UnixNanos`], or the backing clock rejects the timer.
248    /// - [`ComponentAccessError`] if the native clock is already borrowed.
249    #[expect(clippy::too_many_arguments, reason = "timer scheduling mirrors Clock")]
250    pub fn set_timer(
251        &self,
252        name: &str,
253        interval: Duration,
254        start_time: Option<Timestamp>,
255        stop_time: Option<Timestamp>,
256        callback: Option<TimeEventCallback>,
257        allow_past: Option<bool>,
258        fire_immediately: Option<bool>,
259    ) -> anyhow::Result<()> {
260        match &self.backing {
261            ClockApiBacking::Native(clock) => clock_mut(clock, "set_timer")?.set_timer(
262                name,
263                interval,
264                start_time,
265                stop_time,
266                callback,
267                allow_past,
268                fire_immediately,
269            ),
270            ClockApiBacking::Handlers(handlers) => (handlers.set_timer_ns)(
271                name,
272                duration_to_nanos(interval)?,
273                start_time.map(try_datetime_to_unix_nanos).transpose()?,
274                stop_time.map(try_datetime_to_unix_nanos).transpose()?,
275                callback,
276                allow_past,
277                fire_immediately,
278            ),
279        }
280    }
281
282    /// Sets an interval timer using UNIX nanosecond timestamps.
283    ///
284    /// See [`Clock::set_timer_ns`] for scheduling and callback selection semantics.
285    ///
286    /// # Errors
287    ///
288    /// Returns:
289    /// - An error if the backing clock rejects the timer.
290    /// - [`ComponentAccessError`] if the native clock is already borrowed.
291    #[expect(clippy::too_many_arguments, reason = "timer scheduling mirrors Clock")]
292    pub fn set_timer_ns(
293        &self,
294        name: &str,
295        interval_ns: DurationNanos,
296        start_time_ns: Option<UnixNanos>,
297        stop_time_ns: Option<UnixNanos>,
298        callback: Option<TimeEventCallback>,
299        allow_past: Option<bool>,
300        fire_immediately: Option<bool>,
301    ) -> anyhow::Result<()> {
302        match &self.backing {
303            ClockApiBacking::Native(clock) => clock_mut(clock, "set_timer_ns")?.set_timer_ns(
304                name,
305                interval_ns,
306                start_time_ns,
307                stop_time_ns,
308                callback,
309                allow_past,
310                fire_immediately,
311            ),
312            ClockApiBacking::Handlers(handlers) => (handlers.set_timer_ns)(
313                name,
314                interval_ns,
315                start_time_ns,
316                stop_time_ns,
317                callback,
318                allow_past,
319                fire_immediately,
320            ),
321        }
322    }
323
324    /// Returns the names of active timers.
325    ///
326    /// # Panics
327    ///
328    /// With native backing, panics if the clock is already mutably borrowed.
329    #[must_use]
330    pub fn timer_names(&self) -> Vec<String> {
331        match &self.backing {
332            ClockApiBacking::Native(clock) => clock_ref(clock, "timer_names")
333                .timer_names()
334                .into_iter()
335                .map(str::to_string)
336                .collect(),
337            ClockApiBacking::Handlers(handlers) => (handlers.timer_names)(),
338        }
339    }
340
341    /// Returns the count of active timers.
342    ///
343    /// # Panics
344    ///
345    /// With native backing, panics if the clock is already mutably borrowed.
346    #[must_use]
347    pub fn timer_count(&self) -> usize {
348        match &self.backing {
349            ClockApiBacking::Native(clock) => clock_ref(clock, "timer_count").timer_count(),
350            ClockApiBacking::Handlers(handlers) => (handlers.timer_count)(),
351        }
352    }
353
354    /// Returns whether an active timer named `name` exists.
355    ///
356    /// # Panics
357    ///
358    /// With native backing, panics if the clock is already mutably borrowed.
359    #[must_use]
360    pub fn timer_exists(&self, name: &str) -> bool {
361        match &self.backing {
362            ClockApiBacking::Native(clock) => {
363                clock_ref(clock, "timer_exists").timer_exists(&Ustr::from(name))
364            }
365            ClockApiBacking::Handlers(handlers) => (handlers.timer_exists)(name),
366        }
367    }
368
369    /// Returns the next trigger timestamp for the active timer named `name`.
370    ///
371    /// Returns `None` if no active timer with that name exists.
372    ///
373    /// # Panics
374    ///
375    /// With native backing, panics if the clock is already mutably borrowed.
376    #[must_use]
377    pub fn next_time_ns(&self, name: &str) -> Option<UnixNanos> {
378        match &self.backing {
379            ClockApiBacking::Native(clock) => clock_ref(clock, "next_time_ns").next_time_ns(name),
380            ClockApiBacking::Handlers(handlers) => (handlers.next_time_ns)(name),
381        }
382    }
383
384    /// Cancels the timer named `name`, if it exists.
385    ///
386    /// # Panics
387    ///
388    /// With native backing, panics if the clock is already borrowed.
389    pub fn cancel_timer(&self, name: &str) {
390        match &self.backing {
391            ClockApiBacking::Native(clock) => clock_mut(clock, "cancel_timer")
392                .unwrap_or_else(|e| panic!("{e}"))
393                .cancel_timer(name),
394            ClockApiBacking::Handlers(handlers) => (handlers.cancel_timer)(name),
395        }
396    }
397
398    /// Cancels all timers.
399    ///
400    /// # Panics
401    ///
402    /// With native backing, panics if the clock is already borrowed.
403    pub fn cancel_timers(&self) {
404        match &self.backing {
405            ClockApiBacking::Native(clock) => clock_mut(clock, "cancel_timers")
406                .unwrap_or_else(|e| panic!("{e}"))
407                .cancel_timers(),
408            ClockApiBacking::Handlers(handlers) => (handlers.cancel_timers)(),
409        }
410    }
411}
412
413enum ClockApiBacking<'a> {
414    Native(&'a RefCell<dyn Clock>),
415    Handlers(ClockApiHandlers<'a>),
416}
417
418struct ClockApiHandlers<'a> {
419    timestamp_ns: Box<dyn Fn() -> UnixNanos + 'a>,
420    set_time_alert_ns: Box<SetTimeAlertNsHandler<'a>>,
421    set_timer_ns: Box<SetTimerNsHandler<'a>>,
422    timer_names: Box<dyn Fn() -> Vec<String> + 'a>,
423    timer_count: Box<dyn Fn() -> usize + 'a>,
424    timer_exists: Box<dyn Fn(&str) -> bool + 'a>,
425    next_time_ns: Box<NextTimeNsHandler<'a>>,
426    cancel_timer: Box<dyn Fn(&str) + 'a>,
427    cancel_timers: Box<dyn Fn() + 'a>,
428}
429
430impl Debug for ClockApiBacking<'_> {
431    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
432        match self {
433            Self::Native(_) => f.write_str("Native"),
434            Self::Handlers(_) => f.write_str("Handlers"),
435        }
436    }
437}
438
439type SetTimeAlertNsHandler<'a> =
440    dyn Fn(&str, UnixNanos, Option<TimeEventCallback>, Option<bool>) -> anyhow::Result<()> + 'a;
441type NextTimeNsHandler<'a> = dyn Fn(&str) -> Option<UnixNanos> + 'a;
442type SetTimerNsHandler<'a> = dyn Fn(
443        &str,
444        DurationNanos,
445        Option<UnixNanos>,
446        Option<UnixNanos>,
447        Option<TimeEventCallback>,
448        Option<bool>,
449        Option<bool>,
450    ) -> anyhow::Result<()>
451    + 'a;
452
453fn clock_ref<'a>(clock: &'a RefCell<dyn Clock>, operation: &'static str) -> Ref<'a, dyn Clock> {
454    clock.try_borrow().unwrap_or_else(|_| {
455        panic!(
456            "{}",
457            ComponentAccessError::ReadConflict {
458                resource: "clock",
459                operation,
460            }
461        )
462    })
463}
464
465fn clock_mut<'a>(
466    clock: &'a RefCell<dyn Clock>,
467    operation: &'static str,
468) -> Result<RefMut<'a, dyn Clock>, ComponentAccessError> {
469    clock
470        .try_borrow_mut()
471        .map_err(|_| ComponentAccessError::WriteConflict {
472            resource: "clock",
473            operation,
474        })
475}