1use std::collections::HashMap;
9use std::hash::{DefaultHasher, Hash, Hasher};
10use std::time::{Duration, Instant};
11
12use crate::lobe::LobeOutput;
13
14pub struct LobeCache {
36 entries: HashMap<(String, u64), CacheEntry>,
37 ttl: Duration,
38}
39
40struct CacheEntry {
41 output: LobeOutput,
42 input: String, created_at: Instant,
44}
45
46impl LobeCache {
47 pub fn new(ttl: Duration) -> Self {
49 Self {
50 entries: HashMap::new(),
51 ttl,
52 }
53 }
54
55 pub fn get(&self, lobe_name: &str, input: &str) -> Option<&LobeOutput> {
57 let key = (lobe_name.to_string(), hash_input(input));
58 self.entries.get(&key).and_then(|entry| {
59 if entry.input != input {
61 return None;
62 }
63 if entry.created_at.elapsed() < self.ttl {
64 Some(&entry.output)
65 } else {
66 None
67 }
68 })
69 }
70
71 pub fn put(&mut self, lobe_name: &str, input: &str, output: LobeOutput) {
73 let key = (lobe_name.to_string(), hash_input(input));
74 self.entries.insert(
75 key,
76 CacheEntry {
77 output,
78 input: input.to_string(),
79 created_at: Instant::now(),
80 },
81 );
82 }
83
84 pub fn evict_expired(&mut self) {
86 self.entries
87 .retain(|_, entry| entry.created_at.elapsed() < self.ttl);
88 }
89
90 pub fn clear(&mut self) {
92 self.entries.clear();
93 }
94
95 pub fn len(&self) -> usize {
97 self.entries.len()
98 }
99
100 pub fn is_empty(&self) -> bool {
102 self.entries.is_empty()
103 }
104}
105
106fn hash_input(input: &str) -> u64 {
108 let mut hasher = DefaultHasher::new();
109 input.hash(&mut hasher);
110 hasher.finish()
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 #[test]
118 fn test_cache_hit() {
119 let mut cache = LobeCache::new(Duration::from_secs(60));
120 let output = LobeOutput::new("analysis result", 0.9);
121
122 cache.put("analyst", "test input", output);
123
124 let cached = cache.get("analyst", "test input");
125 assert!(cached.is_some());
126 assert_eq!(cached.unwrap().content, "analysis result");
127 }
128
129 #[test]
130 fn test_cache_miss_different_input() {
131 let mut cache = LobeCache::new(Duration::from_secs(60));
132 cache.put("analyst", "input A", LobeOutput::new("result A", 0.9));
133
134 assert!(cache.get("analyst", "input B").is_none());
135 }
136
137 #[test]
138 fn test_cache_miss_different_lobe() {
139 let mut cache = LobeCache::new(Duration::from_secs(60));
140 cache.put("analyst", "input", LobeOutput::new("result", 0.9));
141
142 assert!(cache.get("critic", "input").is_none());
143 }
144
145 #[test]
146 fn test_cache_expiry() {
147 let mut cache = LobeCache::new(Duration::ZERO);
149 cache.put("analyst", "input", LobeOutput::new("result", 0.9));
150
151 assert!(cache.get("analyst", "input").is_none());
153 }
154
155 #[test]
156 fn test_cache_overwrite() {
157 let mut cache = LobeCache::new(Duration::from_secs(60));
158 cache.put("analyst", "input", LobeOutput::new("old", 0.5));
159 cache.put("analyst", "input", LobeOutput::new("new", 0.9));
160
161 let cached = cache.get("analyst", "input").unwrap();
162 assert_eq!(cached.content, "new");
163 assert_eq!(cache.len(), 1);
164 }
165
166 #[test]
167 fn test_evict_expired() {
168 let mut cache = LobeCache::new(Duration::ZERO);
169 cache.put("a", "input", LobeOutput::new("a", 0.5));
170 cache.put("b", "input", LobeOutput::new("b", 0.5));
171 assert_eq!(cache.len(), 2);
172
173 cache.evict_expired();
174 assert!(cache.is_empty());
175 }
176
177 #[test]
178 fn test_clear() {
179 let mut cache = LobeCache::new(Duration::from_secs(60));
180 cache.put("a", "input", LobeOutput::new("a", 0.5));
181 cache.put("b", "input", LobeOutput::new("b", 0.5));
182
183 cache.clear();
184 assert!(cache.is_empty());
185 }
186}