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}