1use crate::{if_native, if_wasm, wasm_or_native};
4use std::fmt;
5
6if_native! {
7 pub use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
8}
9if_wasm! {
10 pub use web_time::{Duration, Instant, SystemTime, UNIX_EPOCH};
11}
12
13#[derive(Debug)]
14pub struct Expired;
15
16impl std::error::Error for Expired {
17 fn description(&self) -> &str {
18 "Timer duration expired"
19 }
20}
21
22if_native! {
23 impl From<tokio::time::error::Elapsed> for Expired {
24 fn from(_: tokio::time::error::Elapsed) -> Expired {
25 Expired
26 }
27 }
28}
29
30impl fmt::Display for Expired {
31 fn fmt(&self, f: &mut fmt::Formatter) -> std::fmt::Result {
32 write!(f, "timer duration expired")
33 }
34}
35
36impl crate::ErrorCode for Expired {
37 fn error_code(&self) -> &'static str {
38 "Expired"
39 }
40}
41
42fn duration_since_epoch() -> Duration {
43 SystemTime::now()
44 .duration_since(UNIX_EPOCH)
45 .expect("Time went backwards")
46}
47
48pub fn now_ns() -> i64 {
49 duration_since_epoch().as_nanos() as i64
50}
51
52pub fn now_ms() -> u64 {
53 duration_since_epoch().as_millis() as u64
54}
55pub fn now_secs() -> i64 {
56 duration_since_epoch().as_secs() as i64
57}
58
59pub async fn timeout<F>(duration: Duration, future: F) -> Result<F::Output, Expired>
60where
61 F: std::future::IntoFuture,
62{
63 wasm_or_native! {
64 wasm => {
65 use futures::future::Either::*;
66 let millis = duration.as_millis().min(u32::MAX as u128) as u32;
67 let timeout = gloo_timers::future::TimeoutFuture::new(millis);
68 let future = future.into_future();
69 futures::pin_mut!(future);
70 match futures::future::select(timeout, future).await {
71 Left(_) => Err(Expired),
72 Right((value, _)) => Ok(value),
73 }
74 },
75 native => {
76 tokio::time::timeout(duration, future).await.map_err(Into::into)
77 }
78 }
79}
80
81#[doc(hidden)]
82pub async fn sleep(duration: Duration) {
83 wasm_or_native! {
84 native => {tokio::time::sleep(duration).await},
85 wasm => {gloo_timers::future::sleep(duration).await},
86 }
87}
88
89pub fn interval_stream(
90 period: crate::time::Duration,
91) -> impl futures::Stream<Item = crate::time::Instant> {
92 use futures::StreamExt;
93 wasm_or_native! {
94 wasm => {gloo_timers::future::IntervalStream::new(period.as_millis().min(u32::MAX as u128) as u32).map(|_| crate::time::Instant::now())},
95 native => {tokio_stream::wrappers::IntervalStream::new(tokio::time::interval(period)).map(|t| t.into_std())},
96 }
97}
98
99pub fn rand_offset(jitter: crate::time::Duration) -> crate::time::Duration {
105 use rand::RngExt;
106 if jitter.is_zero() {
107 return crate::time::Duration::ZERO;
108 }
109 let distr = rand::distr::Uniform::new_inclusive(crate::time::Duration::ZERO, jitter).unwrap();
110 rand::rng().sample(distr)
111}
112
113pub fn jittered_interval_stream(
123 base: crate::time::Duration,
124 jitter: crate::time::Duration,
125) -> impl futures::Stream<Item = crate::time::Instant> + Unpin {
126 use futures::StreamExt;
127
128 let jittered = move || {
134 let startup_offset = rand_offset(jitter);
135 futures::stream::once(async move {
138 sleep(startup_offset).await;
139 })
140 .flat_map(move |_| {
141 interval_stream(base).then(move |instant| async move {
142 sleep(rand_offset(jitter)).await;
143 instant
144 })
145 })
146 };
147
148 wasm_or_native! {
149 wasm => {
150 if jitter.is_zero() {
151 interval_stream(base).boxed_local()
152 } else {
153 jittered().boxed_local()
154 }
155 },
156 native => {
157 if jitter.is_zero() {
158 interval_stream(base).boxed()
159 } else {
160 jittered().boxed()
161 }
162 },
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169 use crate::ErrorCode;
170
171 #[test]
172 fn test_expired_error_code() {
173 let expired = Expired;
174 assert_eq!(expired.error_code(), "Expired");
175 }
176
177 #[test]
178 fn test_expired_display() {
179 let expired = Expired;
180 assert_eq!(format!("{}", expired), "timer duration expired");
181 }
182
183 #[test]
184 fn test_expired_description() {
185 use std::error::Error;
186 #[allow(deprecated)]
187 let desc = Expired.description();
188 assert_eq!(desc, "Timer duration expired");
189 }
190
191 #[cfg(not(target_arch = "wasm32"))]
193 mod jitter {
194 use super::super::jittered_interval_stream;
195 use crate::time::Duration;
196 use futures::StreamExt;
197
198 #[tokio::test(start_paused = true)]
205 async fn zero_jitter_uses_base_period() {
206 let base = Duration::from_secs(10);
207 let start = tokio::time::Instant::now();
208 let mut s = Box::pin(jittered_interval_stream(base, Duration::ZERO));
209 s.next().await.unwrap();
211 assert_eq!(start.elapsed(), Duration::ZERO, "first tick is immediate");
212 s.next().await.unwrap();
214 assert_eq!(start.elapsed(), base, "second tick after exactly base");
215 }
216
217 #[tokio::test(start_paused = true)]
218 async fn jitter_keeps_ticks_within_bounds() {
219 let base = Duration::from_secs(10);
220 let jitter = Duration::from_secs(5);
221 let start = tokio::time::Instant::now();
222 let mut s = Box::pin(jittered_interval_stream(base, jitter));
223 s.next().await.unwrap();
226 let first = start.elapsed();
227 assert!(
228 first <= jitter * 2,
229 "first tick within 2*jitter, got {first:?}"
230 );
231 s.next().await.unwrap();
237 let gap = start.elapsed() - first;
238 assert!(
239 gap >= base.saturating_sub(jitter),
240 "gap >= base - jitter, got {gap:?}"
241 );
242 assert!(gap <= base + jitter, "gap <= base + jitter, got {gap:?}");
243 }
244 }
245}