arch_toolkit/cache/mod.rs
1//! Caching layer for arch-toolkit operations.
2//!
3//! Provides optional in-memory and disk caching for AUR operations to reduce
4//! network requests and improve performance.
5
6#[cfg(feature = "aur")]
7mod config;
8#[cfg(feature = "cache-disk")]
9#[cfg(feature = "aur")]
10mod disk;
11#[cfg(feature = "aur")]
12mod memory;
13
14#[cfg(feature = "aur")]
15pub use config::{CacheConfig, CacheConfigBuilder};
16
17use serde::{Deserialize, Serialize};
18use std::time::Duration;
19
20/// What: Trait for cache implementations.
21///
22/// Inputs:
23/// - `K`: Cache key type (must be `AsRef<str>`)
24/// - `V`: Value type (must be `Clone + Serialize + Deserialize`)
25///
26/// Output:
27/// - Various methods return `Option<V>` for cache hits or `Result` for operations
28///
29/// Details:
30/// - Provides abstract interface for cache operations
31/// - Supports get, set, invalidate, and clear operations
32/// - Generic over value type to support different cached data
33#[cfg(feature = "aur")]
34pub trait Cache<K, V>
35where
36 K: AsRef<str>,
37 V: Clone + Serialize + for<'de> Deserialize<'de>,
38{
39 /// What: Get a value from the cache.
40 ///
41 /// Inputs:
42 /// - `key`: Cache key to look up
43 ///
44 /// Output:
45 /// - `Option<V>` containing cached value if found and not expired, `None` otherwise
46 ///
47 /// Details:
48 /// - Returns `None` if key not found or entry has expired
49 /// - Should check TTL before returning value
50 fn get(&self, key: &K) -> Option<V>;
51
52 /// What: Store a value in the cache.
53 ///
54 /// Inputs:
55 /// - `key`: Cache key
56 /// - `value`: Value to cache
57 /// - `ttl`: Time-to-live duration
58 ///
59 /// Output:
60 /// - `Result<(), CacheError>` indicating success or failure
61 ///
62 /// Details:
63 /// - Stores value with specified TTL
64 /// - May evict entries if cache is full (LRU)
65 /// - Should not block the main operation
66 ///
67 /// # Errors
68 /// - Returns `Err(CacheError::Serialization)` if value serialization fails
69 /// - Returns `Err(CacheError::Io)` if disk cache write fails (disk cache only)
70 fn set(&self, key: &K, value: &V, ttl: Duration) -> Result<(), CacheError>;
71
72 /// What: Invalidate a specific cache entry.
73 ///
74 /// Inputs:
75 /// - `key`: Cache key to invalidate
76 ///
77 /// Output:
78 /// - `Result<(), CacheError>` indicating success or failure
79 ///
80 /// Details:
81 /// - Removes the entry from cache
82 /// - Safe to call if key doesn't exist
83 ///
84 /// # Errors
85 /// - Returns `Err(CacheError::Io)` if disk cache file removal fails (disk cache only)
86 fn invalidate(&self, key: &K) -> Result<(), CacheError>;
87
88 /// What: Clear all entries from the cache.
89 ///
90 /// Inputs: None
91 ///
92 /// Output:
93 /// - `Result<(), CacheError>` indicating success or failure
94 ///
95 /// Details:
96 /// - Removes all entries from cache
97 /// - Useful for cache invalidation operations
98 ///
99 /// # Errors
100 /// - Returns `Err(CacheError::Io)` if disk cache cleanup fails (disk cache only)
101 fn clear(&self) -> Result<(), CacheError>;
102}
103
104/// What: Error type for cache operations.
105///
106/// Inputs: None
107///
108/// Output:
109/// - `CacheError` enum with various error variants
110///
111/// Details:
112/// - Represents errors that can occur during cache operations
113/// - Includes serialization, I/O, and other errors
114#[cfg(feature = "aur")]
115#[derive(Debug, thiserror::Error)]
116pub enum CacheError {
117 /// Serialization error when storing cache entry.
118 #[error("Cache serialization error: {0}")]
119 Serialization(String),
120
121 /// Deserialization error when loading cache entry.
122 #[error("Cache deserialization error: {0}")]
123 Deserialization(String),
124
125 /// I/O error when accessing disk cache.
126 #[error("Cache I/O error: {0}")]
127 Io(#[from] std::io::Error),
128
129 /// Other cache-related errors.
130 #[error("Cache error: {0}")]
131 Other(String),
132}
133
134/// What: Generate cache key for search operation.
135///
136/// Inputs:
137/// - `query`: Search query string
138///
139/// Output:
140/// - `String` containing normalized cache key
141///
142/// Details:
143/// - Normalizes query by trimming whitespace
144/// - Format: `"search:{query}"`
145#[cfg(feature = "aur")]
146#[must_use]
147pub fn cache_key_search(query: &str) -> String {
148 let trimmed = query.trim();
149 format!("search:{trimmed}")
150}
151
152/// What: Generate cache key for info operation.
153///
154/// Inputs:
155/// - `names`: Slice of package names
156///
157/// Output:
158/// - `String` containing normalized cache key
159///
160/// Details:
161/// - Sorts package names for consistent keys
162/// - Format: `"info:{sorted_names}"`
163#[cfg(feature = "aur")]
164#[must_use]
165pub fn cache_key_info(names: &[&str]) -> String {
166 let mut sorted = names.to_vec();
167 sorted.sort_unstable();
168 format!("info:{}", sorted.join(","))
169}
170
171/// What: Generate cache key for comments operation.
172///
173/// Inputs:
174/// - `pkgname`: Package name
175///
176/// Output:
177/// - `String` containing normalized cache key
178///
179/// Details:
180/// - Format: `"comments:{pkgname}"`
181#[cfg(feature = "aur")]
182#[must_use]
183pub fn cache_key_comments(pkgname: &str) -> String {
184 format!("comments:{pkgname}")
185}
186
187/// What: Generate cache key for pkgbuild operation.
188///
189/// Inputs:
190/// - `package`: Package name
191///
192/// Output:
193/// - `String` containing normalized cache key
194///
195/// Details:
196/// - Format: `"pkgbuild:{package}"`
197#[cfg(feature = "aur")]
198#[must_use]
199pub fn cache_key_pkgbuild(package: &str) -> String {
200 format!("pkgbuild:{package}")
201}
202
203#[cfg(feature = "aur")]
204use memory::MemoryCache;
205
206#[cfg(feature = "cache-disk")]
207#[cfg(feature = "aur")]
208use disk::DiskCache;
209
210/// What: Combined cache wrapper that uses memory and optionally disk cache.
211///
212/// Inputs: None (created via `CacheWrapper::new()`)
213///
214/// Output:
215/// - `CacheWrapper` instance ready for use
216///
217/// Details:
218/// - Always uses in-memory LRU cache
219/// - Optionally uses disk cache if enabled in config
220/// - Checks memory cache first, then disk cache
221/// - Writes to both caches when storing
222#[cfg(feature = "aur")]
223#[derive(Debug)]
224pub struct CacheWrapper {
225 /// In-memory LRU cache.
226 memory: MemoryCache,
227 /// Optional disk cache.
228 #[cfg(feature = "cache-disk")]
229 disk: Option<DiskCache>,
230}
231
232#[cfg(feature = "aur")]
233impl CacheWrapper {
234 /// What: Create a new cache wrapper with memory and optional disk cache.
235 ///
236 /// Inputs:
237 /// - `config`: Cache configuration
238 ///
239 /// Output:
240 /// - `Result<CacheWrapper>` with configured caches, or error if initialization fails
241 ///
242 /// Details:
243 /// - Always creates memory cache
244 /// - Creates disk cache if enabled in config
245 /// - Returns error if disk cache creation fails
246 ///
247 /// # Errors
248 /// - Returns `Err(CacheError::Io)` if disk cache directory creation fails
249 pub fn new(config: &CacheConfig) -> Result<Self, CacheError> {
250 let memory = MemoryCache::new(config.memory_cache_size);
251 #[cfg(feature = "cache-disk")]
252 {
253 let disk = if config.enable_disk_cache {
254 Some(DiskCache::new().map_err(CacheError::Io)?)
255 } else {
256 None
257 };
258 Ok(Self { memory, disk })
259 }
260 #[cfg(not(feature = "cache-disk"))]
261 {
262 Ok(Self { memory })
263 }
264 }
265
266 /// What: Get a value from cache (checks memory first, then disk).
267 ///
268 /// Inputs:
269 /// - `key`: Cache key
270 ///
271 /// Output:
272 /// - `Option<V>` containing cached value if found, `None` otherwise
273 ///
274 /// Details:
275 /// - Checks memory cache first (fastest)
276 /// - Falls back to disk cache if memory miss
277 /// - Promotes disk cache hits to memory cache
278 #[must_use]
279 pub fn get<V>(&self, key: &str) -> Option<V>
280 where
281 V: Clone + Serialize + for<'de> Deserialize<'de>,
282 {
283 let key_str = key.to_string();
284 // Try memory cache first
285 if let Some(value) = <MemoryCache as Cache<String, V>>::get(&self.memory, &key_str) {
286 return Some(value);
287 }
288
289 // Try disk cache if available
290 #[cfg(feature = "cache-disk")]
291 if let Some(ref disk) = self.disk
292 && let Some(value) = <DiskCache as Cache<String, V>>::get(disk, &key_str)
293 {
294 // Promote to memory cache
295 let _ = <MemoryCache as Cache<String, V>>::set(
296 &self.memory,
297 &key_str,
298 &value,
299 Duration::from_secs(300),
300 );
301 return Some(value);
302 }
303
304 None
305 }
306
307 /// What: Store a value in cache (writes to both memory and disk if enabled).
308 ///
309 /// Inputs:
310 /// - `key`: Cache key
311 /// - `value`: Value to cache
312 /// - `ttl`: Time-to-live duration
313 ///
314 /// Output:
315 /// - `Result<(), CacheError>` indicating success or failure
316 ///
317 /// Details:
318 /// - Writes to memory cache (always)
319 /// - Writes to disk cache if enabled
320 /// - Errors in disk cache don't prevent memory cache write
321 ///
322 /// # Errors
323 /// - Returns `Err(CacheError::Serialization)` if value serialization fails
324 pub fn set<V>(&self, key: &str, value: &V, ttl: Duration) -> Result<(), CacheError>
325 where
326 V: Clone + Serialize + for<'de> Deserialize<'de>,
327 {
328 let key_str = key.to_string();
329 // Always write to memory cache
330 <MemoryCache as Cache<String, V>>::set(&self.memory, &key_str, value, ttl)?;
331
332 // Write to disk cache if enabled
333 #[cfg(feature = "cache-disk")]
334 if let Some(ref disk) = self.disk {
335 let _ = <DiskCache as Cache<String, V>>::set(disk, &key_str, value, ttl);
336 }
337
338 Ok(())
339 }
340
341 /// What: Invalidate a cache entry (removes from both memory and disk).
342 ///
343 /// Inputs:
344 /// - `key`: Cache key to invalidate
345 ///
346 /// Output:
347 /// - `Result<(), CacheError>` indicating success or failure
348 ///
349 /// Details:
350 /// - Removes from memory cache
351 /// - Removes from disk cache if enabled
352 ///
353 /// # Errors
354 /// - Returns `Err(CacheError::Io)` if disk cache file removal fails (disk cache only)
355 pub fn invalidate(&self, key: &str) -> Result<(), CacheError> {
356 let key_str = key.to_string();
357 <MemoryCache as Cache<String, ()>>::invalidate(&self.memory, &key_str)?;
358 #[cfg(feature = "cache-disk")]
359 if let Some(ref disk) = self.disk {
360 let _ = <DiskCache as Cache<String, ()>>::invalidate(disk, &key_str);
361 }
362 Ok(())
363 }
364
365 /// What: Clear all cache entries (both memory and disk).
366 ///
367 /// Inputs: None
368 ///
369 /// Output:
370 /// - `Result<(), CacheError>` indicating success or failure
371 ///
372 /// Details:
373 /// - Clears memory cache
374 /// - Clears disk cache if enabled
375 ///
376 /// # Errors
377 /// - Returns `Err(CacheError::Io)` if disk cache cleanup fails (disk cache only)
378 pub fn clear(&self) -> Result<(), CacheError> {
379 <MemoryCache as Cache<String, ()>>::clear(&self.memory)?;
380 #[cfg(feature = "cache-disk")]
381 if let Some(ref disk) = self.disk {
382 let _ = <DiskCache as Cache<String, ()>>::clear(disk);
383 }
384 Ok(())
385 }
386}