1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
5pub struct ClockConfig {
6 pub base_millis: i64,
8 pub tick_millis: i64,
10 pub max_abs_drift_ppm: i32,
12 pub max_abs_skew_millis: i64,
14}
15
16impl Default for ClockConfig {
17 fn default() -> Self {
18 Self {
19 base_millis: 1_700_000_000_000,
20 tick_millis: 100,
21 max_abs_drift_ppm: 100,
22 max_abs_skew_millis: 25,
23 }
24 }
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
29pub struct ClockSpec {
30 pub base_millis: i64,
32 pub tick_millis: i64,
34 pub drift_ppm: i32,
36 pub skew_millis: i64,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub struct SimulatedClock {
43 spec: ClockSpec,
44 frozen_at: Option<i64>,
45}
46
47impl SimulatedClock {
48 #[must_use]
50 pub const fn new(spec: ClockSpec) -> Self {
51 Self {
52 spec,
53 frozen_at: None,
54 }
55 }
56
57 #[must_use]
59 pub const fn spec(&self) -> ClockSpec {
60 self.spec
61 }
62
63 #[must_use]
65 pub fn now_millis(&self, round: u64) -> i64 {
66 if let Some(frozen) = self.frozen_at {
67 return frozen;
68 }
69
70 let round_i64 = i64::try_from(round).unwrap_or(i64::MAX);
71 let base_progress = self.spec.tick_millis.saturating_mul(round_i64);
72 let drift_adjust = base_progress
73 .saturating_mul(i64::from(self.spec.drift_ppm))
74 .saturating_div(1_000_000);
75
76 self.spec
77 .base_millis
78 .saturating_add(self.spec.skew_millis)
79 .saturating_add(base_progress)
80 .saturating_add(drift_adjust)
81 }
82
83 pub fn freeze(&mut self, round: u64) {
85 self.frozen_at = Some(self.now_millis(round));
86 }
87
88 pub const fn unfreeze(&mut self) {
90 self.frozen_at = None;
91 }
92
93 #[must_use]
95 pub const fn is_frozen(&self) -> bool {
96 self.frozen_at.is_some()
97 }
98}