pubmed_client/
time.rs

1//! Cross-platform time module for rate limiting and retry backoff.
2//!
3//! - **Native**: delegates to `std::time::Instant` and `tokio::time::sleep`.
4//! - **WASM**: uses `js_sys::Date::now()` for time measurement and
5//!   `setTimeout` (via `js_sys::Promise`) for async sleep.
6
7#[cfg(not(target_arch = "wasm32"))]
8use std::time::{Duration as StdDuration, Instant as StdInstant};
9#[cfg(not(target_arch = "wasm32"))]
10use tokio::time as tokio_time;
11
12/// Simple duration representation for cross-platform compatibility
13///
14/// This struct provides basic duration functionality without relying on
15/// `std::time::Duration` which is not available in WASM environments.
16#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
17pub struct Duration {
18    millis: u64,
19}
20
21impl Duration {
22    /// Create a new Duration from seconds
23    ///
24    /// # Arguments
25    ///
26    /// * `secs` - Number of seconds
27    ///
28    /// # Example
29    ///
30    /// ```
31    /// use pubmed_client::time::Duration;
32    ///
33    /// let duration = Duration::from_secs(30);
34    /// assert_eq!(duration.as_secs(), 30);
35    /// ```
36    pub fn from_secs(secs: u64) -> Self {
37        Self {
38            millis: secs * 1000,
39        }
40    }
41
42    /// Create a new Duration from milliseconds
43    ///
44    /// # Arguments
45    ///
46    /// * `millis` - Number of milliseconds
47    ///
48    /// # Example
49    ///
50    /// ```
51    /// use pubmed_client::time::Duration;
52    ///
53    /// let duration = Duration::from_millis(1500);
54    /// assert_eq!(duration.as_secs(), 1);
55    /// assert_eq!(duration.as_millis(), 1500);
56    /// ```
57    pub fn from_millis(millis: u64) -> Self {
58        Self { millis }
59    }
60
61    /// Get duration as seconds
62    pub fn as_secs(&self) -> u64 {
63        self.millis / 1000
64    }
65
66    /// Get duration as milliseconds
67    pub fn as_millis(&self) -> u64 {
68        self.millis
69    }
70
71    /// Get duration as seconds f64 (useful for rate calculations)
72    pub fn as_secs_f64(&self) -> f64 {
73        self.millis as f64 / 1000.0
74    }
75
76    /// Check if duration is zero
77    pub fn is_zero(&self) -> bool {
78        self.millis == 0
79    }
80}
81
82impl Default for Duration {
83    fn default() -> Self {
84        Self::from_secs(0)
85    }
86}
87
88impl From<u64> for Duration {
89    fn from(secs: u64) -> Self {
90        Self::from_secs(secs)
91    }
92}
93
94// ---------------------------------------------------------------------------
95// sleep
96// ---------------------------------------------------------------------------
97
98#[cfg(not(target_arch = "wasm32"))]
99pub async fn sleep(duration: Duration) {
100    if duration.is_zero() {
101        return;
102    }
103    tokio_time::sleep(StdDuration::from_millis(duration.as_millis())).await;
104}
105
106#[cfg(target_arch = "wasm32")]
107pub async fn sleep(duration: Duration) {
108    use wasm_bindgen::prelude::*;
109
110    if duration.is_zero() {
111        return;
112    }
113    let ms = duration.as_millis() as f64;
114    let promise = js_sys::Promise::new(&mut |resolve, _| {
115        let global = js_sys::global();
116        if let Ok(set_timeout) = js_sys::Reflect::get(&global, &JsValue::from_str("setTimeout")) {
117            let set_timeout_fn: js_sys::Function = set_timeout.unchecked_into();
118            let _ = set_timeout_fn.call2(&JsValue::undefined(), &resolve, &JsValue::from_f64(ms));
119        }
120    });
121    let _ = wasm_bindgen_futures::JsFuture::from(promise).await;
122}
123
124// ---------------------------------------------------------------------------
125// Instant
126// ---------------------------------------------------------------------------
127
128#[cfg(not(target_arch = "wasm32"))]
129#[derive(Clone, Copy, Debug)]
130pub struct Instant {
131    inner: StdInstant,
132}
133
134#[cfg(not(target_arch = "wasm32"))]
135impl Instant {
136    pub fn now() -> Self {
137        Self {
138            inner: StdInstant::now(),
139        }
140    }
141
142    pub fn duration_since(&self, earlier: Instant) -> Duration {
143        let std_dur = self.inner.duration_since(earlier.inner);
144        Duration::from_millis(std_dur.as_millis() as u64)
145    }
146
147    pub fn elapsed(&self) -> Duration {
148        let std_dur = self.inner.elapsed();
149        Duration::from_millis(std_dur.as_millis() as u64)
150    }
151}
152
153#[cfg(target_arch = "wasm32")]
154#[derive(Clone, Copy, Debug)]
155pub struct Instant {
156    epoch_millis: f64,
157}
158
159#[cfg(target_arch = "wasm32")]
160impl Instant {
161    pub fn now() -> Self {
162        Self {
163            epoch_millis: js_sys::Date::now(),
164        }
165    }
166
167    pub fn duration_since(&self, earlier: Instant) -> Duration {
168        let diff = (self.epoch_millis - earlier.epoch_millis).max(0.0);
169        Duration::from_millis(diff as u64)
170    }
171
172    pub fn elapsed(&self) -> Duration {
173        let now = js_sys::Date::now();
174        let diff = (now - self.epoch_millis).max(0.0);
175        Duration::from_millis(diff as u64)
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn test_duration_creation() {
185        let duration = Duration::from_secs(30);
186        assert_eq!(duration.as_secs(), 30);
187        assert_eq!(duration.as_millis(), 30000);
188        assert_eq!(duration.as_secs_f64(), 30.0);
189    }
190
191    #[test]
192    fn test_duration_from_millis() {
193        let duration = Duration::from_millis(1500);
194        assert_eq!(duration.as_secs(), 1);
195        assert_eq!(duration.as_millis(), 1500);
196    }
197
198    #[test]
199    fn test_duration_zero() {
200        let duration = Duration::default();
201        assert!(duration.is_zero());
202
203        let non_zero = Duration::from_secs(1);
204        assert!(!non_zero.is_zero());
205    }
206
207    #[test]
208    fn test_duration_ordering() {
209        let dur1 = Duration::from_secs(10);
210        let dur2 = Duration::from_secs(20);
211
212        assert!(dur1 < dur2);
213        assert!(dur2 > dur1);
214        assert_eq!(dur1, dur1);
215    }
216
217    #[test]
218    fn test_duration_from_u64() {
219        let duration: Duration = 42u64.into();
220        assert_eq!(duration.as_secs(), 42);
221    }
222
223    #[test]
224    fn test_instant_elapsed_is_non_negative() {
225        let instant = Instant::now();
226        let elapsed = instant.elapsed();
227        assert!(elapsed.as_millis() < 1000);
228    }
229
230    #[tokio::test]
231    async fn test_instant_duration_since() {
232        let earlier = Instant::now();
233        sleep(Duration::from_millis(50)).await;
234        let later = Instant::now();
235        let diff = later.duration_since(earlier);
236        assert!(diff.as_millis() >= 30);
237    }
238
239    #[tokio::test]
240    async fn test_sleep_functionality() {
241        let duration = Duration::from_secs(0);
242        sleep(duration).await;
243    }
244
245    #[tokio::test]
246    async fn test_sleep_actually_waits() {
247        let before = Instant::now();
248        sleep(Duration::from_millis(100)).await;
249        let elapsed = before.elapsed();
250        assert!(elapsed.as_millis() >= 50);
251    }
252}