keepsake_sqlx/repository/
cache.rs1use keepsake::{RelationDefinition, RelationId, RelationKey};
2
3#[cfg(feature = "cache")]
4use std::collections::BTreeMap;
5#[cfg(feature = "cache")]
6use std::sync::{Arc, RwLock};
7#[cfg(feature = "cache")]
8use std::time::{Duration, Instant};
9
10#[async_trait::async_trait]
12pub trait RelationCache: Send + Sync + std::fmt::Debug {
13 async fn get_by_id(&self, relation_id: RelationId) -> Option<RelationDefinition>;
15
16 async fn get_by_key(&self, key: &RelationKey) -> Option<RelationDefinition>;
18
19 async fn store(&self, relation: &RelationDefinition);
21
22 async fn remove_by_id(&self, relation_id: RelationId);
24}
25
26#[derive(Debug, Clone, Copy, Default)]
28pub struct NoopRelationCache;
29
30#[async_trait::async_trait]
31impl RelationCache for NoopRelationCache {
32 async fn get_by_id(&self, _relation_id: RelationId) -> Option<RelationDefinition> {
33 None
34 }
35
36 async fn get_by_key(&self, _key: &RelationKey) -> Option<RelationDefinition> {
37 None
38 }
39
40 async fn store(&self, _relation: &RelationDefinition) {}
41
42 async fn remove_by_id(&self, _relation_id: RelationId) {}
43}
44
45#[cfg(feature = "cache")]
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub struct LocalRelationCacheConfig {
49 pub ttl: Duration,
51}
52
53#[cfg(feature = "cache")]
54impl LocalRelationCacheConfig {
55 #[must_use]
57 pub const fn new(ttl: Duration) -> Self {
58 Self { ttl }
59 }
60}
61
62#[cfg(feature = "cache")]
64#[derive(Debug, Clone)]
65pub struct LocalRelationCache {
66 config: LocalRelationCacheConfig,
67 state: Arc<RwLock<LocalRelationCacheState>>,
71}
72
73#[cfg(feature = "cache")]
74impl LocalRelationCache {
75 #[must_use]
77 pub fn new(config: LocalRelationCacheConfig) -> Self {
78 Self {
79 config,
80 state: Arc::new(RwLock::new(LocalRelationCacheState::default())),
81 }
82 }
83}
84
85#[cfg(feature = "cache")]
86#[async_trait::async_trait]
87impl RelationCache for LocalRelationCache {
88 async fn get_by_id(&self, relation_id: RelationId) -> Option<RelationDefinition> {
89 self.state
90 .read()
91 .ok()
92 .and_then(|state| state.by_id.get(&relation_id).cloned())
93 .and_then(CacheEntry::fresh_relation)
94 }
95
96 async fn get_by_key(&self, key: &RelationKey) -> Option<RelationDefinition> {
97 self.state
98 .read()
99 .ok()
100 .and_then(|state| state.by_key.get(key).cloned())
101 .and_then(CacheEntry::fresh_relation)
102 }
103
104 async fn store(&self, relation: &RelationDefinition) {
105 let entry = CacheEntry {
106 relation: relation.clone(),
107 expires_at: Instant::now() + self.config.ttl,
108 };
109 if let Ok(mut state) = self.state.write() {
110 state.by_id.insert(relation.id, entry.clone());
111 state.by_key.insert(relation.key.clone(), entry);
112 }
113 }
114
115 async fn remove_by_id(&self, relation_id: RelationId) {
116 if let Ok(mut state) = self.state.write()
117 && let Some(entry) = state.by_id.remove(&relation_id)
118 {
119 state.by_key.remove(&entry.relation.key);
120 }
121 }
122}
123
124#[cfg(feature = "cache")]
125#[derive(Debug, Default)]
126struct LocalRelationCacheState {
127 by_id: BTreeMap<RelationId, CacheEntry>,
128 by_key: BTreeMap<RelationKey, CacheEntry>,
129}
130
131#[cfg(feature = "cache")]
132#[derive(Debug, Clone)]
133struct CacheEntry {
134 relation: RelationDefinition,
135 expires_at: Instant,
136}
137
138#[cfg(feature = "cache")]
139impl CacheEntry {
140 fn fresh_relation(self) -> Option<RelationDefinition> {
141 (Instant::now() <= self.expires_at).then_some(self.relation)
142 }
143}