use dashmap::DashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::broadcast;
use uuid::Uuid;
#[derive(Clone)]
pub struct SessionStore {
sessions: Arc<DashMap<String, SessionState>>,
sse_channels: Arc<DashMap<String, broadcast::Sender<String>>>,
}
struct SessionState {
last_seen: Instant,
}
impl Default for SessionStore {
fn default() -> Self {
Self::new()
}
}
impl SessionStore {
#[must_use]
pub fn new() -> Self {
Self {
sessions: Arc::new(DashMap::new()),
sse_channels: Arc::new(DashMap::new()),
}
}
#[must_use]
pub fn create(&self) -> String {
let id = Uuid::new_v4().to_string();
let now = Instant::now();
self.sessions
.insert(id.clone(), SessionState { last_seen: now });
id
}
pub fn touch(&self, id: &str) {
if let Some(mut session) = self.sessions.get_mut(id) {
session.last_seen = Instant::now();
}
}
#[must_use]
pub fn exists(&self, id: &str) -> bool {
self.sessions.contains_key(id)
}
#[must_use]
pub fn create_session(&self) -> (String, broadcast::Receiver<String>) {
let id = self.create();
let (tx, rx) = broadcast::channel(100);
self.sse_channels.insert(id.clone(), tx);
(id, rx)
}
#[must_use]
pub fn get_receiver(&self, id: &str) -> Option<broadcast::Receiver<String>> {
self.sse_channels.get(id).map(|tx| tx.subscribe())
}
pub fn cleanup(&self, max_age: Duration) {
let now = Instant::now();
self.sessions
.retain(|_, session| now.duration_since(session.last_seen) < max_age);
}
}
pub trait SessionManager {
fn create_session(&self) -> String;
fn touch_session(&self, id: &str);
fn session_exists(&self, id: &str) -> bool;
}
impl SessionManager for SessionStore {
fn create_session(&self) -> String {
self.create()
}
fn touch_session(&self, id: &str) {
self.touch(id);
}
fn session_exists(&self, id: &str) -> bool {
self.exists(id)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_session_store_creation() {
let store = SessionStore::new();
let id = store.create();
assert!(!id.is_empty());
assert!(store.exists(&id));
}
#[test]
fn test_session_store_default() {
let store = SessionStore::default();
let id = store.create();
assert!(store.exists(&id));
}
#[test]
fn test_session_store_touch() {
let store = SessionStore::new();
let id = store.create();
store.touch(&id);
assert!(store.exists(&id));
store.touch("non-existent");
}
#[test]
fn test_session_store_exists() {
let store = SessionStore::new();
let id = store.create();
assert!(store.exists(&id));
assert!(!store.exists("non-existent"));
}
#[test]
fn test_session_store_cleanup() {
let store = SessionStore::new();
let id = store.create();
assert!(store.exists(&id));
store.cleanup(Duration::from_secs(0));
assert!(!store.exists(&id));
}
#[tokio::test]
async fn test_session_store_sse_channel() {
let store = SessionStore::new();
let (id, mut rx) = store.create_session();
let tx = store.sse_channels.get(&id).unwrap();
tx.send("test message".to_string()).unwrap();
drop(tx);
let msg = rx.recv().await.unwrap();
assert_eq!(msg, "test message");
}
#[test]
fn test_session_store_get_receiver() {
let store = SessionStore::new();
let (id, _rx) = store.create_session();
let rx2 = store.get_receiver(&id);
assert!(rx2.is_some());
let rx3 = store.get_receiver("non-existent");
assert!(rx3.is_none());
}
#[test]
fn test_session_manager_trait() {
let store = SessionStore::new();
let id = SessionManager::create_session(&store);
assert!(SessionManager::session_exists(&store, &id));
SessionManager::touch_session(&store, &id);
assert!(SessionManager::session_exists(&store, &id));
assert!(!SessionManager::session_exists(&store, "non-existent"));
}
#[test]
fn test_multiple_sessions() {
let store = SessionStore::new();
let id1 = store.create();
let id2 = store.create();
let id3 = store.create();
assert!(store.exists(&id1));
assert!(store.exists(&id2));
assert!(store.exists(&id3));
assert_ne!(id1, id2);
assert_ne!(id2, id3);
assert_ne!(id1, id3);
}
}