1#![cfg_attr(feature = "fail-on-warnings", deny(warnings))]
2#![cfg_attr(feature = "fail-on-warnings", deny(clippy::all))]
3#![forbid(unsafe_code)]
4
5mod config;
6
7use chrono::{DateTime, Utc};
8pub use config::*;
9use std::{
10 sync::{
11 Arc, OnceLock,
12 atomic::{AtomicU64, Ordering},
13 },
14 time::Duration,
15};
16
17static INSTANCE: OnceLock<Time> = OnceLock::new();
18
19#[derive(Clone)]
20struct Time {
21 config: TimeConfig,
22 elapsed_ms: Arc<AtomicU64>,
23 ticker_task: Arc<OnceLock<()>>,
24}
25
26impl Time {
27 fn new(config: TimeConfig) -> Self {
28 let time = Self {
29 config,
30 elapsed_ms: Arc::new(AtomicU64::new(0)),
31 ticker_task: Arc::new(OnceLock::new()),
32 };
33 if !time.config.realtime {
34 time.spawn_ticker();
35 }
36 time
37 }
38
39 fn spawn_ticker(&self) {
40 let elapsed_ms = self.elapsed_ms.clone();
41 let sim_config = self
42 .config
43 .simulation
44 .as_ref()
45 .expect("sim_time required when realtime is false");
46 let tick_interval_ms = sim_config.tick_interval_ms;
47 let tick_duration = sim_config.tick_duration_secs;
48 self.ticker_task.get_or_init(|| {
49 tokio::spawn(async move {
50 let mut interval =
51 tokio::time::interval(tokio::time::Duration::from_millis(tick_interval_ms));
52 loop {
53 interval.tick().await;
54 elapsed_ms.fetch_add(tick_duration.as_millis() as u64, Ordering::Relaxed);
55 }
56 });
57 });
58 }
59
60 fn now(&self) -> DateTime<Utc> {
61 if self.config.realtime {
62 Utc::now()
63 } else {
64 let sim_config = self
65 .config
66 .simulation
67 .as_ref()
68 .expect("sim_time required when realtime is false");
69 let elapsed_ms = self.elapsed_ms.load(Ordering::Relaxed);
70
71 let simulated_time =
72 sim_config.start_at + chrono::Duration::milliseconds(elapsed_ms as i64);
73
74 if sim_config.transform_to_realtime && simulated_time >= Utc::now() {
75 Utc::now()
76 } else {
77 simulated_time
78 }
79 }
80 }
81
82 fn real_ms(&self, duration: Duration) -> Duration {
83 if self.config.realtime {
84 duration
85 } else {
86 let sim_config = self
87 .config
88 .simulation
89 .as_ref()
90 .expect("sim_time required when realtime is false");
91
92 let current_time = self.now();
93 let real_now = Utc::now();
94
95 if sim_config.transform_to_realtime && current_time >= real_now {
96 return duration;
97 }
98
99 let sim_ms_per_real_ms = sim_config.tick_duration_secs.as_millis() as f64
100 / sim_config.tick_interval_ms as f64;
101
102 Duration::from_millis((duration.as_millis() as f64 / sim_ms_per_real_ms).ceil() as u64)
103 }
104 }
105
106 fn sleep(&self, duration: Duration) -> tokio::time::Sleep {
107 tokio::time::sleep(self.real_ms(duration))
108 }
109
110 fn timeout<F>(&self, duration: Duration, future: F) -> tokio::time::Timeout<F::IntoFuture>
111 where
112 F: core::future::IntoFuture,
113 {
114 tokio::time::timeout(self.real_ms(duration), future)
115 }
116
117 pub async fn wait_until_realtime(&self) {
118 if self.config.realtime {
119 return;
120 }
121
122 let current = self.now();
123 let real_now = Utc::now();
124
125 if current >= real_now {
126 return;
127 }
128
129 let wait_duration =
130 std::time::Duration::from_millis((real_now - current).num_milliseconds() as u64);
131
132 self.sleep(wait_duration).await;
133 }
134}
135
136pub async fn wait_until_realtime() {
141 INSTANCE
142 .get_or_init(|| Time::new(TimeConfig::default()))
143 .wait_until_realtime()
144 .await
145}
146
147pub fn init(config: TimeConfig) {
150 INSTANCE.get_or_init(|| Time::new(config));
151}
152
153pub fn now() -> DateTime<Utc> {
155 INSTANCE
156 .get_or_init(|| Time::new(TimeConfig::default()))
157 .now()
158}
159
160pub fn sleep(duration: Duration) -> tokio::time::Sleep {
162 INSTANCE
163 .get_or_init(|| Time::new(TimeConfig::default()))
164 .sleep(duration)
165}
166
167pub fn timeout<F>(duration: Duration, future: F) -> tokio::time::Timeout<F::IntoFuture>
169where
170 F: core::future::IntoFuture,
171{
172 INSTANCE
173 .get_or_init(|| Time::new(TimeConfig::default()))
174 .timeout(duration, future)
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use chrono::Duration as ChronoDuration;
181 use std::time::Duration as StdDuration;
182
183 #[tokio::test]
184 async fn test_simulated_time() {
185 let config = TimeConfig {
187 realtime: false,
188 simulation: Some(SimulationConfig {
189 start_at: Utc::now(),
190 tick_interval_ms: 10,
191 tick_duration_secs: StdDuration::from_secs(10 * 24 * 60 * 60), transform_to_realtime: false,
193 }),
194 };
195
196 init(config);
197 let start = now();
198 tokio::time::sleep(tokio::time::Duration::from_millis(20)).await;
199 let end = now();
200 let elapsed = end - start;
201
202 assert!(
203 elapsed >= ChronoDuration::days(19) && elapsed <= ChronoDuration::days(21),
204 "Expected ~20 days to pass, but got {} days",
205 elapsed.num_days()
206 );
207 }
208
209 #[test]
210 fn test_default_realtime() {
211 let t1 = now();
212 let t2 = Utc::now();
213 assert!(t2 - t1 < ChronoDuration::seconds(1));
214 }
215}