es_entity/clock/global.rs
1use chrono::{DateTime, Utc};
2
3use std::sync::OnceLock;
4use std::time::Duration;
5
6use super::{ArtificialClockConfig, ClockController, ClockHandle, ClockSleep, ClockTimeout};
7
8struct GlobalState {
9 handle: ClockHandle,
10 controller: Option<ClockController>,
11}
12
13static GLOBAL: OnceLock<GlobalState> = OnceLock::new();
14
15/// Global clock access - like `Utc::now()` but testable.
16pub struct Clock;
17
18impl Clock {
19 /// Get current time from the global clock.
20 ///
21 /// Lazily initializes to realtime if not already set.
22 pub fn now() -> DateTime<Utc> {
23 Self::handle().now()
24 }
25
26 /// Get the current date (without time component).
27 ///
28 /// Lazily initializes to realtime if not already set.
29 pub fn today() -> chrono::NaiveDate {
30 Self::handle().today()
31 }
32
33 /// Sleep using the global clock.
34 pub fn sleep(duration: Duration) -> ClockSleep {
35 Self::handle().sleep(duration)
36 }
37
38 /// Timeout using the global clock.
39 pub fn timeout<F: std::future::Future>(duration: Duration, future: F) -> ClockTimeout<F> {
40 Self::handle().timeout(duration, future)
41 }
42
43 /// Get a reference to the global clock handle.
44 pub fn handle() -> &'static ClockHandle {
45 &GLOBAL
46 .get_or_init(|| GlobalState {
47 handle: ClockHandle::realtime(),
48 controller: None,
49 })
50 .handle
51 }
52
53 /// Install an artificial clock globally.
54 ///
55 /// - If not initialized: installs artificial clock, returns controller
56 /// - If already artificial: returns existing controller (idempotent)
57 /// - If already realtime: panics
58 ///
59 /// Must be called before any `Clock::now()` calls if you want artificial time.
60 pub fn install_artificial(config: ArtificialClockConfig) -> ClockController {
61 // Check if already initialized
62 if let Some(state) = GLOBAL.get() {
63 return state
64 .controller
65 .clone()
66 .expect("Cannot install artificial clock: realtime clock already initialized");
67 }
68
69 // Try to initialize
70 let (handle, ctrl) = ClockHandle::artificial(config);
71
72 match GLOBAL.set(GlobalState {
73 handle,
74 controller: Some(ctrl.clone()),
75 }) {
76 Ok(()) => ctrl,
77 Err(_) => {
78 // Race: someone else initialized between our check and set
79 GLOBAL
80 .get()
81 .unwrap()
82 .controller
83 .clone()
84 .expect("Cannot install artificial clock: realtime clock already initialized")
85 }
86 }
87 }
88
89 /// Check if an artificial clock is installed.
90 pub fn is_artificial() -> bool {
91 GLOBAL
92 .get()
93 .map(|s| s.controller.is_some())
94 .unwrap_or(false)
95 }
96
97 /// Get the current artificial time, if an artificial clock is installed
98 /// and hasn't transitioned to realtime.
99 ///
100 /// Returns:
101 /// - `None` if no clock is initialized (doesn't initialize one)
102 /// - `None` for realtime clocks
103 /// - `None` for artificial clocks that have transitioned to realtime
104 /// - `Some(time)` for artificial clocks that are still artificial
105 pub fn artificial_now() -> Option<DateTime<Utc>> {
106 GLOBAL.get().and_then(|s| s.handle.artificial_now())
107 }
108}