use crate::web::ApiError; use lru::LruCache; use magnetar_calckey_model::emoji::{EmojiResolver, EmojiTag}; use magnetar_calckey_model::{ck, CalckeyDbError, CalckeyModel}; use std::collections::HashSet; use std::sync::Arc; use strum::EnumVariantNames; use thiserror::Error; use tokio::sync::Mutex; #[derive(Debug, Error, EnumVariantNames)] pub enum EmojiCacheError { #[error("Database error: {0}")] DbError(#[from] CalckeyDbError), } impl From for ApiError { fn from(err: EmojiCacheError) -> Self { let mut api_error: ApiError = match err { EmojiCacheError::DbError(err) => err.into(), }; api_error.message = format!("Emoji cache error: {}", api_error.message); api_error } } #[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)] struct EmojiLocator { name: String, host: Option, } pub struct EmojiCacheService { cache: Mutex>>, db: EmojiResolver, } impl EmojiCacheService { pub(super) fn new(db: CalckeyModel) -> Self { const CACHE_SIZE: usize = 4096; Self { cache: Mutex::new(LruCache::new(CACHE_SIZE.try_into().unwrap())), db: EmojiResolver::new(db), } } pub async fn get( &self, name: &str, host: Option<&str>, ) -> Result>, EmojiCacheError> { let loc = EmojiLocator { name: name.to_string(), host: host.map(str::to_string), }; let mut read = self.cache.lock().await; if let Some(emoji) = read.get(&loc) { return Ok(Some(emoji.clone())); } drop(read); let emoji = self.db.fetch_emoji(name, host).await?; if emoji.is_none() { return Ok(None); } let mut write = self.cache.lock().await; let emoji = Arc::new(emoji.unwrap()); write.put(loc, emoji.clone()); Ok(Some(emoji)) } pub async fn get_many( &self, names: &[String], host: Option<&str>, ) -> Result>, EmojiCacheError> { let locs = names .iter() .map(|n| EmojiLocator { name: n.clone(), host: host.map(str::to_string), }) .collect::>(); let mut to_resolve = Vec::new(); let mut resolved = Vec::new(); let mut read = self.cache.lock().await; for loc in locs { if let Some(emoji) = read.get(&loc) { resolved.push(emoji.clone()); } else { to_resolve.push(loc.name); } } drop(read); let emoji = self .db .fetch_many_emojis(&to_resolve, host) .await? .into_iter() .map(Arc::new) .collect::>(); resolved.extend(emoji.iter().cloned()); let mut write = self.cache.lock().await; emoji.iter().for_each(|e| { write.put( EmojiLocator { name: e.name.clone(), host: e.host.clone(), }, e.clone(), ); }); Ok(resolved) } pub async fn get_many_tagged( &self, tags: &[EmojiTag<'_>], ) -> Result>, EmojiCacheError> { let locs = tags .iter() .map(|tag| EmojiLocator { name: tag.name.to_string(), host: tag.host.map(str::to_string), }) .collect::>(); let mut to_resolve = Vec::new(); let mut resolved = Vec::new(); let mut read = self.cache.lock().await; for loc in locs.iter() { if let Some(emoji) = read.get(loc) { resolved.push(emoji.clone()); } else { to_resolve.push(EmojiTag { name: &loc.name, host: loc.host.as_deref(), }); } } drop(read); let emoji = self .db .fetch_many_tagged_emojis(&to_resolve) .await? .into_iter() .map(Arc::new) .collect::>(); resolved.extend(emoji.iter().cloned()); let mut write = self.cache.lock().await; emoji.iter().for_each(|e| { write.put( EmojiLocator { name: e.name.clone(), host: e.host.clone(), }, e.clone(), ); }); Ok(resolved) } }