use std::{
any::{Any, TypeId},
cmp::Ordering,
collections::{BTreeMap, HashMap},
sync::{Mutex, PoisonError},
};
use crate::compare_revisions;
#[cfg(test)]
mod tests;
type Key = (TypeId, String);
#[derive(Debug)]
struct Entry {
state: Box<dyn Any + Send>,
last_event_id: String,
used: u64,
}
#[derive(Debug, Default)]
struct Entries {
by_key: HashMap<Key, Entry>,
by_use: BTreeMap<u64, Key>,
uses: u64,
}
impl Entries {
fn touch(&mut self, key: &Key) {
let Some(entry) = self.by_key.get_mut(key) else {
return;
};
self.uses += 1;
let _ = self.by_use.remove(&entry.used);
entry.used = self.uses;
let _ = self.by_use.insert(entry.used, key.clone());
}
}
#[derive(Debug)]
pub(super) struct StateCache {
max_subjects: usize,
entries: Mutex<Entries>,
}
impl StateCache {
pub(super) fn new(max_subjects: usize) -> Self {
Self {
max_subjects,
entries: Mutex::new(Entries::default()),
}
}
pub(super) fn get<S: Clone + 'static>(&self, subject: &str) -> Option<(S, String)> {
let mut entries = self.lock();
let key = (TypeId::of::<S>(), subject.to_owned());
entries.touch(&key);
let entry = entries.by_key.get(&key)?;
let state = entry
.state
.downcast_ref::<S>()
.expect("the type of the state is part of the key");
Some((state.clone(), entry.last_event_id.clone()))
}
pub(super) fn put<S: Send + 'static>(&self, subject: &str, state: S, last_event_id: String) {
let mut entries = self.lock();
let key = (TypeId::of::<S>(), subject.to_owned());
if let Some(entry) = entries.by_key.get_mut(&key) {
if matches!(
compare_revisions(&last_event_id, &entry.last_event_id),
Ok(Ordering::Greater | Ordering::Equal)
) {
entry.state = Box::new(state);
entry.last_event_id = last_event_id;
}
entries.touch(&key);
return;
}
let _ = entries.by_key.insert(
key.clone(),
Entry {
state: Box::new(state),
last_event_id,
used: 0,
},
);
entries.touch(&key);
if entries.by_key.len() > self.max_subjects
&& let Some((_, oldest)) = entries.by_use.pop_first()
{
let _ = entries.by_key.remove(&oldest);
}
}
fn lock(&self) -> std::sync::MutexGuard<'_, Entries> {
self.entries.lock().unwrap_or_else(PoisonError::into_inner)
}
}