Skip to main content

github_mcp/core/
cache_manager.rs

1// GitHub v3 REST API MCP server — generated by mcpify. Do not hand-edit.
2
3use std::collections::HashMap;
4use std::sync::Mutex;
5use std::time::{Duration, Instant};
6
7struct CacheEntry<T> {
8    value: T,
9    expires_at: Instant,
10}
11
12/// Simple TTL cache backing repeated `search`/`get` lookups.
13pub struct QueryCache<T> {
14    max_size: usize,
15    ttl: Duration,
16    store: Mutex<HashMap<String, CacheEntry<T>>>,
17}
18
19impl<T: Clone> QueryCache<T> {
20    pub fn new(max_size: usize, ttl: Duration) -> Self {
21        Self {
22            max_size,
23            ttl,
24            store: Mutex::new(HashMap::new()),
25        }
26    }
27
28    pub fn get(&self, key: &str) -> Option<T> {
29        let mut store = self.store.lock().unwrap();
30        let entry = store.get(key)?;
31        if Instant::now() > entry.expires_at {
32            store.remove(key);
33            return None;
34        }
35        Some(entry.value.clone())
36    }
37
38    pub fn set(&self, key: String, value: T) {
39        let mut store = self.store.lock().unwrap();
40        if store.len() >= self.max_size
41            && !store.contains_key(&key)
42            && let Some(oldest_key) = store.keys().next().cloned()
43        {
44            store.remove(&oldest_key);
45        }
46        store.insert(
47            key,
48            CacheEntry {
49                value,
50                expires_at: Instant::now() + self.ttl,
51            },
52        );
53    }
54
55    pub fn clear(&self) {
56        self.store.lock().unwrap().clear();
57    }
58}
59
60impl<T: Clone> Default for QueryCache<T> {
61    fn default() -> Self {
62        Self::new(500, Duration::from_secs(60))
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    #[test]
71    fn returns_a_previously_set_value() {
72        let cache: QueryCache<String> = QueryCache::new(10, Duration::from_secs(60));
73        cache.set("key".to_string(), "value".to_string());
74        assert_eq!(cache.get("key"), Some("value".to_string()));
75    }
76
77    #[test]
78    fn expires_entries_past_their_ttl() {
79        let cache: QueryCache<String> = QueryCache::new(10, Duration::from_millis(1));
80        cache.set("key".to_string(), "value".to_string());
81        std::thread::sleep(Duration::from_millis(10));
82        assert_eq!(cache.get("key"), None);
83    }
84
85    #[test]
86    fn evicts_when_full() {
87        let cache: QueryCache<String> = QueryCache::new(1, Duration::from_secs(60));
88        cache.set("a".to_string(), "1".to_string());
89        cache.set("b".to_string(), "2".to_string());
90        // Exactly one of the two entries should remain — which one is
91        // unspecified (no LRU ordering, mirrors the TS `QueryCache`'s
92        // `Map` insertion-order eviction only incidentally, not by
93        // contract), but the store must not have grown past `max_size`.
94        let remaining = [cache.get("a"), cache.get("b")]
95            .into_iter()
96            .flatten()
97            .count();
98        assert_eq!(remaining, 1);
99    }
100}