agentmux_common/time.rs
1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Wall-clock "now" helpers.
5//!
6//! The three-line `SystemTime::now().duration_since(UNIX_EPOCH)` body was
7//! re-implemented in 26 files across four crates before this module
8//! existed, with the only variation being the return type and whether a
9//! pre-epoch clock maps to `0` or `unwrap_or_default()` — which are the
10//! same thing (`docs/reports/REPORT_DRY_AND_MODULARITY_AUDIT_2026_09_06.md`
11//! §2.2).
12//!
13//! Two integer widths are offered rather than one, because callers were
14//! split 16/8 between `i64` (SQLite column types, JSON timestamps) and
15//! `u64` (durations, comparisons against other `u64`s). Forcing one width
16//! would push a cast into every caller of the other; offering both keeps
17//! every existing call site's type unchanged.
18//!
19//! A clock set before the Unix epoch yields `0`, never a panic — these are
20//! used on hot paths (every persisted event, every stats sample) where a
21//! panic on a misconfigured clock would be far worse than a zero timestamp.
22
23use std::time::{Duration, SystemTime, UNIX_EPOCH};
24
25fn since_epoch() -> Duration {
26 SystemTime::now()
27 .duration_since(UNIX_EPOCH)
28 .unwrap_or_default()
29}
30
31/// Milliseconds since the Unix epoch, as `i64`.
32pub fn now_ms() -> i64 {
33 since_epoch().as_millis() as i64
34}
35
36/// Milliseconds since the Unix epoch, as `u64`.
37pub fn now_ms_u64() -> u64 {
38 since_epoch().as_millis() as u64
39}
40
41/// Seconds since the Unix epoch, as `i64`.
42pub fn now_secs() -> i64 {
43 since_epoch().as_secs() as i64
44}
45
46/// Seconds since the Unix epoch, as `u64`.
47pub fn now_secs_u64() -> u64 {
48 since_epoch().as_secs()
49}
50
51#[cfg(test)]
52mod tests {
53 use super::*;
54
55 /// Sanity bounds rather than an exact value: after 2020-01-01 (so the
56 /// epoch math isn't off by a factor of 1000) and the two widths agree.
57 #[test]
58 fn now_is_after_2020_and_widths_agree() {
59 const JAN_1_2020_SECS: i64 = 1_577_836_800;
60 let s = now_secs();
61 let ms = now_ms();
62 assert!(s > JAN_1_2020_SECS, "now_secs() = {s}");
63 assert!(ms > JAN_1_2020_SECS * 1000, "now_ms() = {ms}");
64 // ms and secs sampled back-to-back must agree to within a second.
65 assert!((ms / 1000 - s).abs() <= 1);
66 assert!(now_ms_u64() as i64 - now_ms() <= 1000);
67 assert_eq!(now_secs_u64() as i64 - now_secs() <= 1, true);
68 }
69}