1use alloc::collections::BTreeMap;
7use alloc::string::String;
8use alloc::vec::Vec;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
17pub struct Now {
18 pub monotonic_ms: u64,
20 pub unix_ms: u64,
22}
23
24pub(crate) const PING_KEEPALIVE_MS: u64 = 30_000;
26
27pub(crate) const DEAD_LINK_MS: u64 = 10_000;
29
30pub(crate) const TYPING_EXPIRY_MS: u64 = 6_000;
35
36#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
38#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
39pub(crate) enum Deadline {
40 PingKeepalive,
42 DeadLink,
44 Reconnect,
46 Typing(String, String),
53}
54
55#[derive(Debug, Clone, Default)]
60pub(crate) struct Timers {
61 deadlines: BTreeMap<Deadline, u64>,
62}
63
64impl Timers {
65 pub(crate) fn new() -> Self {
67 Self::default()
68 }
69
70 pub(crate) fn set(&mut self, deadline: Deadline, at_monotonic_ms: u64) {
72 self.deadlines.insert(deadline, at_monotonic_ms);
73 }
74
75 pub(crate) fn clear(&mut self, deadline: &Deadline) {
77 self.deadlines.remove(deadline);
78 }
79
80 pub(crate) fn expire(&mut self, now: Now) -> Vec<Deadline> {
83 drain_due(&mut self.deadlines, now.monotonic_ms, |at| *at)
84 .into_iter()
85 .map(|(deadline, _)| deadline)
86 .collect()
87 }
88
89 pub(crate) fn next(&self) -> Option<u64> {
91 self.deadlines.values().min().copied()
92 }
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub(crate) struct ReconnectBackoff {
104 base_ms: u64,
105 cap_ms: u64,
106 max_attempts: u32,
107 attempts: u32,
108}
109
110impl ReconnectBackoff {
111 pub(crate) const DEFAULT_BASE_MS: u64 = 2_000;
113 pub(crate) const DEFAULT_CAP_MS: u64 = 300_000;
115 pub(crate) const DEFAULT_MAX_ATTEMPTS: u32 = 100;
117
118 pub(crate) fn new(base_ms: u64, cap_ms: u64, max_attempts: u32) -> Self {
120 Self {
121 base_ms,
122 cap_ms,
123 max_attempts,
124 attempts: 0,
125 }
126 }
127
128 pub(crate) fn next_delay_ms(&mut self) -> Option<u64> {
133 if self.attempts >= self.max_attempts {
134 return None;
135 }
136 let factor = 1u64.checked_shl(self.attempts).unwrap_or(u64::MAX);
139 let delay = self.base_ms.saturating_mul(factor).min(self.cap_ms);
140 self.attempts += 1;
141 Some(delay)
142 }
143
144 pub(crate) fn reset(&mut self) {
146 self.attempts = 0;
147 }
148}
149
150impl Default for ReconnectBackoff {
151 fn default() -> Self {
154 Self::new(
155 Self::DEFAULT_BASE_MS,
156 Self::DEFAULT_CAP_MS,
157 Self::DEFAULT_MAX_ATTEMPTS,
158 )
159 }
160}
161
162pub(crate) fn drain_due<K, V>(
168 map: &mut alloc::collections::BTreeMap<K, V>,
169 now_ms: u64,
170 deadline_of: impl Fn(&V) -> u64,
171) -> Vec<(K, V)>
172where
173 K: Ord + Clone,
174{
175 let mut due: Vec<(u64, K)> = map
176 .iter()
177 .filter(|(_, value)| deadline_of(value) <= now_ms)
178 .map(|(key, value)| (deadline_of(value), key.clone()))
179 .collect();
180 due.sort_by_key(|(at, _)| *at);
181 due.into_iter()
182 .filter_map(|(_, key)| map.remove(&key).map(|value| (key, value)))
183 .collect()
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189 use alloc::string::ToString;
190 use alloc::vec;
191
192 fn now(monotonic_ms: u64) -> Now {
193 Now {
194 monotonic_ms,
195 unix_ms: 0,
196 }
197 }
198
199 #[test]
200 fn a_deadline_fires_exactly_at_its_instant_and_not_before() {
201 let mut timers = Timers::new();
202 timers.set(Deadline::PingKeepalive, 1_000);
203 assert!(timers.expire(now(999)).is_empty());
204 assert_eq!(timers.expire(now(1_000)), vec![Deadline::PingKeepalive]);
205 }
206
207 #[test]
208 fn several_deadlines_expire_in_one_call_in_order() {
209 let mut timers = Timers::new();
210 timers.set(Deadline::DeadLink, 300);
211 timers.set(Deadline::PingKeepalive, 100);
212 timers.set(Deadline::Reconnect, 200);
213 let due = timers.expire(now(1_000));
214 assert_eq!(
215 due,
216 vec![
217 Deadline::PingKeepalive,
218 Deadline::Reconnect,
219 Deadline::DeadLink
220 ]
221 );
222 }
223
224 #[test]
225 fn expiring_drains_a_deadline_so_it_does_not_fire_twice() {
226 let mut timers = Timers::new();
227 timers.set(Deadline::DeadLink, 100);
228 assert_eq!(timers.expire(now(200)), vec![Deadline::DeadLink]);
229 assert!(timers.expire(now(200)).is_empty());
230 }
231
232 #[test]
233 fn next_returns_the_earliest_pending_deadline() {
234 let mut timers = Timers::new();
235 assert_eq!(timers.next(), None);
236 timers.set(Deadline::DeadLink, 500);
237 timers.set(Deadline::PingKeepalive, 200);
238 assert_eq!(timers.next(), Some(200));
239 }
240
241 #[test]
242 fn clearing_a_deadline_disarms_it() {
243 let mut timers = Timers::new();
244 timers.set(Deadline::PingKeepalive, 100);
245 timers.clear(&Deadline::PingKeepalive);
246 assert!(timers.expire(now(1_000)).is_empty());
247 assert_eq!(timers.next(), None);
248 }
249
250 #[test]
251 fn per_key_typing_deadlines_are_independent() {
252 let mut timers = Timers::new();
253 timers.set(Deadline::Typing("#a".to_string(), "alice".to_string()), 100);
254 timers.set(Deadline::Typing("#a".to_string(), "bob".to_string()), 200);
255 assert_eq!(
256 timers.expire(now(100)),
257 vec![Deadline::Typing("#a".to_string(), "alice".to_string())]
258 );
259 assert_eq!(timers.next(), Some(200));
260 }
261
262 #[test]
263 fn reconnect_backoff_doubles_then_caps_then_gives_up() {
264 let mut backoff = ReconnectBackoff::new(1_000, 5_000, 4);
265 assert_eq!(backoff.next_delay_ms(), Some(1_000));
266 assert_eq!(backoff.next_delay_ms(), Some(2_000));
267 assert_eq!(backoff.next_delay_ms(), Some(4_000));
268 assert_eq!(
269 backoff.next_delay_ms(),
270 Some(5_000),
271 "8000 would exceed the 5000 cap"
272 );
273 assert_eq!(
274 backoff.next_delay_ms(),
275 None,
276 "the fifth attempt exceeds max_attempts"
277 );
278 }
279
280 #[test]
281 fn reconnect_backoff_resets_back_to_the_base_delay() {
282 let mut backoff = ReconnectBackoff::new(1_000, 5_000, 4);
283 assert_eq!(backoff.next_delay_ms(), Some(1_000));
284 assert_eq!(backoff.next_delay_ms(), Some(2_000));
285 backoff.reset();
286 assert_eq!(
287 backoff.next_delay_ms(),
288 Some(1_000),
289 "a connection that succeeded means the next failure starts over"
290 );
291 }
292
293 #[test]
294 fn default_reconnect_backoff_matches_the_reference_client() {
295 let mut backoff = ReconnectBackoff::default();
296 assert_eq!(backoff.next_delay_ms(), Some(2_000));
297 assert_eq!(backoff.next_delay_ms(), Some(4_000));
298 }
299}