Skip to main content

flag_rs/
completion_cache.rs

1//! Caching system for expensive completion operations
2//!
3//! This module provides a time-based cache for completion results to improve
4//! performance when users repeatedly request completions for the same context.
5
6use crate::completion::CompletionResult;
7use std::collections::HashMap;
8use std::sync::{Arc, Mutex};
9use std::time::{Duration, Instant};
10
11/// A cached completion entry with timestamp
12#[derive(Clone)]
13struct CacheEntry {
14    result: CompletionResult,
15    timestamp: Instant,
16}
17
18/// A thread-safe cache for completion results
19///
20/// The cache automatically expires entries after a configurable duration
21/// to ensure that completions remain fresh while still providing performance benefits.
22pub struct CompletionCache {
23    cache: Arc<Mutex<HashMap<String, CacheEntry>>>,
24    ttl: Duration,
25}
26
27impl CompletionCache {
28    /// Creates a new completion cache with the specified time-to-live
29    ///
30    /// # Arguments
31    ///
32    /// * `ttl` - How long cached entries should remain valid
33    pub fn new(ttl: Duration) -> Self {
34        Self {
35            cache: Arc::new(Mutex::new(HashMap::new())),
36            ttl,
37        }
38    }
39
40    /// Creates a new completion cache with a default TTL of 5 seconds
41    pub fn with_default_ttl() -> Self {
42        Self::new(Duration::from_secs(5))
43    }
44
45    /// Generates a cache key from completion context
46    ///
47    /// The key includes the command path and current prefix to ensure
48    /// we only return cached results for identical contexts.
49    pub fn make_key(
50        command_path: &[String],
51        prefix: &str,
52        flags: &HashMap<String, String>,
53    ) -> String {
54        let mut parts = vec![];
55
56        // Include command path
57        parts.extend(command_path.iter().cloned());
58
59        // Include the prefix being completed
60        parts.push(format!("__prefix:{prefix}"));
61
62        // Include relevant flags that might affect completion
63        let mut flag_parts: Vec<String> = flags.iter().map(|(k, v)| format!("{k}={v}")).collect();
64        flag_parts.sort(); // Ensure consistent ordering
65        parts.extend(flag_parts);
66
67        parts.join(":")
68    }
69
70    /// Attempts to get a cached completion result
71    ///
72    /// Returns `Some(CompletionResult)` if a valid cached entry exists,
73    /// or `None` if the entry doesn't exist or has expired.
74    pub fn get(&self, key: &str) -> Option<CompletionResult> {
75        let mut cache = self.cache.lock().ok()?;
76
77        if let Some(entry) = cache.get(key) {
78            if entry.timestamp.elapsed() < self.ttl {
79                return Some(entry.result.clone());
80            }
81            // Entry has expired, remove it
82            cache.remove(key);
83        }
84
85        None
86    }
87
88    /// Stores a completion result in the cache
89    ///
90    /// # Arguments
91    ///
92    /// * `key` - The cache key
93    /// * `result` - The completion result to cache
94    pub fn put(&self, key: String, result: CompletionResult) {
95        if let Ok(mut cache) = self.cache.lock() {
96            cache.insert(
97                key,
98                CacheEntry {
99                    result,
100                    timestamp: Instant::now(),
101                },
102            );
103
104            // Opportunistically clean up expired entries
105            self.cleanup_expired(&mut cache);
106        }
107    }
108
109    /// Removes expired entries from the cache
110    fn cleanup_expired(&self, cache: &mut HashMap<String, CacheEntry>) {
111        let now = Instant::now();
112        cache.retain(|_, entry| now.duration_since(entry.timestamp) < self.ttl);
113    }
114
115    /// Clears all cached entries
116    pub fn clear(&self) {
117        if let Ok(mut cache) = self.cache.lock() {
118            cache.clear();
119        }
120    }
121
122    /// Returns the number of cached entries
123    pub fn size(&self) -> usize {
124        self.cache.lock().map(|c| c.len()).unwrap_or(0)
125    }
126}
127
128impl Default for CompletionCache {
129    fn default() -> Self {
130        Self::with_default_ttl()
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use crate::completion::CompletionResult;
138
139    #[test]
140    fn test_cache_basic_operations() {
141        let cache = CompletionCache::new(Duration::from_secs(1));
142        let key = "test:key";
143        let result = CompletionResult::new().add("item1").add("item2");
144
145        // Test cache miss
146        assert!(cache.get(key).is_none());
147
148        // Test cache put and hit
149        cache.put(key.to_string(), result.clone());
150        let cached = cache.get(key).unwrap();
151        assert_eq!(cached.values, result.values);
152
153        // Test expiration
154        std::thread::sleep(Duration::from_millis(1100));
155        assert!(cache.get(key).is_none());
156    }
157
158    #[test]
159    fn test_cache_key_generation() {
160        let mut flags = HashMap::new();
161        flags.insert("namespace".to_string(), "default".to_string());
162        flags.insert("verbose".to_string(), "true".to_string());
163
164        let key1 =
165            CompletionCache::make_key(&["kubectl".to_string(), "get".to_string()], "po", &flags);
166        let key2 =
167            CompletionCache::make_key(&["kubectl".to_string(), "get".to_string()], "po", &flags);
168        assert_eq!(key1, key2);
169
170        // Different prefix should generate different key
171        let key3 =
172            CompletionCache::make_key(&["kubectl".to_string(), "get".to_string()], "pod", &flags);
173        assert_ne!(key1, key3);
174
175        // Different flags should generate different key
176        flags.insert("all-namespaces".to_string(), "true".to_string());
177        let key4 =
178            CompletionCache::make_key(&["kubectl".to_string(), "get".to_string()], "po", &flags);
179        assert_ne!(key1, key4);
180    }
181
182    #[test]
183    fn test_cache_cleanup() {
184        let cache = CompletionCache::new(Duration::from_millis(100));
185
186        // Add multiple entries
187        for i in 0..5 {
188            let key = format!("key{i}");
189            let result = CompletionResult::new().add(format!("item{i}"));
190            cache.put(key, result);
191        }
192
193        assert_eq!(cache.size(), 5);
194
195        // Wait for expiration
196        std::thread::sleep(Duration::from_millis(150));
197
198        // Trigger cleanup by adding a new entry
199        cache.put("new".to_string(), CompletionResult::new());
200
201        // Only the new entry should remain
202        assert_eq!(cache.size(), 1);
203    }
204}