1use parking_lot::RwLock;
7use std::collections::HashMap;
8use std::sync::Arc;
9use std::time::{Duration, Instant};
10
11#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
13pub struct CacheEntry {
14 pub status: u16,
16 pub headers: HashMap<String, String>,
18 #[serde(with = "barbacane_plugin_sdk::types::base64_body")]
20 pub body: Option<Vec<u8>>,
21 #[serde(skip_serializing_if = "Option::is_none")]
23 pub metadata: Option<CacheMetadata>,
24}
25
26#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
28pub struct CacheMetadata {
29 pub created_at: u64,
31 pub expires_at: u64,
33 pub ttl_remaining: u64,
35}
36
37struct InternalEntry {
39 entry: CacheEntry,
40 expires_at: Instant,
41 created_at: Instant,
42 _ttl_secs: u64,
43}
44
45#[derive(Debug, Clone, serde::Serialize)]
47pub struct CacheResult {
48 pub hit: bool,
50 #[serde(skip_serializing_if = "Option::is_none")]
52 pub entry: Option<CacheEntry>,
53}
54
55#[derive(Clone)]
57pub struct ResponseCache {
58 entries: Arc<RwLock<HashMap<String, InternalEntry>>>,
60 cleanup_interval: Duration,
62 last_cleanup: Arc<RwLock<Instant>>,
64}
65
66impl Default for ResponseCache {
67 fn default() -> Self {
68 Self::new()
69 }
70}
71
72impl ResponseCache {
73 pub fn new() -> Self {
75 Self {
76 entries: Arc::new(RwLock::new(HashMap::new())),
77 cleanup_interval: Duration::from_secs(60),
78 last_cleanup: Arc::new(RwLock::new(Instant::now())),
79 }
80 }
81
82 pub fn get(&self, key: &str) -> CacheResult {
84 self.maybe_cleanup();
85
86 let entries = self.entries.read();
87 let now = Instant::now();
88
89 if let Some(internal) = entries.get(key) {
90 if internal.expires_at > now {
91 let ttl_remaining = internal.expires_at.saturating_duration_since(now).as_secs();
93 let now_unix = std::time::SystemTime::now()
94 .duration_since(std::time::UNIX_EPOCH)
95 .map(|d| d.as_secs())
96 .unwrap_or(0);
97
98 let mut entry = internal.entry.clone();
99 entry.metadata = Some(CacheMetadata {
100 created_at: now_unix - internal.created_at.elapsed().as_secs(),
101 expires_at: now_unix + ttl_remaining,
102 ttl_remaining,
103 });
104
105 return CacheResult {
106 hit: true,
107 entry: Some(entry),
108 };
109 }
110 }
111
112 CacheResult {
114 hit: false,
115 entry: None,
116 }
117 }
118
119 pub fn set(&self, key: &str, entry: CacheEntry, ttl_secs: u64) {
121 let now = Instant::now();
122 let expires_at = now + Duration::from_secs(ttl_secs);
123
124 let internal = InternalEntry {
125 entry,
126 expires_at,
127 created_at: now,
128 _ttl_secs: ttl_secs,
129 };
130
131 let mut entries = self.entries.write();
132 entries.insert(key.to_string(), internal);
133 }
134
135 pub fn invalidate(&self, key: &str) {
137 let mut entries = self.entries.write();
138 entries.remove(key);
139 }
140
141 pub fn clear(&self) {
143 let mut entries = self.entries.write();
144 entries.clear();
145 }
146
147 fn maybe_cleanup(&self) {
149 let now = Instant::now();
150
151 {
152 let last = self.last_cleanup.read();
153 if now.duration_since(*last) < self.cleanup_interval {
154 return;
155 }
156 }
157
158 if let Some(mut last) = self.last_cleanup.try_write() {
159 if now.duration_since(*last) >= self.cleanup_interval {
160 *last = now;
161
162 if let Some(mut entries) = self.entries.try_write() {
163 entries.retain(|_, v| v.expires_at > now);
164 }
165 }
166 }
167 }
168
169 pub fn stats(&self) -> CacheStats {
171 let entries = self.entries.read();
172 let now = Instant::now();
173 let valid_count = entries.values().filter(|e| e.expires_at > now).count();
174
175 CacheStats {
176 total_entries: entries.len(),
177 valid_entries: valid_count,
178 }
179 }
180}
181
182#[derive(Debug, Clone)]
184pub struct CacheStats {
185 pub total_entries: usize,
187 pub valid_entries: usize,
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[test]
196 fn test_cache_miss() {
197 let cache = ResponseCache::new();
198 let result = cache.get("test-key");
199 assert!(!result.hit);
200 assert!(result.entry.is_none());
201 }
202
203 #[test]
204 fn test_cache_hit() {
205 let cache = ResponseCache::new();
206
207 let entry = CacheEntry {
208 status: 200,
209 headers: HashMap::new(),
210 body: Some(b"test body".to_vec()),
211 metadata: None,
212 };
213
214 cache.set("test-key", entry, 60);
215
216 let result = cache.get("test-key");
217 assert!(result.hit);
218 assert!(result.entry.is_some());
219 let cached = result.entry.unwrap();
220 assert_eq!(cached.status, 200);
221 assert_eq!(cached.body, Some(b"test body".to_vec()));
222 }
223
224 #[test]
225 fn test_cache_invalidate() {
226 let cache = ResponseCache::new();
227
228 let entry = CacheEntry {
229 status: 200,
230 headers: HashMap::new(),
231 body: None,
232 metadata: None,
233 };
234
235 cache.set("test-key", entry, 60);
236 assert!(cache.get("test-key").hit);
237
238 cache.invalidate("test-key");
239 assert!(!cache.get("test-key").hit);
240 }
241
242 #[test]
243 fn test_cache_stats() {
244 let cache = ResponseCache::new();
245
246 let entry = CacheEntry {
247 status: 200,
248 headers: HashMap::new(),
249 body: None,
250 metadata: None,
251 };
252
253 cache.set("key1", entry.clone(), 60);
254 cache.set("key2", entry.clone(), 60);
255 cache.set("key3", entry, 60);
256
257 let stats = cache.stats();
258 assert_eq!(stats.total_entries, 3);
259 assert_eq!(stats.valid_entries, 3);
260 }
261
262 #[test]
263 fn test_cache_binary_body_roundtrip() {
264 let cache = ResponseCache::new();
265
266 let binary_body = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]; let entry = CacheEntry {
268 status: 200,
269 headers: {
270 let mut h = HashMap::new();
271 h.insert("content-type".to_string(), "image/png".to_string());
272 h
273 },
274 body: Some(binary_body.clone()),
275 metadata: None,
276 };
277
278 cache.set("binary-key", entry, 60);
279 let cached = cache.get("binary-key").entry.unwrap();
280 assert_eq!(cached.body, Some(binary_body));
281 }
282
283 #[test]
284 fn test_cache_entry_json_roundtrip() {
285 let entry = CacheEntry {
287 status: 200,
288 headers: HashMap::new(),
289 body: Some(vec![0x00, 0xFF, 0x80]),
290 metadata: None,
291 };
292 let json = serde_json::to_string(&entry).unwrap();
293 let decoded: CacheEntry = serde_json::from_str(&json).unwrap();
294 assert_eq!(decoded.body, entry.body);
295 }
296
297 #[test]
298 fn test_cache_entry_json_none_body() {
299 let entry = CacheEntry {
300 status: 204,
301 headers: HashMap::new(),
302 body: None,
303 metadata: None,
304 };
305 let json = serde_json::to_string(&entry).unwrap();
306 let decoded: CacheEntry = serde_json::from_str(&json).unwrap();
307 assert!(decoded.body.is_none());
308 }
309}