Skip to main content

algocline_core/execution/
session_id.rs

1//! Session identifier type for the `ExecutionService` layer.
2
3use std::sync::atomic::{AtomicU64, Ordering};
4use std::time::{SystemTime, UNIX_EPOCH};
5
6use serde::{Deserialize, Serialize};
7
8/// Global counter to ensure uniqueness within a process even at sub-millisecond resolution.
9static SESSION_COUNTER: AtomicU64 = AtomicU64::new(0);
10
11/// Opaque handle that uniquely identifies an execution session across all verb calls.
12///
13/// `SessionId` is a newtype over [`String`] and is the sole cross-boundary handle
14/// needed to address a session.  Generation is the responsibility of the engine layer;
15/// the core value layer only provides construction and inspection primitives.
16///
17/// # Serde
18/// Serializes as a plain JSON string (transparent newtype) so that wire consumers do
19/// not need to unwrap a wrapper object.
20#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
21#[serde(transparent)]
22pub struct SessionId(String);
23
24impl SessionId {
25    /// Wraps an existing string as a `SessionId`.
26    pub fn new(s: String) -> Self {
27        Self(s)
28    }
29
30    /// Generate a new unique `SessionId`.
31    ///
32    /// Uses Unix milliseconds combined with a process-global counter to ensure
33    /// uniqueness within a process even at sub-millisecond resolution.
34    /// Format: `"ses-{ms}-{counter}"`.
35    pub fn generate() -> Self {
36        let ms = SystemTime::now()
37            .duration_since(UNIX_EPOCH)
38            .unwrap_or_default()
39            .as_millis() as u64;
40        let counter = SESSION_COUNTER.fetch_add(1, Ordering::Relaxed);
41        Self(format!("ses-{ms}-{counter}"))
42    }
43
44    /// Returns the inner string value.
45    pub fn as_str(&self) -> &str {
46        &self.0
47    }
48}
49
50impl std::fmt::Display for SessionId {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        f.write_str(&self.0)
53    }
54}
55
56impl From<String> for SessionId {
57    fn from(s: String) -> Self {
58        Self(s)
59    }
60}
61
62impl From<&str> for SessionId {
63    fn from(s: &str) -> Self {
64        Self(s.to_owned())
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn session_id_serde_string_form() {
74        // SessionId must serialize as a bare JSON string, not as {"0": "…"}.
75        let id = SessionId::new("01HX1234567890ABCDEFGHJKMP".to_owned());
76        let json = serde_json::to_string(&id).expect("serialize");
77        assert_eq!(json, r#""01HX1234567890ABCDEFGHJKMP""#);
78
79        let roundtripped: SessionId = serde_json::from_str(&json).expect("deserialize");
80        assert_eq!(roundtripped, id);
81    }
82
83    #[test]
84    fn session_id_display() {
85        let id = SessionId::new("abc123".to_owned());
86        assert_eq!(id.to_string(), "abc123");
87    }
88}