1use std::sync::OnceLock;
2
3struct SessionInfo {
4 id: String,
5 start_str: String,
6}
7
8static SESSION: OnceLock<SessionInfo> = OnceLock::new();
9
10#[cfg(target_arch = "wasm32")]
16fn now_epoch_millis() -> u64 {
17 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
19 {
20 js_sys::Date::now() as u64
21 }
22}
23
24#[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
42pub 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 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}