1use crate::{Error, Result};
2use std::collections::HashMap;
3use std::sync::Arc;
4use std::time::{Duration, Instant};
5use tokio::sync::RwLock;
6use tracing::{debug, instrument};
7
8#[derive(Debug, Clone)]
10pub struct CacheEntry<T> {
11 pub value: T,
12 pub expires_at: Instant,
13 pub created_at: Instant,
14}
15
16impl<T> CacheEntry<T> {
17 pub fn new(value: T, ttl: Duration) -> Self {
18 let now = Instant::now();
19 Self {
20 value,
21 expires_at: now + ttl,
22 created_at: now,
23 }
24 }
25
26 pub fn is_expired(&self) -> bool {
27 Instant::now() > self.expires_at
28 }
29
30 pub fn age(&self) -> Duration {
31 Instant::now().duration_since(self.created_at)
32 }
33}
34
35#[derive(Debug)]
37pub struct MemoryCache<T> {
38 store: Arc<RwLock<HashMap<String, CacheEntry<T>>>>,
39 default_ttl: Duration,
40 max_entries: usize,
41}
42
43impl<T: Clone + Send + Sync + 'static> MemoryCache<T> {
44 pub fn new(default_ttl: Duration, max_entries: usize) -> Self {
45 Self {
46 store: Arc::new(RwLock::new(HashMap::new())),
47 default_ttl,
48 max_entries,
49 }
50 }
51
52 #[instrument(skip(self), fields(key = %key))]
54 pub async fn get(&self, key: &str) -> Option<T> {
55 let mut store = self.store.write().await;
56
57 if let Some(entry) = store.get(key) {
58 if entry.is_expired() {
59 debug!("Cache entry expired, removing");
60 store.remove(key);
61 None
62 } else {
63 debug!(age_ms = %entry.age().as_millis(), "Cache hit");
64 Some(entry.value.clone())
65 }
66 } else {
67 debug!("Cache miss");
68 None
69 }
70 }
71
72 #[instrument(skip(self, value), fields(key = %key))]
74 pub async fn set(&self, key: String, value: T) {
75 self.set_with_ttl(key, value, self.default_ttl).await;
76 }
77
78 #[instrument(skip(self, value), fields(key = %key, ttl_secs = %ttl.as_secs()))]
80 pub async fn set_with_ttl(&self, key: String, value: T, ttl: Duration) {
81 let mut store = self.store.write().await;
82
83 if store.len() >= self.max_entries {
85 self.evict_expired_entries(&mut store).await;
86
87 if store.len() >= self.max_entries {
89 if let Some(oldest_key) = self.find_oldest_entry(&store).await {
90 debug!(evicted_key = %oldest_key, "Evicting oldest cache entry");
91 store.remove(&oldest_key);
92 }
93 }
94 }
95
96 let entry = CacheEntry::new(value, ttl);
97 store.insert(key, entry);
98 debug!("Value cached successfully");
99 }
100
101 #[instrument(skip(self), fields(key = %key))]
103 pub async fn remove(&self, key: &str) -> Option<T> {
104 let mut store = self.store.write().await;
105 store.remove(key).map(|entry| {
106 debug!("Cache entry removed");
107 entry.value
108 })
109 }
110
111 #[instrument(skip(self))]
113 pub async fn clear(&self) {
114 let mut store = self.store.write().await;
115 let count = store.len();
116 store.clear();
117 debug!(cleared_entries = %count, "Cache cleared");
118 }
119
120 pub async fn stats(&self) -> CacheStats {
122 let store = self.store.read().await;
123 let total_entries = store.len();
124 let expired_entries = store.values().filter(|entry| entry.is_expired()).count();
125
126 CacheStats {
127 total_entries,
128 expired_entries,
129 active_entries: total_entries - expired_entries,
130 max_entries: self.max_entries,
131 }
132 }
133
134 async fn evict_expired_entries(&self, store: &mut HashMap<String, CacheEntry<T>>) {
136 let expired_keys: Vec<String> = store
137 .iter()
138 .filter_map(|(key, entry)| {
139 if entry.is_expired() {
140 Some(key.clone())
141 } else {
142 None
143 }
144 })
145 .collect();
146
147 let expired_count = expired_keys.len();
148
149 for key in expired_keys {
150 store.remove(&key);
151 }
152
153 if expired_count > 0 {
154 debug!(expired_count = %expired_count, "Evicted expired cache entries");
155 }
156 }
157
158 async fn find_oldest_entry(&self, store: &HashMap<String, CacheEntry<T>>) -> Option<String> {
160 store
161 .iter()
162 .min_by_key(|(_, entry)| entry.created_at)
163 .map(|(key, _)| key.clone())
164 }
165}
166
167#[derive(Debug, Clone)]
169pub struct CacheStats {
170 pub total_entries: usize,
171 pub expired_entries: usize,
172 pub active_entries: usize,
173 pub max_entries: usize,
174}
175
176#[derive(Debug, Clone)]
178pub struct CacheConfig {
179 pub balance_ttl: Duration,
181 pub transaction_ttl: Duration,
183 pub nft_metadata_ttl: Duration,
185 pub nft_collection_ttl: Duration,
187 pub max_entries: usize,
189 pub enabled: bool,
191}
192
193impl Default for CacheConfig {
194 fn default() -> Self {
195 Self {
196 balance_ttl: Duration::from_secs(30), transaction_ttl: Duration::from_secs(300), nft_metadata_ttl: Duration::from_secs(3600), nft_collection_ttl: Duration::from_secs(3600), max_entries: 1000,
201 enabled: true,
202 }
203 }
204}
205
206pub fn cache_key_for_balances(chain_name: &str, address: &str, options: &str) -> String {
208 format!("balances:{}:{}:{}", chain_name, address, options)
209}
210
211pub fn cache_key_for_transactions(chain_name: &str, address: &str, options: &str) -> String {
212 format!("transactions:{}:{}:{}", chain_name, address, options)
213}
214
215pub fn cache_key_for_transaction(chain_name: &str, tx_hash: &str) -> String {
216 format!("transaction:{}:{}", chain_name, tx_hash)
217}
218
219pub fn cache_key_for_nfts(chain_name: &str, address: &str, options: &str) -> String {
220 format!("nfts:{}:{}:{}", chain_name, address, options)
221}
222
223pub fn cache_key_for_nft_metadata(chain_name: &str, address: &str, token_id: &str) -> String {
224 format!("nft_metadata:{}:{}:{}", chain_name, address, token_id)
225}