use crate::authn::ids::UserId;
use crate::session::id::SessionId;
use chrono::{DateTime, Utc};
use std::collections::{HashMap, HashSet, VecDeque};
use std::sync::{Arc, Mutex, MutexGuard};
pub type SidKey = (String, String);
pub type SidEntry = (UserId, SessionId, DateTime<Utc>);
#[derive(Clone, Default)]
pub struct SidMap {
state: Arc<Mutex<SidState>>,
}
#[derive(Default)]
struct SidState {
entries: HashMap<SidKey, SidEntry>,
by_user: HashMap<(String, UserId), HashSet<String>>,
order: VecDeque<(DateTime<Utc>, SidKey)>,
subjects: HashMap<(String, UserId), HashSet<SessionId>>,
subject_count: usize,
subject_order: VecDeque<(String, UserId, SessionId)>,
}
const QUEUE_SLACK: usize = 64;
impl SidState {
fn insert(&mut self, key: SidKey, entry: SidEntry) -> Option<SidEntry> {
let (user, _, at) = entry;
let displaced = self.entries.insert(key.clone(), entry);
if let Some((old_user, _, _)) = &displaced
&& *old_user != user
{
self.unindex(&key.0, old_user, &key.1);
}
self.by_user
.entry((key.0.clone(), user))
.or_default()
.insert(key.1.clone());
self.order.push_back((at, key));
if self.order.len() > 2 * self.entries.len() + QUEUE_SLACK {
let entries = &self.entries;
self.order
.retain(|(at, key)| entries.get(key).is_some_and(|e| e.2 == *at));
}
displaced
}
fn register_subject(&mut self, issuer: &str, user: UserId, session: SessionId) {
let fresh = self
.subjects
.entry((issuer.to_string(), user))
.or_default()
.insert(session);
if !fresh {
return;
}
self.subject_count += 1;
self.subject_order
.push_back((issuer.to_string(), user, session));
if self.subject_order.len() > 2 * self.subject_count + QUEUE_SLACK {
let subjects = &self.subjects;
self.subject_order.retain(|(iss, u, s)| {
subjects
.get(&(iss.clone(), *u))
.is_some_and(|set| set.contains(s))
});
}
}
fn forget_subject(&mut self, issuer: &str, user: &UserId, session: &SessionId) -> bool {
let slot = (issuer.to_string(), *user);
let Some(set) = self.subjects.get_mut(&slot) else {
return false;
};
let removed = set.remove(session);
if set.is_empty() {
self.subjects.remove(&slot);
}
if removed {
self.subject_count -= 1;
}
removed
}
fn unindex(&mut self, issuer: &str, user: &UserId, sid: &str) {
let slot = (issuer.to_string(), *user);
if let Some(sids) = self.by_user.get_mut(&slot) {
sids.remove(sid);
if sids.is_empty() {
self.by_user.remove(&slot);
}
}
}
fn remove(&mut self, key: &SidKey) -> Option<SidEntry> {
let entry = self.entries.remove(key)?;
self.unindex(&key.0, &entry.0, &key.1);
Some(entry)
}
fn evict_front(
&mut self,
max: usize,
due: impl Fn(DateTime<Utc>) -> bool,
) -> Vec<(SidKey, SidEntry)> {
let mut evicted = Vec::new();
for _ in 0..max {
match self.order.front() {
Some((at, _)) if due(*at) => {}
_ => break,
}
let Some((at, key)) = self.order.pop_front() else {
break;
};
if self.entries.get(&key).is_some_and(|e| e.2 == at)
&& let Some(entry) = self.remove(&key)
{
evicted.push((key, entry));
}
}
evicted
}
}
impl SidMap {
pub fn new() -> Self {
Self::default()
}
fn lock(&self) -> MutexGuard<'_, SidState> {
self.state
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
pub fn insert(
&self,
key: SidKey,
user: UserId,
session: SessionId,
inserted_at: DateTime<Utc>,
) -> Option<SidEntry> {
self.lock().insert(key, (user, session, inserted_at))
}
pub fn get(&self, key: &SidKey) -> Option<SidEntry> {
self.lock().entries.get(key).copied()
}
pub fn contains_key(&self, key: &SidKey) -> bool {
self.lock().entries.contains_key(key)
}
pub fn remove(&self, key: &SidKey) -> Option<SidEntry> {
let mut state = self.lock();
let entry = state.remove(key)?;
state.forget_subject(&key.0, &entry.0, &entry.1);
Some(entry)
}
pub fn register_subject_session(&self, issuer: &str, user: UserId, session: SessionId) {
self.lock().register_subject(issuer, user, session);
}
pub fn forget_subject_session(&self, issuer: &str, user: &UserId, session: &SessionId) -> bool {
self.lock().forget_subject(issuer, user, session)
}
pub fn take_subject_sessions(&self, issuer: &str, user: &UserId) -> Vec<SessionId> {
let mut state = self.lock();
let mut out: Vec<SessionId> = state
.subjects
.remove(&(issuer.to_string(), *user))
.map(|set| set.into_iter().collect())
.unwrap_or_default();
state.subject_count -= out.len();
if let Some(sids) = state.by_user.get(&(issuer.to_string(), *user)).cloned() {
for sid in sids {
if let Some((_, session, _)) = state.remove(&(issuer.to_string(), sid))
&& !out.contains(&session)
{
out.push(session);
}
}
}
out
}
pub fn sweep_subject_candidates(&self, max: usize) -> Vec<(String, UserId, SessionId)> {
let mut state = self.lock();
let mut out = Vec::new();
for _ in 0..max.min(state.subject_order.len()) {
let Some((iss, user, session)) = state.subject_order.pop_front() else {
break;
};
let live = state
.subjects
.get(&(iss.clone(), user))
.is_some_and(|set| set.contains(&session));
if live {
state.subject_order.push_back((iss.clone(), user, session));
out.push((iss, user, session));
}
}
out
}
pub fn subject_len(&self) -> usize {
self.lock().subject_count
}
pub fn take_user_sessions(&self, issuer: &str, user: &UserId) -> Vec<SessionId> {
let mut state = self.lock();
let Some(sids) = state.by_user.get(&(issuer.to_string(), *user)).cloned() else {
return Vec::new();
};
let taken: Vec<SessionId> = sids
.into_iter()
.filter_map(|sid| state.remove(&(issuer.to_string(), sid)))
.map(|(_, session, _)| session)
.collect();
for session in &taken {
state.forget_subject(issuer, user, session);
}
taken
}
pub fn remove_session(&self, session: &SessionId) -> usize {
let mut state = self.lock();
let keys: Vec<SidKey> = state
.entries
.iter()
.filter(|(_, e)| e.1 == *session)
.map(|(k, _)| k.clone())
.collect();
let removed = keys.iter().filter_map(|k| state.remove(k)).count();
let held: Vec<(String, UserId)> = state
.subjects
.iter()
.filter(|(_, set)| set.contains(session))
.map(|(slot, _)| slot.clone())
.collect();
for (issuer, user) in held {
state.forget_subject(&issuer, &user, session);
}
removed
}
pub fn evict_inserted_before(
&self,
cutoff: DateTime<Utc>,
max: usize,
) -> Vec<(SidKey, SidEntry)> {
self.lock().evict_front(max, |at| at < cutoff)
}
pub fn evict_oldest(&self, max: usize) -> Vec<(SidKey, SidEntry)> {
self.lock().evict_front(max, |_| true)
}
pub fn len(&self) -> usize {
self.lock().entries.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
impl std::fmt::Debug for SidMap {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SidMap")
.field("len", &self.len())
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests;