use std::{ sync::Arc, time::{Duration, Instant}, }; use tokio::sync::RwLock; use super::Media; #[derive(Debug)] pub struct CachedMediaResult { media_result: Option, cache_time: Instant, ttl: Duration, } impl CachedMediaResult { fn has_expired(&self) -> bool { let ttl = if self.media_result.is_some() { self.ttl } else { Duration::from_secs(30) }; self.cache_time.elapsed() >= ttl } } pub type MediaCache = Arc>>; pub fn try_cache_or_fetch(cache: &MediaCache, ttl: Duration, fetcher: F) -> Option where F: FnOnce() -> Fut + Send + 'static, Fut: Future> + Send, { { let cached = cache.try_read().ok()?; if let Some(cached) = &*cached && !cached.has_expired() { return cached.media_result.clone(); } } let cache = cache.clone(); tokio::spawn(async move { synced_fetch(&cache, ttl, fetcher).await; }); None } pub async fn cache_or_fetch(cache: &MediaCache, ttl: Duration, fetcher: F) -> Option where F: FnOnce() -> Fut, Fut: Future>, { { let cached = cache.read().await; if let Some(cached) = &*cached && !cached.has_expired() { return cached.media_result.clone(); } } synced_fetch(cache, ttl, fetcher).await } async fn synced_fetch(cache: &MediaCache, ttl: Duration, fetcher: F) -> Option where F: FnOnce() -> Fut, Fut: Future>, { let mut cached = cache.write().await; if let Some(cached) = &*cached && !cached.has_expired() { return cached.media_result.clone(); } let result = fetcher() .await .map_err(|error| tracing::warn!(?error, "failed to scrape backend")) .ok(); *cached = Some(CachedMediaResult { media_result: result.clone(), cache_time: Instant::now(), ttl, }); result }