forge_core/realtime/
session.rs1use std::str::FromStr;
2
3use chrono::{DateTime, Utc};
4use uuid::Uuid;
5
6use crate::cluster::NodeId;
7
8#[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#[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#[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}