Skip to main content

forge_core/realtime/
session.rs

1use std::str::FromStr;
2
3use chrono::{DateTime, Utc};
4use uuid::Uuid;
5
6use crate::cluster::NodeId;
7
8/// Unique session identifier.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub struct SessionId(pub Uuid);
11
12impl SessionId {
13    pub fn new() -> Self {
14        Self(Uuid::new_v4())
15    }
16
17    pub fn from_uuid(id: Uuid) -> Self {
18        Self(id)
19    }
20
21    pub fn as_uuid(&self) -> Uuid {
22        self.0
23    }
24}
25
26impl Default for SessionId {
27    fn default() -> Self {
28        Self::new()
29    }
30}
31
32impl std::fmt::Display for SessionId {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        write!(f, "{}", self.0)
35    }
36}
37
38/// Session status.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40#[non_exhaustive]
41pub enum SessionStatus {
42    Connecting,
43    Connected,
44    Reconnecting,
45    Disconnected,
46}
47
48impl SessionStatus {
49    pub fn as_str(&self) -> &'static str {
50        match self {
51            Self::Connecting => "connecting",
52            Self::Connected => "connected",
53            Self::Reconnecting => "reconnecting",
54            Self::Disconnected => "disconnected",
55        }
56    }
57}
58
59impl FromStr for SessionStatus {
60    type Err = std::convert::Infallible;
61
62    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
63        Ok(match s.to_lowercase().as_str() {
64            "connecting" => Self::Connecting,
65            "connected" => Self::Connected,
66            "reconnecting" => Self::Reconnecting,
67            "disconnected" => Self::Disconnected,
68            _ => Self::Disconnected,
69        })
70    }
71}
72
73/// Information about a WebSocket session.
74#[derive(Debug, Clone)]
75pub struct SessionInfo {
76    pub id: SessionId,
77    pub node_id: NodeId,
78    pub user_id: Option<String>,
79    pub status: SessionStatus,
80    pub subscription_count: u32,
81    pub created_at: DateTime<Utc>,
82    pub last_active_at: DateTime<Utc>,
83    pub client_ip: Option<String>,
84    pub user_agent: Option<String>,
85}
86
87impl SessionInfo {
88    pub fn new(node_id: NodeId) -> Self {
89        let now = Utc::now();
90        Self {
91            id: SessionId::new(),
92            node_id,
93            user_id: None,
94            status: SessionStatus::Connecting,
95            subscription_count: 0,
96            created_at: now,
97            last_active_at: now,
98            client_ip: None,
99            user_agent: None,
100        }
101    }
102
103    pub fn with_user_id(mut self, user_id: impl Into<String>) -> Self {
104        self.user_id = Some(user_id.into());
105        self
106    }
107
108    pub fn with_client_info(
109        mut self,
110        client_ip: Option<String>,
111        user_agent: Option<String>,
112    ) -> Self {
113        self.client_ip = client_ip;
114        self.user_agent = user_agent;
115        self
116    }
117
118    pub fn connect(&mut self) {
119        self.status = SessionStatus::Connected;
120        self.last_active_at = Utc::now();
121    }
122
123    pub fn disconnect(&mut self) {
124        self.status = SessionStatus::Disconnected;
125        self.last_active_at = Utc::now();
126    }
127
128    pub fn reconnecting(&mut self) {
129        self.status = SessionStatus::Reconnecting;
130        self.last_active_at = Utc::now();
131    }
132
133    pub fn touch(&mut self) {
134        self.last_active_at = Utc::now();
135    }
136
137    pub fn is_connected(&self) -> bool {
138        matches!(self.status, SessionStatus::Connected)
139    }
140
141    pub fn is_authenticated(&self) -> bool {
142        self.user_id.is_some()
143    }
144}
145
146#[cfg(test)]
147#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn test_session_id_generation() {
153        let id1 = SessionId::new();
154        let id2 = SessionId::new();
155        assert_ne!(id1, id2);
156    }
157
158    #[test]
159    fn test_session_status_conversion() {
160        assert_eq!(
161            "connected".parse::<SessionStatus>(),
162            Ok(SessionStatus::Connected)
163        );
164        assert_eq!(
165            "disconnected".parse::<SessionStatus>(),
166            Ok(SessionStatus::Disconnected)
167        );
168        assert_eq!(SessionStatus::Connected.as_str(), "connected");
169    }
170
171    #[test]
172    fn test_session_info_creation() {
173        let node_id = NodeId::new();
174        let session = SessionInfo::new(node_id);
175
176        assert_eq!(session.status, SessionStatus::Connecting);
177        assert!(!session.is_connected());
178        assert!(!session.is_authenticated());
179    }
180
181    #[test]
182    fn test_session_lifecycle() {
183        let node_id = NodeId::new();
184        let mut session = SessionInfo::new(node_id);
185
186        session.connect();
187        assert!(session.is_connected());
188
189        session = session.with_user_id("user123");
190        assert!(session.is_authenticated());
191
192        session.disconnect();
193        assert!(!session.is_connected());
194    }
195}