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    /// Generate a new random session ID.
14    pub fn new() -> Self {
15        Self(Uuid::new_v4())
16    }
17
18    /// Create from an existing UUID.
19    pub fn from_uuid(id: Uuid) -> Self {
20        Self(id)
21    }
22
23    /// Get the inner UUID.
24    pub fn as_uuid(&self) -> Uuid {
25        self.0
26    }
27}
28
29impl Default for SessionId {
30    fn default() -> Self {
31        Self::new()
32    }
33}
34
35impl std::fmt::Display for SessionId {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        write!(f, "{}", self.0)
38    }
39}
40
41/// Session status.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum SessionStatus {
44    /// Session is connecting.
45    Connecting,
46    /// Session is connected and active.
47    Connected,
48    /// Session is reconnecting.
49    Reconnecting,
50    /// Session is disconnected.
51    Disconnected,
52}
53
54impl SessionStatus {
55    /// Convert to string.
56    pub fn as_str(&self) -> &'static str {
57        match self {
58            Self::Connecting => "connecting",
59            Self::Connected => "connected",
60            Self::Reconnecting => "reconnecting",
61            Self::Disconnected => "disconnected",
62        }
63    }
64}
65
66impl FromStr for SessionStatus {
67    type Err = std::convert::Infallible;
68
69    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
70        Ok(match s.to_lowercase().as_str() {
71            "connecting" => Self::Connecting,
72            "connected" => Self::Connected,
73            "reconnecting" => Self::Reconnecting,
74            "disconnected" => Self::Disconnected,
75            _ => Self::Disconnected,
76        })
77    }
78}
79
80/// Information about a WebSocket session.
81#[derive(Debug, Clone)]
82pub struct SessionInfo {
83    /// Unique session ID.
84    pub id: SessionId,
85    /// Node hosting this session.
86    pub node_id: NodeId,
87    /// User ID if authenticated.
88    pub user_id: Option<String>,
89    /// Current status.
90    pub status: SessionStatus,
91    /// Number of active subscriptions.
92    pub subscription_count: u32,
93    /// When the session was created.
94    pub created_at: DateTime<Utc>,
95    /// When the session was last active.
96    pub last_active_at: DateTime<Utc>,
97    /// Client IP address.
98    pub client_ip: Option<String>,
99    /// User agent string.
100    pub user_agent: Option<String>,
101}
102
103impl SessionInfo {
104    /// Create a new session info.
105    pub fn new(node_id: NodeId) -> Self {
106        let now = Utc::now();
107        Self {
108            id: SessionId::new(),
109            node_id,
110            user_id: None,
111            status: SessionStatus::Connecting,
112            subscription_count: 0,
113            created_at: now,
114            last_active_at: now,
115            client_ip: None,
116            user_agent: None,
117        }
118    }
119
120    /// Set user ID after authentication.
121    pub fn with_user_id(mut self, user_id: impl Into<String>) -> Self {
122        self.user_id = Some(user_id.into());
123        self
124    }
125
126    /// Set client metadata.
127    pub fn with_client_info(
128        mut self,
129        client_ip: Option<String>,
130        user_agent: Option<String>,
131    ) -> Self {
132        self.client_ip = client_ip;
133        self.user_agent = user_agent;
134        self
135    }
136
137    /// Mark session as connected.
138    pub fn connect(&mut self) {
139        self.status = SessionStatus::Connected;
140        self.last_active_at = Utc::now();
141    }
142
143    /// Mark session as disconnected.
144    pub fn disconnect(&mut self) {
145        self.status = SessionStatus::Disconnected;
146        self.last_active_at = Utc::now();
147    }
148
149    /// Mark session as reconnecting.
150    pub fn reconnecting(&mut self) {
151        self.status = SessionStatus::Reconnecting;
152        self.last_active_at = Utc::now();
153    }
154
155    /// Update last activity time.
156    pub fn touch(&mut self) {
157        self.last_active_at = Utc::now();
158    }
159
160    /// Check if session is connected.
161    pub fn is_connected(&self) -> bool {
162        matches!(self.status, SessionStatus::Connected)
163    }
164
165    /// Check if session is authenticated.
166    pub fn is_authenticated(&self) -> bool {
167        self.user_id.is_some()
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn test_session_id_generation() {
177        let id1 = SessionId::new();
178        let id2 = SessionId::new();
179        assert_ne!(id1, id2);
180    }
181
182    #[test]
183    fn test_session_status_conversion() {
184        assert_eq!(
185            "connected".parse::<SessionStatus>(),
186            Ok(SessionStatus::Connected)
187        );
188        assert_eq!(
189            "disconnected".parse::<SessionStatus>(),
190            Ok(SessionStatus::Disconnected)
191        );
192        assert_eq!(SessionStatus::Connected.as_str(), "connected");
193    }
194
195    #[test]
196    fn test_session_info_creation() {
197        let node_id = NodeId::new();
198        let session = SessionInfo::new(node_id);
199
200        assert_eq!(session.status, SessionStatus::Connecting);
201        assert!(!session.is_connected());
202        assert!(!session.is_authenticated());
203    }
204
205    #[test]
206    fn test_session_lifecycle() {
207        let node_id = NodeId::new();
208        let mut session = SessionInfo::new(node_id);
209
210        // Connect
211        session.connect();
212        assert!(session.is_connected());
213
214        // Authenticate
215        session = session.with_user_id("user123");
216        assert!(session.is_authenticated());
217
218        // Disconnect
219        session.disconnect();
220        assert!(!session.is_connected());
221    }
222}