use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use tokio_util::sync::CancellationToken;
use crate::app_server::AppServerError;
pub struct AppServerSessionRegistry<Runtime> {
active_turn_cancellations: Arc<Mutex<HashMap<String, CancellationToken>>>,
provider_name: &'static str,
sessions: Arc<Mutex<HashMap<String, Runtime>>>,
}
impl<Runtime> AppServerSessionRegistry<Runtime> {
pub fn new(provider_name: &'static str) -> Self {
Self {
active_turn_cancellations: Arc::new(Mutex::new(HashMap::new())),
provider_name,
sessions: Arc::new(Mutex::new(HashMap::new())),
}
}
pub fn take_session(&self, session_id: &str) -> Result<Option<Runtime>, AppServerError> {
let mut sessions = self
.sessions
.lock()
.map_err(|_| AppServerError::LockPoisoned {
provider: self.provider_name,
})?;
Ok(sessions.remove(session_id))
}
pub fn store_session(
&self,
session_id: String,
session: Runtime,
) -> Result<(), AppServerError> {
let mut sessions = self
.sessions
.lock()
.map_err(|_| AppServerError::LockPoisoned {
provider: self.provider_name,
})?;
sessions.insert(session_id, session);
Ok(())
}
pub fn store_session_or_recover(
&self,
session_id: String,
session: Runtime,
) -> Result<(), (AppServerError, Runtime)> {
let Ok(mut sessions) = self.sessions.lock() else {
return Err((
AppServerError::LockPoisoned {
provider: self.provider_name,
},
session,
));
};
sessions.insert(session_id, session);
Ok(())
}
pub fn provider_name(&self) -> &'static str {
self.provider_name
}
pub fn register_active_turn(
&self,
session_id: &str,
) -> Result<ActiveAppServerTurn, AppServerError> {
let token = CancellationToken::new();
let mut active_turn_cancellations =
self.active_turn_cancellations
.lock()
.map_err(|_| AppServerError::LockPoisoned {
provider: self.provider_name,
})?;
active_turn_cancellations.insert(session_id.to_string(), token.clone());
Ok(ActiveAppServerTurn {
active_turn_cancellations: Arc::clone(&self.active_turn_cancellations),
session_id: session_id.to_string(),
token,
})
}
pub fn cancel_active_turn(&self, session_id: &str) -> Result<bool, AppServerError> {
let active_turn_cancellations =
self.active_turn_cancellations
.lock()
.map_err(|_| AppServerError::LockPoisoned {
provider: self.provider_name,
})?;
let Some(token) = active_turn_cancellations.get(session_id) else {
return Ok(false);
};
token.cancel();
Ok(true)
}
}
impl<Runtime> Clone for AppServerSessionRegistry<Runtime> {
fn clone(&self) -> Self {
Self {
active_turn_cancellations: Arc::clone(&self.active_turn_cancellations),
provider_name: self.provider_name,
sessions: Arc::clone(&self.sessions),
}
}
}
pub struct ActiveAppServerTurn {
active_turn_cancellations: Arc<Mutex<HashMap<String, CancellationToken>>>,
session_id: String,
token: CancellationToken,
}
impl ActiveAppServerTurn {
pub fn token(&self) -> CancellationToken {
self.token.clone()
}
}
impl Drop for ActiveAppServerTurn {
fn drop(&mut self) {
if let Ok(mut active_turn_cancellations) = self.active_turn_cancellations.lock() {
active_turn_cancellations.remove(&self.session_id);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_returns_registry_with_provider_name() {
let registry = AppServerSessionRegistry::<String>::new("test-provider");
assert_eq!(registry.provider_name(), "test-provider");
}
#[test]
fn store_and_take_session_round_trips_runtime() {
let registry = AppServerSessionRegistry::<String>::new("test");
registry
.store_session("session-1".to_string(), "runtime-a".to_string())
.expect("store should succeed");
let taken = registry
.take_session("session-1")
.expect("take should succeed");
assert_eq!(taken, Some("runtime-a".to_string()));
}
#[test]
fn take_session_returns_none_for_missing_id() {
let registry = AppServerSessionRegistry::<String>::new("test");
let taken = registry
.take_session("missing")
.expect("take should succeed");
assert_eq!(taken, None);
}
#[test]
fn take_session_removes_entry_from_registry() {
let registry = AppServerSessionRegistry::<String>::new("test");
registry
.store_session("session-1".to_string(), "runtime-a".to_string())
.expect("store should succeed");
let _ = registry.take_session("session-1");
let second_take = registry
.take_session("session-1")
.expect("take should succeed");
assert_eq!(second_take, None);
}
#[test]
fn store_session_or_recover_stores_runtime_on_healthy_lock() {
let registry = AppServerSessionRegistry::<String>::new("test");
let result =
registry.store_session_or_recover("session-1".to_string(), "runtime-a".to_string());
assert!(result.is_ok());
let taken = registry
.take_session("session-1")
.expect("take should succeed");
assert_eq!(taken, Some("runtime-a".to_string()));
}
#[test]
fn clone_shares_underlying_session_map() {
let registry = AppServerSessionRegistry::<String>::new("test");
let cloned = registry.clone();
registry
.store_session("session-1".to_string(), "runtime-a".to_string())
.expect("store should succeed");
let taken = cloned
.take_session("session-1")
.expect("take on clone should succeed");
assert_eq!(taken, Some("runtime-a".to_string()));
}
#[test]
fn cancel_active_turn_signals_registered_token() {
let registry = AppServerSessionRegistry::<String>::new("test");
let active_turn = registry
.register_active_turn("session-1")
.expect("register should succeed");
let token = active_turn.token();
let did_cancel = registry
.cancel_active_turn("session-1")
.expect("cancel should succeed");
assert!(did_cancel);
assert!(token.is_cancelled());
}
#[test]
fn active_turn_guard_unregisters_on_drop() {
let registry = AppServerSessionRegistry::<String>::new("test");
let active_turn = registry
.register_active_turn("session-1")
.expect("register should succeed");
drop(active_turn);
let did_cancel = registry
.cancel_active_turn("session-1")
.expect("cancel should succeed");
assert!(!did_cancel);
}
}