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    sync::Arc,
22    time::Duration,
23};
24
25use jiff::{Span, Timestamp, Zoned};
26use nautilus_core::{
27    DurationNanos, UnixNanos,
28    datetime::{NANOSECONDS_IN_SECOND, try_datetime_to_unix_nanos},
29};
30use ustr::Ustr;
31
32use super::{Clock, duration_to_nanos};
33use crate::{
34    calendar::CalendarSchedule,
35    component::ComponentAccessError,
36    timer::{TimeEventCallback, TimerSchedule},
37};
38
39/// Provides a user-facing facade over clock operations.
40///
41/// Calls delegate to either a borrowed [`Clock`] or a set of operation handlers.
42/// Panics from supplied operation handlers propagate to the caller.
43#[derive(Debug)]
44pub struct ClockApi<'a> {
45    backing: ClockApiBacking<'a>,
46}
47
48impl<'a> ClockApi<'a> {
49    pub(crate) fn new(clock: &'a RefCell<dyn Clock>) -> Self {
50        Self {
51            backing: ClockApiBacking::Native(clock),
52        }
53    }
54
55    /// Creates a clock API backed by the supplied operation handlers.
56    ///
57    /// The nanosecond timestamp handler also supplies the derived UTC, second, millisecond, and
58    /// microsecond values. Timestamp-based scheduling methods convert their inputs before invoking
59    /// the corresponding nanosecond handler.
60    #[doc(hidden)]
61    #[must_use]
62    #[expect(
63        clippy::too_many_arguments,
64        reason = "clock API backing mirrors the full ClockApi surface"
65    )]
66    pub fn from_handlers<
67        TimestampNs,
68        SetTimeAlertNs,
69        SetTimerNs,
70        TimerNames,
71        TimerCount,
72        TimerExists,
73        NextTimeNs,
74        CancelTimer,
75        CancelTimers,
76    >(
77        timestamp_ns: TimestampNs,
78        set_time_alert_ns: SetTimeAlertNs,
79        set_timer_ns: SetTimerNs,
80        timer_names: TimerNames,
81        timer_count: TimerCount,
82        timer_exists: TimerExists,
83        next_time_ns: NextTimeNs,
84        cancel_timer: CancelTimer,
85        cancel_timers: CancelTimers,
86    ) -> Self
87    where
88        TimestampNs: Fn() -> UnixNanos + 'a,
89        SetTimeAlertNs:
90            Fn(&str, UnixNanos, Option<TimeEventCallback>, Option<bool>) -> anyhow::Result<()> + 'a,
91        SetTimerNs: Fn(
92                &str,
93                DurationNanos,
94                Option<UnixNanos>,
95                Option<UnixNanos>,
96                Option<TimeEventCallback>,
97                Option<bool>,
98                Option<bool>,
99            ) -> anyhow::Result<()>
100            + 'a,
101        TimerNames: Fn() -> Vec<String> + 'a,
102        TimerCount: Fn() -> usize + 'a,
103        TimerExists: Fn(&str) -> bool + 'a,
104        NextTimeNs: Fn(&str) -> Option<UnixNanos> + 'a,
105        CancelTimer: Fn(&str) + 'a,
106        CancelTimers: Fn() + 'a,
107    {
108        Self {
109            backing: ClockApiBacking::Handlers(ClockApiHandlers {
110                timestamp_ns: Box::new(timestamp_ns),
111                set_time_alert_ns: Box::new(set_time_alert_ns),
112                set_timer_ns: Box::new(set_timer_ns),
113                timer_names: Box::new(timer_names),
114                timer_count: Box::new(timer_count),
115                timer_exists: Box::new(timer_exists),
116                next_time_ns: Box::new(next_time_ns),
117                cancel_timer: Box::new(cancel_timer),
118                cancel_timers: Box::new(cancel_timers),
119            }),
120        }
121    }
122
123    /// Returns the current UNIX timestamp in nanoseconds.
124    ///
125    /// # Panics
126    ///
127    /// Panics if native backing is used and the clock is already mutably borrowed.
128    #[must_use]
129    pub fn timestamp_ns(&self) -> UnixNanos {
130        match &self.backing {
131            ClockApiBacking::Native(clock) => clock_ref(clock, "timestamp_ns").timestamp_ns(),
132            ClockApiBacking::Handlers(handlers) => (handlers.timestamp_ns)(),
133        }
134    }
135
136    /// Returns the current UNIX timestamp in microseconds.
137    ///
138    /// # Panics
139    ///
140    /// Panics if native backing is used and the clock is already mutably borrowed.
141    #[must_use]
142    pub fn timestamp_us(&self) -> u64 {
143        match &self.backing {
144            ClockApiBacking::Native(clock) => clock_ref(clock, "timestamp_us").timestamp_us(),
145            ClockApiBacking::Handlers(handlers) => (handlers.timestamp_ns)().as_micros(),
146        }
147    }
148
149    /// Returns the current UNIX timestamp in milliseconds.
150    ///
151    /// # Panics
152    ///
153    /// Panics if native backing is used and the clock is already mutably borrowed.
154    #[must_use]
155    pub fn timestamp_ms(&self) -> u64 {
156        match &self.backing {
157            ClockApiBacking::Native(clock) => clock_ref(clock, "timestamp_ms").timestamp_ms(),
158            ClockApiBacking::Handlers(handlers) => (handlers.timestamp_ns)().as_millis(),
159        }
160    }
161
162    /// Returns the current UNIX timestamp in seconds.
163    ///
164    /// # Panics
165    ///
166    /// Panics if native backing is used and the clock is already mutably borrowed.
167    #[must_use]
168    pub fn timestamp(&self) -> f64 {
169        match &self.backing {
170            ClockApiBacking::Native(clock) => clock_ref(clock, "timestamp").timestamp(),
171            ClockApiBacking::Handlers(handlers) => {
172                (handlers.timestamp_ns)().as_f64() / (NANOSECONDS_IN_SECOND as f64)
173            }
174        }
175    }
176
177    /// Returns the current UTC timestamp.
178    ///
179    /// # Panics
180    ///
181    /// Panics if native backing is used and the clock is already mutably borrowed.
182    #[must_use]
183    pub fn utc_now(&self) -> Timestamp {
184        match &self.backing {
185            ClockApiBacking::Native(clock) => clock_ref(clock, "utc_now").utc_now(),
186            ClockApiBacking::Handlers(handlers) => (handlers.timestamp_ns)().to_datetime_utc(),
187        }
188    }
189
190    /// Sets a time alert for the specified UTC timestamp.
191    ///
192    /// See [`Clock::set_time_alert`] for timing and callback selection semantics.
193    ///
194    /// # Errors
195    ///
196    /// Returns an error if:
197    /// - The timestamp cannot be converted to [`UnixNanos`] or the backing clock
198    ///   rejects the alert.
199    /// - The native clock is already borrowed.
200    pub fn set_time_alert(
201        &self,
202        name: &str,
203        alert_time: Timestamp,
204        callback: Option<TimeEventCallback>,
205        allow_past: Option<bool>,
206    ) -> anyhow::Result<()> {
207        match &self.backing {
208            ClockApiBacking::Native(clock) => clock_mut(clock, "set_time_alert")?
209                .set_time_alert(name, alert_time, callback, allow_past),
210            ClockApiBacking::Handlers(handlers) => (handlers.set_time_alert_ns)(
211                name,
212                try_datetime_to_unix_nanos(alert_time)?,
213                callback,
214                allow_past,
215            ),
216        }
217    }
218
219    /// Sets a time alert for the specified UNIX nanosecond timestamp.
220    ///
221    /// See [`Clock::set_time_alert_ns`] for timing and callback selection semantics.
222    ///
223    /// # Errors
224    ///
225    /// Returns an error if:
226    /// - The backing clock rejects the alert.
227    /// - The native clock is already borrowed.
228    pub fn set_time_alert_ns(
229        &self,
230        name: &str,
231        alert_time_ns: UnixNanos,
232        callback: Option<TimeEventCallback>,
233        allow_past: Option<bool>,
234    ) -> anyhow::Result<()> {
235        match &self.backing {
236            ClockApiBacking::Native(clock) => clock_mut(clock, "set_time_alert_ns")?
237                .set_time_alert_ns(name, alert_time_ns, callback, allow_past),
238            ClockApiBacking::Handlers(handlers) => {
239                (handlers.set_time_alert_ns)(name, alert_time_ns, callback, allow_past)
240            }
241        }
242    }
243
244    /// Sets an interval timer using UTC timestamps.
245    ///
246    /// See [`Clock::set_timer`] for scheduling and callback selection semantics.
247    ///
248    /// # Errors
249    ///
250    /// Returns an error if:
251    /// - The interval exceeds `u64::MAX` nanoseconds, a timestamp cannot be
252    ///   converted to [`UnixNanos`], or the backing clock rejects the timer.
253    /// - The native clock is already borrowed.
254    #[expect(clippy::too_many_arguments, reason = "timer scheduling mirrors Clock")]
255    pub fn set_timer(
256        &self,
257        name: &str,
258        interval: Duration,
259        start_time: Option<Timestamp>,
260        stop_time: Option<Timestamp>,
261        callback: Option<TimeEventCallback>,
262        allow_past: Option<bool>,
263        fire_immediately: Option<bool>,
264    ) -> anyhow::Result<()> {
265        match &self.backing {
266            ClockApiBacking::Native(clock) => clock_mut(clock, "set_timer")?.set_timer(
267                name,
268                interval,
269                start_time,
270                stop_time,
271                callback,
272                allow_past,
273                fire_immediately,
274            ),
275            ClockApiBacking::Handlers(handlers) => (handlers.set_timer_ns)(
276                name,
277                duration_to_nanos(interval)?,
278                start_time.map(try_datetime_to_unix_nanos).transpose()?,
279                stop_time.map(try_datetime_to_unix_nanos).transpose()?,
280                callback,
281                allow_past,
282                fire_immediately,
283            ),
284        }
285    }
286
287    /// Sets an interval timer using UNIX nanosecond timestamps.
288    ///
289    /// See [`Clock::set_timer_ns`] for scheduling and callback selection semantics.
290    ///
291    /// # Errors
292    ///
293    /// Returns an error if:
294    /// - The backing clock rejects the timer.
295    /// - The native clock is already borrowed.
296    #[expect(clippy::too_many_arguments, reason = "timer scheduling mirrors Clock")]
297    pub fn set_timer_ns(
298        &self,
299        name: &str,
300        interval_ns: DurationNanos,
301        start_time_ns: Option<UnixNanos>,
302        stop_time_ns: Option<UnixNanos>,
303        callback: Option<TimeEventCallback>,
304        allow_past: Option<bool>,
305        fire_immediately: Option<bool>,
306    ) -> anyhow::Result<()> {
307        match &self.backing {
308            ClockApiBacking::Native(clock) => clock_mut(clock, "set_timer_ns")?.set_timer_ns(
309                name,
310                interval_ns,
311                start_time_ns,
312                stop_time_ns,
313                callback,
314                allow_past,
315                fire_immediately,
316            ),
317            ClockApiBacking::Handlers(handlers) => (handlers.set_timer_ns)(
318                name,
319                interval_ns,
320                start_time_ns,
321                stop_time_ns,
322                callback,
323                allow_past,
324                fire_immediately,
325            ),
326        }
327    }
328
329    /// Registers a source of absolute UTC deadlines.
330    ///
331    /// See [`Clock::set_timer_schedule`] for timer lifecycle, flag, and callback semantics.
332    ///
333    /// # Errors
334    ///
335    /// Returns an error if:
336    /// - The backing clock rejects the schedule, or operation handlers back this API.
337    /// - The native clock is already borrowed.
338    pub fn set_timer_schedule(
339        &self,
340        name: &str,
341        schedule: Arc<dyn TimerSchedule>,
342        stop_time_ns: Option<UnixNanos>,
343        callback: Option<TimeEventCallback>,
344        allow_past: Option<bool>,
345        fire_immediately: Option<bool>,
346    ) -> anyhow::Result<()> {
347        match &self.backing {
348            ClockApiBacking::Native(clock) => clock_mut(clock, "set_timer_schedule")?
349                .set_timer_schedule(
350                    name,
351                    schedule,
352                    stop_time_ns,
353                    callback,
354                    allow_past,
355                    fire_immediately,
356                ),
357            ClockApiBacking::Handlers(_) => {
358                anyhow::bail!("Timer '{name}' needs a native clock for a deadline schedule")
359            }
360        }
361    }
362
363    /// Sets a calendar timer stepped in the timezone of `start_time`.
364    ///
365    /// Each occurrence is calculated from the original start. Days, weeks, months, and years
366    /// preserve local wall-clock time across DST changes; time-only spans use elapsed time.
367    /// Event timestamps remain UTC. See [`Clock::set_timer_ns`] for flags and callback selection.
368    ///
369    /// # Errors
370    ///
371    /// Returns an error if:
372    /// - The interval is not positive, or start or stop timestamps are outside
373    ///   the [`UnixNanos`] range.
374    /// - The backing clock rejects the schedule, or operation handlers back this API.
375    /// - The native clock is already borrowed.
376    #[expect(clippy::too_many_arguments, reason = "timer scheduling mirrors Clock")]
377    pub fn set_timer_zoned(
378        &self,
379        name: &str,
380        interval: Span,
381        start_time: &Zoned,
382        stop_time: Option<Timestamp>,
383        callback: Option<TimeEventCallback>,
384        allow_past: Option<bool>,
385        fire_immediately: Option<bool>,
386    ) -> anyhow::Result<()> {
387        let schedule = CalendarSchedule::new(start_time, interval)?;
388        self.set_timer_schedule(
389            name,
390            Arc::new(schedule),
391            stop_time.map(try_datetime_to_unix_nanos).transpose()?,
392            callback,
393            allow_past,
394            fire_immediately,
395        )
396    }
397
398    /// Returns the names of active timers.
399    ///
400    /// # Panics
401    ///
402    /// Panics if native backing is used and the clock is already mutably borrowed.
403    #[must_use]
404    pub fn timer_names(&self) -> Vec<String> {
405        match &self.backing {
406            ClockApiBacking::Native(clock) => clock_ref(clock, "timer_names")
407                .timer_names()
408                .into_iter()
409                .map(str::to_string)
410                .collect(),
411            ClockApiBacking::Handlers(handlers) => (handlers.timer_names)(),
412        }
413    }
414
415    /// Returns the count of active timers.
416    ///
417    /// # Panics
418    ///
419    /// Panics if native backing is used and the clock is already mutably borrowed.
420    #[must_use]
421    pub fn timer_count(&self) -> usize {
422        match &self.backing {
423            ClockApiBacking::Native(clock) => clock_ref(clock, "timer_count").timer_count(),
424            ClockApiBacking::Handlers(handlers) => (handlers.timer_count)(),
425        }
426    }
427
428    /// Returns whether an active timer named `name` exists.
429    ///
430    /// # Panics
431    ///
432    /// Panics if native backing is used and the clock is already mutably borrowed.
433    #[must_use]
434    pub fn timer_exists(&self, name: &str) -> bool {
435        match &self.backing {
436            ClockApiBacking::Native(clock) => {
437                clock_ref(clock, "timer_exists").timer_exists(&Ustr::from(name))
438            }
439            ClockApiBacking::Handlers(handlers) => (handlers.timer_exists)(name),
440        }
441    }
442
443    /// Returns the next trigger timestamp for the active timer named `name`.
444    ///
445    /// Returns `None` if no active timer with that name exists.
446    ///
447    /// # Panics
448    ///
449    /// Panics if native backing is used and the clock is already mutably borrowed.
450    #[must_use]
451    pub fn next_time_ns(&self, name: &str) -> Option<UnixNanos> {
452        match &self.backing {
453            ClockApiBacking::Native(clock) => clock_ref(clock, "next_time_ns").next_time_ns(name),
454            ClockApiBacking::Handlers(handlers) => (handlers.next_time_ns)(name),
455        }
456    }
457
458    /// Cancels the timer named `name`, if it exists.
459    ///
460    /// # Panics
461    ///
462    /// Panics if native backing is used and the clock is already borrowed.
463    pub fn cancel_timer(&self, name: &str) {
464        match &self.backing {
465            ClockApiBacking::Native(clock) => clock_mut(clock, "cancel_timer")
466                .unwrap_or_else(|e| panic!("{e}"))
467                .cancel_timer(name),
468            ClockApiBacking::Handlers(handlers) => (handlers.cancel_timer)(name),
469        }
470    }
471
472    /// Cancels all timers.
473    ///
474    /// # Panics
475    ///
476    /// Panics if native backing is used and the clock is already borrowed.
477    pub fn cancel_timers(&self) {
478        match &self.backing {
479            ClockApiBacking::Native(clock) => clock_mut(clock, "cancel_timers")
480                .unwrap_or_else(|e| panic!("{e}"))
481                .cancel_timers(),
482            ClockApiBacking::Handlers(handlers) => (handlers.cancel_timers)(),
483        }
484    }
485}
486
487enum ClockApiBacking<'a> {
488    Native(&'a RefCell<dyn Clock>),
489    Handlers(ClockApiHandlers<'a>),
490}
491
492struct ClockApiHandlers<'a> {
493    timestamp_ns: Box<dyn Fn() -> UnixNanos + 'a>,
494    set_time_alert_ns: Box<SetTimeAlertNsHandler<'a>>,
495    set_timer_ns: Box<SetTimerNsHandler<'a>>,
496    timer_names: Box<dyn Fn() -> Vec<String> + 'a>,
497    timer_count: Box<dyn Fn() -> usize + 'a>,
498    timer_exists: Box<dyn Fn(&str) -> bool + 'a>,
499    next_time_ns: Box<NextTimeNsHandler<'a>>,
500    cancel_timer: Box<dyn Fn(&str) + 'a>,
501    cancel_timers: Box<dyn Fn() + 'a>,
502}
503
504impl Debug for ClockApiBacking<'_> {
505    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
506        match self {
507            Self::Native(_) => f.write_str("Native"),
508            Self::Handlers(_) => f.write_str("Handlers"),
509        }
510    }
511}
512
513type SetTimeAlertNsHandler<'a> =
514    dyn Fn(&str, UnixNanos, Option<TimeEventCallback>, Option<bool>) -> anyhow::Result<()> + 'a;
515type NextTimeNsHandler<'a> = dyn Fn(&str) -> Option<UnixNanos> + 'a;
516type SetTimerNsHandler<'a> = dyn Fn(
517        &str,
518        DurationNanos,
519        Option<UnixNanos>,
520        Option<UnixNanos>,
521        Option<TimeEventCallback>,
522        Option<bool>,
523        Option<bool>,
524    ) -> anyhow::Result<()>
525    + 'a;
526
527fn clock_ref<'a>(clock: &'a RefCell<dyn Clock>, operation: &'static str) -> Ref<'a, dyn Clock> {
528    clock.try_borrow().unwrap_or_else(|_| {
529        panic!(
530            "{}",
531            ComponentAccessError::ReadConflict {
532                resource: "clock",
533                operation,
534            }
535        )
536    })
537}
538
539fn clock_mut<'a>(
540    clock: &'a RefCell<dyn Clock>,
541    operation: &'static str,
542) -> Result<RefMut<'a, dyn Clock>, ComponentAccessError> {
543    clock
544        .try_borrow_mut()
545        .map_err(|_| ComponentAccessError::WriteConflict {
546            resource: "clock",
547            operation,
548        })
549}