use std::sync::atomic::{AtomicBool, AtomicI64, Ordering};
static SKEW_SECS: AtomicI64 = AtomicI64::new(0);
static OBSERVED: AtomicBool = AtomicBool::new(false);
const JUMP_WARN_SECS: i64 = 60;
fn local_now_secs_f64() -> f64 {
#[cfg(not(target_arch = "wasm32"))]
{
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs_f64())
.unwrap_or(0.0)
}
#[cfg(target_arch = "wasm32")]
{
js_sys::Date::now() / 1000.0
}
}
pub fn current_skew_secs() -> i64 {
SKEW_SECS.load(Ordering::Relaxed)
}
pub fn has_observed() -> bool {
OBSERVED.load(Ordering::Relaxed)
}
pub fn server_now_secs_f64() -> f64 {
local_now_secs_f64() + (current_skew_secs() as f64)
}
pub fn observe(server_unix_secs: f64) -> (i64, i64) {
let raw_skew = (server_unix_secs - local_now_secs_f64()).round() as i64;
let prev = SKEW_SECS.load(Ordering::Relaxed);
let new = if !OBSERVED.swap(true, Ordering::Relaxed) {
raw_skew
} else {
((prev * 3 + raw_skew) as f64 / 4.0).round() as i64
};
SKEW_SECS.store(new, Ordering::Relaxed);
(prev, new)
}
pub fn observe_and_warn(server_unix_secs: f64) -> i64 {
let (prev, new) = observe(server_unix_secs);
let jumped = (new - prev).abs() >= JUMP_WARN_SECS && OBSERVED.load(Ordering::Relaxed);
let first_observation = prev == 0 && new.abs() >= JUMP_WARN_SECS;
if jumped && !first_observation {
eprintln!(
"[clock] system clock jumped ({} -> {}) — skew is now {}s vs server",
format_skew(prev),
format_skew(new),
new
);
}
new
}
pub fn format_skew(skew_secs: i64) -> String {
let sign = if skew_secs < 0 { '-' } else { '+' };
let abs = skew_secs.unsigned_abs();
let h = abs / 3600;
let m = (abs % 3600) / 60;
let s = abs % 60;
if h > 0 {
format!("{sign}{h}h{m:02}m")
} else if m > 0 {
format!("{sign}{m}m{s:02}s")
} else {
format!("{sign}{s}s")
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
static LOCK: Mutex<()> = Mutex::new(());
fn reset() {
SKEW_SECS.store(0, Ordering::Relaxed);
OBSERVED.store(false, Ordering::Relaxed);
}
#[test]
fn first_observe_is_taken_as_truth() {
let _g = LOCK.lock().unwrap();
reset();
let local = local_now_secs_f64();
let (_prev, new) = observe(local + 7200.0); assert!((new - 7200).abs() <= 1, "expected ~7200, got {new}");
assert!(has_observed());
}
#[test]
fn ewma_smooths_subsequent_samples() {
let _g = LOCK.lock().unwrap();
reset();
let local = local_now_secs_f64();
observe(local + 100.0);
let new = observe(local + 200.0).1;
assert!((new - 125).abs() <= 1, "expected ~125, got {new}");
}
#[test]
fn server_now_lifts_local_into_window() {
let _g = LOCK.lock().unwrap();
reset();
let local = local_now_secs_f64();
observe(local + 10_000.0);
let server_now = server_now_secs_f64();
assert!(
(server_now - (local + 10_000.0)).abs() <= 2.0,
"expected anchored time within 2s of observed"
);
}
#[test]
fn format_skew_renders() {
assert_eq!(format_skew(0), "+0s");
assert_eq!(format_skew(45), "+45s");
assert_eq!(format_skew(-45), "-45s");
assert_eq!(format_skew(125), "+2m05s");
assert_eq!(format_skew(7200), "+2h00m");
assert_eq!(format_skew(-(3 * 3600 + 12 * 60)), "-3h12m");
}
}