leptos_next_metadata/og_image/
cache.rs1use std::collections::HashMap;
7use std::hash::{Hash, Hasher};
8use std::time::{Duration, Instant};
9
10#[cfg(target_arch = "wasm32")]
12use parking_lot::RwLock as AsyncRwLock;
13
14#[cfg(not(target_arch = "wasm32"))]
15use tokio::sync::RwLock as AsyncRwLock;
16
17use crate::og_image::types::*;
18use crate::Result;
19
20#[derive(Debug, Clone)]
22pub struct CacheEntry {
23 pub data: Vec<u8>,
25 pub created_at: Instant,
27 pub access_count: u64,
29 pub last_accessed: Instant,
31}
32
33impl CacheEntry {
34 pub fn new(data: Vec<u8>) -> Self {
35 let now = Instant::now();
36 Self {
37 data,
38 created_at: now,
39 access_count: 1,
40 last_accessed: now,
41 }
42 }
43
44 pub fn access(&mut self) {
45 self.access_count += 1;
46 self.last_accessed = Instant::now();
47 }
48
49 pub fn age(&self) -> Duration {
50 self.created_at.elapsed()
51 }
52
53 pub fn time_since_last_access(&self) -> Duration {
54 self.last_accessed.elapsed()
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct CacheKey {
61 pub template: String,
63 pub data_hash: u64,
65 pub size: (u32, u32),
67 pub format: String,
69}
70
71impl CacheKey {
72 pub fn new(params: &OgImageParams) -> Self {
73 let mut hasher = std::collections::hash_map::DefaultHasher::new();
74
75 for (key, value) in ¶ms.data {
77 key.hash(&mut hasher);
78 let value_str = serde_json::to_string(value).unwrap_or_default();
80 value_str.hash(&mut hasher);
81 }
82
83 Self {
84 template: params.template.clone(),
85 data_hash: hasher.finish(),
86 size: params.size.unwrap_or((1200, 630)),
87 format: format!("{:?}", params.format),
88 }
89 }
90}
91
92impl Hash for CacheKey {
93 fn hash<H: Hasher>(&self, state: &mut H) {
94 self.template.hash(state);
95 self.data_hash.hash(state);
96 self.size.hash(state);
97 self.format.hash(state);
98 }
99}
100
101#[async_trait::async_trait]
103pub trait CacheProvider: Send + Sync {
104 async fn get(&self, key: &CacheKey) -> Result<Option<Vec<u8>>>;
106
107 async fn set(&self, key: &CacheKey, data: &[u8]) -> Result<()>;
109
110 async fn remove(&self, key: &CacheKey) -> Result<()>;
112
113 async fn clear(&self) -> Result<()>;
115
116 async fn stats(&self) -> Result<CacheStats>;
118}
119
120pub struct MemoryCache {
122 cache: AsyncRwLock<HashMap<CacheKey, CacheEntry>>,
124 max_entries: usize,
126 max_age: Duration,
128 max_idle_time: Duration,
130}
131
132impl MemoryCache {
133 pub fn new(max_entries: usize) -> Self {
134 Self {
135 cache: AsyncRwLock::new(HashMap::new()),
136 max_entries,
137 max_age: Duration::from_secs(3600), max_idle_time: Duration::from_secs(1800), }
140 }
141
142 pub fn with_ttl(max_entries: usize, max_age: Duration, max_idle_time: Duration) -> Self {
143 Self {
144 cache: AsyncRwLock::new(HashMap::new()),
145 max_entries,
146 max_age,
147 max_idle_time,
148 }
149 }
150
151 async fn cleanup(&self) -> Result<()> {
153 let mut cache = self.cache.write().await;
154 let now = Instant::now();
155
156 cache.retain(|_, entry| {
157 let age = now.duration_since(entry.created_at);
158 let idle_time = now.duration_since(entry.last_accessed);
159
160 age < self.max_age && idle_time < self.max_idle_time
161 });
162
163 Ok(())
164 }
165
166 async fn evict_lru(&self) -> Result<()> {
168 let mut cache = self.cache.write().await;
169
170 if cache.len() < self.max_entries {
171 return Ok(());
172 }
173
174 let mut oldest_key = None;
176 let mut oldest_time = Instant::now();
177
178 for (key, entry) in cache.iter() {
179 if entry.last_accessed < oldest_time {
180 oldest_time = entry.last_accessed;
181 oldest_key = Some(key.clone());
182 }
183 }
184
185 if let Some(key) = oldest_key {
186 cache.remove(&key);
187 }
188
189 Ok(())
190 }
191}
192
193#[async_trait::async_trait]
194impl CacheProvider for MemoryCache {
195 async fn get(&self, key: &CacheKey) -> Result<Option<Vec<u8>>> {
196 let mut cache = self.cache.write().await;
197
198 if let Some(entry) = cache.get_mut(key) {
199 let age = entry.age();
201 let idle_time = entry.time_since_last_access();
202
203 if age < self.max_age && idle_time < self.max_idle_time {
204 entry.access();
205 return Ok(Some(entry.data.clone()));
206 } else {
207 cache.remove(key);
209 }
210 }
211
212 Ok(None)
213 }
214
215 async fn set(&self, key: &CacheKey, data: &[u8]) -> Result<()> {
216 self.cleanup().await?;
218
219 self.evict_lru().await?;
221
222 let mut cache = self.cache.write().await;
223 cache.insert(key.clone(), CacheEntry::new(data.to_vec()));
224
225 Ok(())
226 }
227
228 async fn remove(&self, key: &CacheKey) -> Result<()> {
229 let mut cache = self.cache.write().await;
230 cache.remove(key);
231 Ok(())
232 }
233
234 async fn clear(&self) -> Result<()> {
235 let mut cache = self.cache.write().await;
236 cache.clear();
237 Ok(())
238 }
239
240 async fn stats(&self) -> Result<CacheStats> {
241 let cache = self.cache.read().await;
242
243 let mut total_accesses = 0;
244 let mut total_age = Duration::ZERO;
245 let mut oldest_entry = Instant::now();
246
247 for entry in cache.values() {
248 total_accesses += entry.access_count;
249 total_age += entry.age();
250 if entry.created_at < oldest_entry {
251 oldest_entry = entry.created_at;
252 }
253 }
254
255 Ok(CacheStats {
256 entries: cache.len(),
257 max_entries: self.max_entries,
258 total_accesses,
259 average_age: if cache.is_empty() {
260 Duration::ZERO
261 } else {
262 total_age / cache.len() as u32
263 },
264 oldest_entry_age: oldest_entry.elapsed(),
265 hit_rate: 0.0, })
267 }
268}
269
270#[derive(Debug, Clone)]
272pub struct CacheStats {
273 pub entries: usize,
275 pub max_entries: usize,
277 pub total_accesses: u64,
279 pub average_age: Duration,
281 pub oldest_entry_age: Duration,
283 pub hit_rate: f64,
285}
286
287pub struct NoOpCache;
289
290#[async_trait::async_trait]
291impl CacheProvider for NoOpCache {
292 async fn get(&self, _key: &CacheKey) -> Result<Option<Vec<u8>>> {
293 Ok(None)
294 }
295
296 async fn set(&self, _key: &CacheKey, _data: &[u8]) -> Result<()> {
297 Ok(())
298 }
299
300 async fn remove(&self, _key: &CacheKey) -> Result<()> {
301 Ok(())
302 }
303
304 async fn clear(&self) -> Result<()> {
305 Ok(())
306 }
307
308 async fn stats(&self) -> Result<CacheStats> {
309 Ok(CacheStats {
310 entries: 0,
311 max_entries: 0,
312 total_accesses: 0,
313 average_age: Duration::ZERO,
314 oldest_entry_age: Duration::ZERO,
315 hit_rate: 0.0,
316 })
317 }
318}
319
320#[derive(Debug, Clone)]
322pub struct CacheConfig {
323 pub max_entries: usize,
325 pub max_age: Duration,
327 pub max_idle_time: Duration,
329 pub enabled: bool,
331}
332
333impl Default for CacheConfig {
334 fn default() -> Self {
335 Self {
336 max_entries: 100,
337 max_age: Duration::from_secs(3600), max_idle_time: Duration::from_secs(1800), enabled: true,
340 }
341 }
342}
343
344impl CacheConfig {
345 pub fn new(max_entries: usize) -> Self {
346 Self {
347 max_entries,
348 ..Default::default()
349 }
350 }
351
352 pub fn with_ttl(max_entries: usize, max_age: Duration, max_idle_time: Duration) -> Self {
353 Self {
354 max_entries,
355 max_age,
356 max_idle_time,
357 enabled: true,
358 }
359 }
360
361 pub fn disabled() -> Self {
362 Self {
363 enabled: false,
364 ..Default::default()
365 }
366 }
367}