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 .sim_time
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 .sim_time
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 .sim_time
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() {
137 INSTANCE
138 .get_or_init(|| Time::new(TimeConfig::default()))
139 .wait_until_realtime()
140 .await
141}
142
143pub fn init(config: TimeConfig) {
144 INSTANCE.get_or_init(|| Time::new(config));
145}
146
147pub fn now() -> DateTime<Utc> {
148 INSTANCE
149 .get_or_init(|| Time::new(TimeConfig::default()))
150 .now()
151}
152
153pub fn sleep(duration: Duration) -> tokio::time::Sleep {
154 INSTANCE
155 .get_or_init(|| Time::new(TimeConfig::default()))
156 .sleep(duration)
157}
158
159pub fn timeout<F>(duration: Duration, future: F) -> tokio::time::Timeout<F::IntoFuture>
160where
161 F: core::future::IntoFuture,
162{
163 INSTANCE
164 .get_or_init(|| Time::new(TimeConfig::default()))
165 .timeout(duration, future)
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use chrono::Duration as ChronoDuration;
172 use std::time::Duration as StdDuration;
173
174 #[tokio::test]
175 async fn test_simulated_time() {
176 let config = TimeConfig {
178 realtime: false,
179 sim_time: Some(SimTimeConfig {
180 start_at: Utc::now(),
181 tick_interval_ms: 10,
182 tick_duration_secs: StdDuration::from_secs(10 * 24 * 60 * 60), transform_to_realtime: false,
184 }),
185 };
186
187 init(config);
188 let start = now();
189 tokio::time::sleep(tokio::time::Duration::from_millis(20)).await;
190 let end = now();
191 let elapsed = end - start;
192
193 assert!(
194 elapsed >= ChronoDuration::days(19) && elapsed <= ChronoDuration::days(21),
195 "Expected ~20 days to pass, but got {} days",
196 elapsed.num_days()
197 );
198 }
199
200 #[test]
201 fn test_default_realtime() {
202 let t1 = now();
203 let t2 = Utc::now();
204 assert!(t2 - t1 < ChronoDuration::seconds(1));
205 }
206}