steam_client/cache/
persona.rs1use std::{
30 sync::RwLock,
31 time::{Duration, Instant},
32};
33
34use rustc_hash::FxHashMap;
35
36use steamid::SteamID;
37
38use crate::client::UserPersona;
39
40#[derive(Debug, Clone)]
42pub struct PersonaCacheConfig {
43 pub ttl: Duration,
50
51 pub max_size: usize,
58}
59
60impl Default for PersonaCacheConfig {
61 fn default() -> Self {
62 Self {
63 ttl: Duration::from_secs(300), max_size: 1000,
65 }
66 }
67}
68
69#[derive(Debug, Clone)]
71pub struct CachedPersona {
72 pub data: UserPersona,
74 pub cached_at: Instant,
76}
77
78impl CachedPersona {
79 pub fn new(data: UserPersona) -> Self {
81 Self { data, cached_at: Instant::now() }
82 }
83
84 pub fn is_expired(&self, ttl: Duration) -> bool {
86 self.cached_at.elapsed() > ttl
87 }
88}
89
90#[derive(Debug)]
95pub struct PersonaCache {
96 cache: RwLock<FxHashMap<SteamID, CachedPersona>>,
98 config: PersonaCacheConfig,
100}
101
102impl PersonaCache {
103 pub fn new(config: PersonaCacheConfig) -> Self {
105 Self { cache: RwLock::new(FxHashMap::default()), config }
106 }
107
108 pub fn get(&self, steam_id: &SteamID) -> Option<UserPersona> {
112 let cache = self.cache.read().ok()?;
113 cache.get(steam_id).and_then(|entry| {
114 if !entry.is_expired(self.config.ttl) {
115 Some(entry.data.clone())
116 } else {
117 None }
119 })
120 }
121
122 pub fn insert(&self, steam_id: SteamID, data: UserPersona) {
126 if let Ok(mut cache) = self.cache.write() {
127 if cache.len() >= self.config.max_size {
129 self.evict_expired_internal(&mut cache);
130 }
131
132 if cache.len() >= self.config.max_size {
134 self.evict_oldest_internal(&mut cache);
135 }
136
137 cache.insert(steam_id, CachedPersona::new(data));
138 }
139 }
140
141 pub fn get_many(&self, steam_ids: &[SteamID]) -> (Vec<UserPersona>, Vec<SteamID>) {
149 let mut found = Vec::new();
150 let mut missing = Vec::new();
151
152 if let Ok(cache) = self.cache.read() {
153 for id in steam_ids {
154 if let Some(entry) = cache.get(id) {
155 if !entry.is_expired(self.config.ttl) {
156 found.push(entry.data.clone());
157 } else {
158 missing.push(*id);
159 }
160 } else {
161 missing.push(*id);
162 }
163 }
164 } else {
165 missing.extend(steam_ids.iter().copied());
167 }
168
169 (found, missing)
170 }
171
172 pub fn clear(&self) {
174 if let Ok(mut cache) = self.cache.write() {
175 cache.clear();
176 }
177 }
178
179 pub fn invalidate(&self, steam_id: &SteamID) {
184 if let Ok(mut cache) = self.cache.write() {
185 cache.remove(steam_id);
186 }
187 }
188
189 pub fn len(&self) -> usize {
191 self.cache.read().map(|c| c.len()).unwrap_or(0)
192 }
193
194 pub fn is_empty(&self) -> bool {
196 self.len() == 0
197 }
198
199 pub fn config(&self) -> &PersonaCacheConfig {
201 &self.config
202 }
203
204 pub fn evict_expired(&self) {
208 if let Ok(mut cache) = self.cache.write() {
209 self.evict_expired_internal(&mut cache);
210 }
211 }
212
213 fn evict_expired_internal(&self, cache: &mut FxHashMap<SteamID, CachedPersona>) {
215 let ttl = self.config.ttl;
216 cache.retain(|_, entry| !entry.is_expired(ttl));
217 }
218
219 fn evict_oldest_internal(&self, cache: &mut FxHashMap<SteamID, CachedPersona>) {
221 if let Some((oldest_id, _)) = cache.iter().min_by_key(|(_, entry)| entry.cached_at).map(|(id, entry)| (*id, entry.cached_at)) {
222 cache.remove(&oldest_id);
223 }
224 }
225}
226
227impl Default for PersonaCache {
228 fn default() -> Self {
229 Self::new(PersonaCacheConfig::default())
230 }
231}
232
233impl Clone for PersonaCache {
234 fn clone(&self) -> Self {
235 let cache_data = self.cache.read().map(|c| c.clone()).unwrap_or_default();
236
237 Self { cache: RwLock::new(cache_data), config: self.config.clone() }
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use steam_enums::EPersonaState;
244
245 use super::*;
246
247 fn create_test_persona(steam_id: SteamID, name: &str) -> UserPersona {
248 UserPersona { steam_id, player_name: name.to_string(), persona_state: EPersonaState::Online, ..Default::default() }
249 }
250
251 #[test]
252 fn test_cache_insert_and_get() {
253 let cache = PersonaCache::default();
254 let steam_id = SteamID::from_steam_id64(76561198000000001);
255 let persona = create_test_persona(steam_id, "TestUser");
256
257 cache.insert(steam_id, persona.clone());
258
259 let retrieved = cache.get(&steam_id);
260 assert!(retrieved.is_some());
261 assert_eq!(retrieved.unwrap().player_name, "TestUser");
262 }
263
264 #[test]
265 fn test_cache_miss() {
266 let cache = PersonaCache::default();
267 let steam_id = SteamID::from_steam_id64(76561198000000001);
268
269 let retrieved = cache.get(&steam_id);
270 assert!(retrieved.is_none());
271 }
272
273 #[test]
274 fn test_cache_invalidate() {
275 let cache = PersonaCache::default();
276 let steam_id = SteamID::from_steam_id64(76561198000000001);
277 let persona = create_test_persona(steam_id, "TestUser");
278
279 cache.insert(steam_id, persona);
280 assert!(cache.get(&steam_id).is_some());
281
282 cache.invalidate(&steam_id);
283 assert!(cache.get(&steam_id).is_none());
284 }
285
286 #[test]
287 fn test_cache_clear() {
288 let cache = PersonaCache::default();
289 let steam_id1 = SteamID::from_steam_id64(76561198000000001);
290 let steam_id2 = SteamID::from_steam_id64(76561198000000002);
291
292 cache.insert(steam_id1, create_test_persona(steam_id1, "User1"));
293 cache.insert(steam_id2, create_test_persona(steam_id2, "User2"));
294 assert_eq!(cache.len(), 2);
295
296 cache.clear();
297 assert!(cache.is_empty());
298 }
299
300 #[test]
301 fn test_cache_get_many_partial() {
302 let cache = PersonaCache::default();
303 let id1 = SteamID::from_steam_id64(76561198000000001);
304 let id2 = SteamID::from_steam_id64(76561198000000002);
305 let id3 = SteamID::from_steam_id64(76561198000000003);
306
307 cache.insert(id1, create_test_persona(id1, "User1"));
308 cache.insert(id2, create_test_persona(id2, "User2"));
309 let (found, missing) = cache.get_many(&[id1, id2, id3]);
312
313 assert_eq!(found.len(), 2);
314 assert_eq!(missing.len(), 1);
315 assert_eq!(missing[0], id3);
316 }
317
318 #[test]
319 fn test_cache_expired_entry() {
320 let config = PersonaCacheConfig { ttl: Duration::from_millis(1), max_size: 100 };
322 let cache = PersonaCache::new(config);
323 let steam_id = SteamID::from_steam_id64(76561198000000001);
324 let persona = create_test_persona(steam_id, "TestUser");
325
326 cache.insert(steam_id, persona);
327
328 std::thread::sleep(Duration::from_millis(10));
330
331 assert!(cache.get(&steam_id).is_none());
333 }
334
335 #[test]
336 fn test_cache_max_size_eviction() {
337 let config = PersonaCacheConfig { ttl: Duration::from_secs(300), max_size: 3 };
338 let cache = PersonaCache::new(config);
339
340 for i in 1..=3 {
342 let id = SteamID::from_steam_id64(76561198000000000 + i);
343 cache.insert(id, create_test_persona(id, &format!("User{}", i)));
344 }
345 assert_eq!(cache.len(), 3);
346
347 let id4 = SteamID::from_steam_id64(76561198000000004);
349 cache.insert(id4, create_test_persona(id4, "User4"));
350
351 assert_eq!(cache.len(), 3);
352 assert!(cache.get(&id4).is_some()); }
354
355 #[test]
356 fn test_cached_persona_is_expired() {
357 let persona = create_test_persona(SteamID::from_steam_id64(76561198000000001), "TestUser");
358 let cached = CachedPersona::new(persona);
359
360 assert!(!cached.is_expired(Duration::from_secs(300)));
362
363 assert!(cached.is_expired(Duration::ZERO));
365 }
366}