use std::sync::Arc; use std::time::Duration; use config::cache::CacheConfig; use config::redis::RedisMode; use oxcache::backend::{CacheReader, CacheWriter, MokaMemoryBackend, RedisBackend, RedisMode as OxRedisMode}; use oxcache::cache::{ChainCache, ChainLink}; use serde::de::DeserializeOwned; use serde::Serialize; use crate::error::CacheError; pub struct CacheClient { chain: Arc, } impl CacheClient { pub async fn new(config: &CacheConfig) -> Result { config.validate().map_err(CacheError::InvalidConfig)?; let l1 = build_l1(config)?; let l2 = build_l2(config).await?; let mut builder = ChainCache::builder() .link(ChainLink::from_backend(l1)) .link(ChainLink::from_backend(l2)); if config.backfill_enabled { builder = builder.enable_backfill(); } if let Some(ttl) = config.default_ttl_secs { builder = builder.default_time_to_live(Duration::from_secs(ttl)); } Ok(Self { chain: Arc::new(builder.build()), }) } pub fn raw(&self) -> &ChainCache { &self.chain } pub async fn get(&self, key: &str) -> Result, CacheError> where V: DeserializeOwned, { let raw = self.chain.get(key).await?; match raw { Some(bytes) => { let value = serde_json::from_slice::(&bytes)?; Ok(Some(value)) } None => Ok(None), } } pub async fn set( &self, key: &str, value: &V, ttl: Option, ) -> Result<(), CacheError> where V: Serialize, { let bytes = serde_json::to_vec(value)?; self.chain.set(key, bytes, ttl).await?; Ok(()) } pub async fn delete(&self, key: &str) -> Result<(), CacheError> { self.chain.delete(key).await?; Ok(()) } pub async fn exists(&self, key: &str) -> Result { Ok(self.chain.exists(key).await?) } pub async fn get_bytes(&self, key: &str) -> Result>, CacheError> { Ok(self.chain.get(key).await?) } pub async fn set_bytes( &self, key: &str, value: Vec, ttl: Option, ) -> Result<(), CacheError> { self.chain.set(key, value, ttl).await?; Ok(()) } } fn build_l1(config: &CacheConfig) -> Result { let mut builder = MokaMemoryBackend::builder().capacity(config.l1_capacity); if let Some(ttl) = config.l1_ttl_secs { builder = builder.ttl(Duration::from_secs(ttl)); } if let Some(tti) = config.l1_tti_secs { builder = builder.time_to_idle(Duration::from_secs(tti)); } Ok(builder.build()) } async fn build_l2(config: &CacheConfig) -> Result { let url = config.redis.validate().map_err(CacheError::InvalidConfig)?; let mut builder = RedisBackend::builder() .connection_string(url) .mode(to_oxcache_mode(config.redis.mode())) .pool_size(config.redis.pool_size()) .connection_timeout(config.redis.connection_timeout()); if let Some(db) = config.redis.database { builder = builder.database(db); } Ok(builder.build().await?) } fn to_oxcache_mode(mode: RedisMode) -> OxRedisMode { match mode { RedisMode::Standalone => OxRedisMode::Standalone, RedisMode::Sentinel => OxRedisMode::Sentinel, RedisMode::Cluster => OxRedisMode::Cluster, RedisMode::ValkeyStandalone => OxRedisMode::ValkeyStandalone, } }