sim_time/
lib.rs

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