use keepsake::{RelationDefinition, RelationId, RelationKey};
#[cfg(feature = "cache")]
use std::collections::BTreeMap;
use std::fmt::Debug;
#[cfg(feature = "cache")]
use std::sync::{Arc, RwLock};
#[cfg(feature = "cache")]
use std::time::{Duration, Instant};
#[async_trait::async_trait]
pub trait RelationCache: Send + Sync + Debug {
async fn get_by_id(&self, relation_id: RelationId) -> Option<RelationDefinition>;
async fn get_by_key(&self, key: &RelationKey) -> Option<RelationDefinition>;
async fn store(&self, relation: &RelationDefinition);
async fn remove_by_id(&self, relation_id: RelationId);
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NoopRelationCache;
#[async_trait::async_trait]
impl RelationCache for NoopRelationCache {
async fn get_by_id(&self, _relation_id: RelationId) -> Option<RelationDefinition> {
None
}
async fn get_by_key(&self, _key: &RelationKey) -> Option<RelationDefinition> {
None
}
async fn store(&self, _relation: &RelationDefinition) {}
async fn remove_by_id(&self, _relation_id: RelationId) {}
}
#[cfg(feature = "cache")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LocalRelationCacheConfig {
pub ttl: Duration,
}
#[cfg(feature = "cache")]
impl LocalRelationCacheConfig {
#[must_use]
pub const fn new(ttl: Duration) -> Self {
Self { ttl }
}
}
#[cfg(feature = "cache")]
#[derive(Debug, Clone)]
pub struct LocalRelationCache {
config: LocalRelationCacheConfig,
state: Arc<RwLock<LocalRelationCacheState>>,
}
#[cfg(feature = "cache")]
impl LocalRelationCache {
#[must_use]
pub fn new(config: LocalRelationCacheConfig) -> Self {
Self {
config,
state: Arc::new(RwLock::new(LocalRelationCacheState::default())),
}
}
}
#[cfg(feature = "cache")]
#[async_trait::async_trait]
impl RelationCache for LocalRelationCache {
async fn get_by_id(&self, relation_id: RelationId) -> Option<RelationDefinition> {
self.state
.read()
.ok()
.and_then(|state| state.by_id.get(&relation_id).cloned())
.and_then(CacheEntry::fresh_relation)
}
async fn get_by_key(&self, key: &RelationKey) -> Option<RelationDefinition> {
self.state
.read()
.ok()
.and_then(|state| state.by_key.get(key).cloned())
.and_then(CacheEntry::fresh_relation)
}
async fn store(&self, relation: &RelationDefinition) {
let entry = CacheEntry {
relation: relation.clone(),
expires_at: Instant::now() + self.config.ttl,
};
if let Ok(mut state) = self.state.write() {
state.by_id.insert(relation.id, entry.clone());
state.by_key.insert(relation.key.clone(), entry);
}
}
async fn remove_by_id(&self, relation_id: RelationId) {
if let Ok(mut state) = self.state.write()
&& let Some(entry) = state.by_id.remove(&relation_id)
{
state.by_key.remove(&entry.relation.key);
}
}
}
#[cfg(feature = "cache")]
#[derive(Debug, Default)]
struct LocalRelationCacheState {
by_id: BTreeMap<RelationId, CacheEntry>,
by_key: BTreeMap<RelationKey, CacheEntry>,
}
#[cfg(feature = "cache")]
#[derive(Debug, Clone)]
struct CacheEntry {
relation: RelationDefinition,
expires_at: Instant,
}
#[cfg(feature = "cache")]
impl CacheEntry {
fn fresh_relation(self) -> Option<RelationDefinition> {
(Instant::now() <= self.expires_at).then_some(self.relation)
}
}