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    /// 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)]
172#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
173mod tests {
174    use super::*;
175
176    #[test]
177    fn test_session_id_generation() {
178        let id1 = SessionId::new();
179        let id2 = SessionId::new();
180        assert_ne!(id1, id2);
181    }
182
183    #[test]
184    fn test_session_status_conversion() {
185        assert_eq!(
186            "connected".parse::<SessionStatus>(),
187            Ok(SessionStatus::Connected)
188        );
189        assert_eq!(
190            "disconnected".parse::<SessionStatus>(),
191            Ok(SessionStatus::Disconnected)
192        );
193        assert_eq!(SessionStatus::Connected.as_str(), "connected");
194    }
195
196    #[test]
197    fn test_session_info_creation() {
198        let node_id = NodeId::new();
199        let session = SessionInfo::new(node_id);
200
201        assert_eq!(session.status, SessionStatus::Connecting);
202        assert!(!session.is_connected());
203        assert!(!session.is_authenticated());
204    }
205
206    #[test]
207    fn test_session_lifecycle() {
208        let node_id = NodeId::new();
209        let mut session = SessionInfo::new(node_id);
210
211        // Connect
212        session.connect();
213        assert!(session.is_connected());
214
215        // Authenticate
216        session = session.with_user_id("user123");
217        assert!(session.is_authenticated());
218
219        // Disconnect
220        session.disconnect();
221        assert!(!session.is_connected());
222    }
223}