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.config.simulation;
42 let tick_interval_ms = sim_config.tick_interval_ms;
43 let tick_duration = sim_config.tick_duration_secs;
44 self.ticker_task.get_or_init(|| {
45 tokio::spawn(async move {
46 let mut interval =
47 tokio::time::interval(tokio::time::Duration::from_millis(tick_interval_ms));
48 loop {
49 interval.tick().await;
50 elapsed_ms.fetch_add(tick_duration.as_millis() as u64, Ordering::Relaxed);
51 }
52 });
53 });
54 }
55
56 fn now(&self) -> DateTime<Utc> {
57 if self.config.realtime {
58 Utc::now()
59 } else {
60 let sim_config = &self.config.simulation;
61 let elapsed_ms = self.elapsed_ms.load(Ordering::Relaxed);
62
63 let simulated_time =
64 sim_config.start_at + chrono::Duration::milliseconds(elapsed_ms as i64);
65
66 if sim_config.transform_to_realtime && simulated_time >= Utc::now() {
67 Utc::now()
68 } else {
69 simulated_time
70 }
71 }
72 }
73
74 fn real_ms(&self, duration: Duration) -> Duration {
75 if self.config.realtime {
76 duration
77 } else {
78 let sim_config = &self.config.simulation;
79
80 let current_time = self.now();
81 let real_now = Utc::now();
82
83 if sim_config.transform_to_realtime && current_time >= real_now {
84 return duration;
85 }
86
87 let sim_ms_per_real_ms = sim_config.tick_duration_secs.as_millis() as f64
88 / sim_config.tick_interval_ms as f64;
89
90 Duration::from_millis((duration.as_millis() as f64 / sim_ms_per_real_ms).ceil() as u64)
91 }
92 }
93
94 fn sleep(&self, duration: Duration) -> tokio::time::Sleep {
95 tokio::time::sleep(self.real_ms(duration))
96 }
97
98 fn timeout<F>(&self, duration: Duration, future: F) -> tokio::time::Timeout<F::IntoFuture>
99 where
100 F: core::future::IntoFuture,
101 {
102 tokio::time::timeout(self.real_ms(duration), future)
103 }
104
105 pub async fn wait_until_realtime(&self) {
106 if self.config.realtime {
107 return;
108 }
109
110 let current = self.now();
111 let real_now = Utc::now();
112
113 if current >= real_now {
114 return;
115 }
116
117 let wait_duration =
118 std::time::Duration::from_millis((real_now - current).num_milliseconds() as u64);
119
120 self.sleep(wait_duration).await;
121 }
122}
123
124pub async fn wait_until_realtime() {
129 INSTANCE
130 .get_or_init(|| Time::new(TimeConfig::default()))
131 .wait_until_realtime()
132 .await
133}
134
135pub fn init(config: TimeConfig) {
138 INSTANCE.get_or_init(|| Time::new(config));
139}
140
141pub fn now() -> DateTime<Utc> {
143 INSTANCE
144 .get_or_init(|| Time::new(TimeConfig::default()))
145 .now()
146}
147
148pub fn sleep(duration: Duration) -> tokio::time::Sleep {
150 INSTANCE
151 .get_or_init(|| Time::new(TimeConfig::default()))
152 .sleep(duration)
153}
154
155pub fn timeout<F>(duration: Duration, future: F) -> tokio::time::Timeout<F::IntoFuture>
157where
158 F: core::future::IntoFuture,
159{
160 INSTANCE
161 .get_or_init(|| Time::new(TimeConfig::default()))
162 .timeout(duration, future)
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168 use chrono::Duration as ChronoDuration;
169 use std::time::Duration as StdDuration;
170
171 #[tokio::test]
172 async fn test_simulated_time() {
173 let config = TimeConfig {
175 realtime: false,
176 simulation: SimulationConfig {
177 start_at: Utc::now(),
178 tick_interval_ms: 10,
179 tick_duration_secs: StdDuration::from_secs(10 * 24 * 60 * 60), transform_to_realtime: false,
181 },
182 };
183
184 init(config);
185 let start = now();
186 tokio::time::sleep(tokio::time::Duration::from_millis(20)).await;
187 let end = now();
188 let elapsed = end - start;
189
190 assert!(
191 elapsed >= ChronoDuration::days(19) && elapsed <= ChronoDuration::days(21),
192 "Expected ~20 days to pass, but got {} days",
193 elapsed.num_days()
194 );
195 }
196
197 #[test]
198 fn test_default_realtime() {
199 let t1 = now();
200 let t2 = Utc::now();
201 assert!(t2 - t1 < ChronoDuration::seconds(1));
202 }
203}