Skip to main content

cachekit/
session.rs

1use std::sync::OnceLock;
2
3struct SessionInfo {
4    id: String,
5    start_str: String,
6}
7
8static SESSION: OnceLock<SessionInfo> = OnceLock::new();
9
10/// Current Unix time in milliseconds, via the JavaScript clock.
11///
12/// `std::time::SystemTime::now()` panics on `wasm32-unknown-unknown` ("time
13/// not implemented on this platform"), which trapped every Workers request
14/// (LAB-1079) — so wasm32 builds must read `js_sys::Date::now()` instead.
15#[cfg(target_arch = "wasm32")]
16fn now_epoch_millis() -> u64 {
17    // Saturating float→int cast: NaN → 0, negative → 0, overflow → u64::MAX.
18    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
19    {
20        js_sys::Date::now() as u64
21    }
22}
23
24/// Current Unix time in milliseconds, via the system clock (native targets).
25#[cfg(not(target_arch = "wasm32"))]
26fn now_epoch_millis() -> u64 {
27    use std::time::{SystemTime, UNIX_EPOCH};
28    let millis = SystemTime::now()
29        .duration_since(UNIX_EPOCH)
30        .unwrap_or_default()
31        .as_millis();
32    u64::try_from(millis).unwrap_or(u64::MAX)
33}
34
35fn get_or_create() -> &'static SessionInfo {
36    SESSION.get_or_init(|| SessionInfo {
37        id: uuid::Uuid::new_v4().to_string(),
38        start_str: now_epoch_millis().to_string(),
39    })
40}
41
42/// Return session identification headers. Values are static — zero allocations per call.
43pub fn session_headers() -> [(&'static str, &'static str); 2] {
44    let s = get_or_create();
45    [
46        ("X-CacheKit-Session-ID", s.id.as_str()),
47        ("X-CacheKit-Session-Start", s.start_str.as_str()),
48    ]
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54
55    #[test]
56    fn session_id_is_uuid_v4_format() {
57        let headers = session_headers();
58        let id = headers[0].1;
59        assert!(
60            uuid::Uuid::parse_str(id).is_ok(),
61            "Session ID should be valid UUID"
62        );
63        let parsed = uuid::Uuid::parse_str(id).unwrap();
64        assert_eq!(parsed.get_version_num(), 4, "Should be UUID v4");
65    }
66
67    #[test]
68    #[allow(clippy::expect_used)]
69    fn session_start_is_reasonable_epoch_millis() {
70        let headers = session_headers();
71        let start_ms: u64 = headers[1].1.parse().expect("Should be numeric");
72        // Plausibility window: after 2024-01-01, before 2100-01-01. Wide on
73        // purpose — it exists to catch unit confusion (epoch seconds trip the
74        // lower bound, micros the upper), not to expire on a schedule. Bounds
75        // mirror tests/wasm_session_tests.rs — keep them in lockstep.
76        assert!(start_ms > 1_704_067_200_000, "Should be after 2024");
77        assert!(start_ms < 4_102_444_800_000, "Should be before 2100");
78    }
79
80    #[test]
81    fn session_is_stable_across_calls() {
82        let h1 = session_headers();
83        let h2 = session_headers();
84        assert_eq!(h1[0].1, h2[0].1, "Session ID should be stable");
85        assert_eq!(h1[1].1, h2[1].1, "Session start should be stable");
86    }
87}