1use std::collections::HashMap;
33use std::sync::Arc;
34use std::time::{Duration, SystemTime};
35
36use async_trait::async_trait;
37use serde::{Deserialize, Serialize};
38use tokio::sync::RwLock;
39
40use crate::protocol::{ClientCapabilities, Implementation};
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
48#[non_exhaustive]
49pub struct SessionRecord {
50 pub id: String,
52 pub protocol_version: String,
54 pub client_info: Option<Implementation>,
56 pub client_capabilities: Option<ClientCapabilities>,
58 pub created_at: SystemTime,
60 pub last_accessed: SystemTime,
62 pub expires_at: SystemTime,
64}
65
66impl SessionRecord {
67 pub fn new(id: impl Into<String>, protocol_version: impl Into<String>, ttl: Duration) -> Self {
71 let now = SystemTime::now();
72 Self {
73 id: id.into(),
74 protocol_version: protocol_version.into(),
75 client_info: None,
76 client_capabilities: None,
77 created_at: now,
78 last_accessed: now,
79 expires_at: now + ttl,
80 }
81 }
82
83 pub fn touch(&mut self, ttl: Duration) {
85 let now = SystemTime::now();
86 self.last_accessed = now;
87 self.expires_at = now + ttl;
88 }
89
90 pub fn is_expired(&self) -> bool {
92 SystemTime::now() >= self.expires_at
93 }
94}
95
96#[derive(Debug, thiserror::Error)]
102#[non_exhaustive]
103pub enum SessionStoreError {
104 #[error("encode error: {0}")]
106 Encode(String),
107 #[error("decode error: {0}")]
109 Decode(String),
110 #[error("backend error: {0}")]
112 Backend(String),
113}
114
115pub type Result<T> = std::result::Result<T, SessionStoreError>;
117
118#[async_trait]
135pub trait SessionStore: Send + Sync + 'static {
136 async fn create(&self, record: &mut SessionRecord) -> Result<()>;
141
142 async fn save(&self, record: &SessionRecord) -> Result<()>;
144
145 async fn load(&self, id: &str) -> Result<Option<SessionRecord>>;
147
148 async fn delete(&self, id: &str) -> Result<()>;
150}
151
152#[derive(Debug, Default, Clone)]
158pub struct MemorySessionStore {
159 inner: Arc<RwLock<HashMap<String, SessionRecord>>>,
160}
161
162impl MemorySessionStore {
163 pub fn new() -> Self {
165 Self::default()
166 }
167
168 pub async fn len(&self) -> usize {
172 self.inner.read().await.len()
173 }
174
175 pub async fn is_empty(&self) -> bool {
177 self.inner.read().await.is_empty()
178 }
179
180 pub async fn cleanup_expired(&self) -> usize {
182 let mut map = self.inner.write().await;
183 let before = map.len();
184 map.retain(|_, record| !record.is_expired());
185 before - map.len()
186 }
187}
188
189#[async_trait]
190impl SessionStore for MemorySessionStore {
191 async fn create(&self, record: &mut SessionRecord) -> Result<()> {
192 let mut map = self.inner.write().await;
193 while map.contains_key(&record.id) {
196 record.id = uuid::Uuid::new_v4().to_string();
197 }
198 map.insert(record.id.clone(), record.clone());
199 Ok(())
200 }
201
202 async fn save(&self, record: &SessionRecord) -> Result<()> {
203 self.inner
204 .write()
205 .await
206 .insert(record.id.clone(), record.clone());
207 Ok(())
208 }
209
210 async fn load(&self, id: &str) -> Result<Option<SessionRecord>> {
211 let map = self.inner.read().await;
212 Ok(map.get(id).filter(|r| !r.is_expired()).cloned())
213 }
214
215 async fn delete(&self, id: &str) -> Result<()> {
216 self.inner.write().await.remove(id);
217 Ok(())
218 }
219}
220
221#[derive(Debug, Clone)]
245pub struct CachingSessionStore<Cache, Store> {
246 cache: Cache,
247 store: Store,
248}
249
250impl<Cache, Store> CachingSessionStore<Cache, Store> {
251 pub fn new(cache: Cache, store: Store) -> Self {
253 Self { cache, store }
254 }
255}
256
257#[async_trait]
258impl<Cache, Store> SessionStore for CachingSessionStore<Cache, Store>
259where
260 Cache: SessionStore,
261 Store: SessionStore,
262{
263 async fn create(&self, record: &mut SessionRecord) -> Result<()> {
264 self.store.create(record).await?;
267 self.cache.save(record).await?;
268 Ok(())
269 }
270
271 async fn save(&self, record: &SessionRecord) -> Result<()> {
272 self.store.save(record).await?;
273 self.cache.save(record).await?;
274 Ok(())
275 }
276
277 async fn load(&self, id: &str) -> Result<Option<SessionRecord>> {
278 if let Some(record) = self.cache.load(id).await? {
279 return Ok(Some(record));
280 }
281 match self.store.load(id).await? {
282 Some(record) => {
283 let _ = self.cache.save(&record).await;
286 Ok(Some(record))
287 }
288 None => Ok(None),
289 }
290 }
291
292 async fn delete(&self, id: &str) -> Result<()> {
293 let cache_result = self.cache.delete(id).await;
294 let store_result = self.store.delete(id).await;
295 cache_result.and(store_result)
296 }
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302 use std::time::Duration;
303
304 fn sample_record(id: &str) -> SessionRecord {
305 SessionRecord::new(id, "2025-11-25", Duration::from_secs(60))
306 }
307
308 #[tokio::test]
309 async fn memory_store_create_load_delete() {
310 let store = MemorySessionStore::new();
311 let mut record = sample_record("abc");
312 store.create(&mut record).await.unwrap();
313
314 let loaded = store.load("abc").await.unwrap();
315 assert!(loaded.is_some());
316 assert_eq!(loaded.unwrap().id, "abc");
317
318 store.delete("abc").await.unwrap();
319 assert!(store.load("abc").await.unwrap().is_none());
320 }
321
322 #[tokio::test]
323 async fn memory_store_create_regenerates_id_on_collision() {
324 let store = MemorySessionStore::new();
325 let mut first = sample_record("dup");
326 store.create(&mut first).await.unwrap();
327
328 let mut second = sample_record("dup");
329 store.create(&mut second).await.unwrap();
330
331 assert_ne!(first.id, second.id);
332 assert!(store.load(&first.id).await.unwrap().is_some());
333 assert!(store.load(&second.id).await.unwrap().is_some());
334 }
335
336 #[tokio::test]
337 async fn memory_store_save_upserts() {
338 let store = MemorySessionStore::new();
339 let mut record = sample_record("upsert");
340 store.create(&mut record).await.unwrap();
341
342 record.protocol_version = "2025-06-18".into();
343 store.save(&record).await.unwrap();
344
345 let loaded = store.load("upsert").await.unwrap().unwrap();
346 assert_eq!(loaded.protocol_version, "2025-06-18");
347 }
348
349 #[tokio::test]
350 async fn memory_store_hides_expired_records() {
351 let store = MemorySessionStore::new();
352 let mut record = SessionRecord::new("expired", "2025-11-25", Duration::from_millis(1));
353 store.create(&mut record).await.unwrap();
354
355 tokio::time::sleep(Duration::from_millis(10)).await;
356
357 assert!(store.load("expired").await.unwrap().is_none());
358 }
359
360 #[tokio::test]
361 async fn memory_store_cleanup_removes_expired() {
362 let store = MemorySessionStore::new();
363
364 let mut live = SessionRecord::new("live", "2025-11-25", Duration::from_secs(60));
365 store.create(&mut live).await.unwrap();
366
367 let mut dead = SessionRecord::new("dead", "2025-11-25", Duration::from_millis(1));
368 store.create(&mut dead).await.unwrap();
369
370 tokio::time::sleep(Duration::from_millis(10)).await;
371
372 let removed = store.cleanup_expired().await;
373 assert_eq!(removed, 1);
374 assert_eq!(store.len().await, 1);
375 }
376
377 #[tokio::test]
378 async fn memory_store_delete_is_idempotent() {
379 let store = MemorySessionStore::new();
380 store.delete("nonexistent").await.unwrap();
381 }
382
383 #[tokio::test]
384 async fn record_round_trips_client_info_and_capabilities() {
385 let store = MemorySessionStore::new();
390 let mut record = sample_record("client-meta");
391 record.client_info = Some(crate::protocol::Implementation {
392 name: "test-client".into(),
393 version: "9.9.9".into(),
394 title: Some("Test Client".into()),
395 description: None,
396 icons: None,
397 website_url: None,
398 meta: None,
399 });
400 record.client_capabilities = Some(crate::protocol::ClientCapabilities {
401 roots: Some(crate::protocol::RootsCapability {
402 list_changed: true,
403 deprecated: None,
404 }),
405 ..Default::default()
406 });
407
408 store.create(&mut record).await.unwrap();
409 let loaded = store
410 .load(&record.id)
411 .await
412 .unwrap()
413 .expect("record should survive create/load round trip");
414
415 let info = loaded
416 .client_info
417 .expect("client_info should survive round trip");
418 assert_eq!(info.name, "test-client");
419 assert_eq!(info.version, "9.9.9");
420 assert_eq!(info.title.as_deref(), Some("Test Client"));
421
422 let caps = loaded
423 .client_capabilities
424 .expect("client_capabilities should survive round trip");
425 assert_eq!(caps.roots.map(|r| r.list_changed), Some(true));
426 }
427
428 #[tokio::test]
429 async fn record_touch_updates_timestamps() {
430 let mut record = SessionRecord::new("t", "2025-11-25", Duration::from_secs(60));
431 let original_expiry = record.expires_at;
432
433 tokio::time::sleep(Duration::from_millis(10)).await;
434 record.touch(Duration::from_secs(60));
435
436 assert!(record.expires_at > original_expiry);
437 }
438
439 #[tokio::test]
440 async fn dyn_session_store_object_safe() {
441 let store: Arc<dyn SessionStore> = Arc::new(MemorySessionStore::new());
443 let mut record = sample_record("dyn");
444 store.create(&mut record).await.unwrap();
445 assert!(store.load(&record.id).await.unwrap().is_some());
446 }
447
448 #[tokio::test]
449 async fn caching_store_writes_to_both_tiers() {
450 let cache = MemorySessionStore::new();
451 let backend = MemorySessionStore::new();
452 let store = CachingSessionStore::new(cache.clone(), backend.clone());
453
454 let mut record = sample_record("cached");
455 store.create(&mut record).await.unwrap();
456
457 assert!(cache.load(&record.id).await.unwrap().is_some());
458 assert!(backend.load(&record.id).await.unwrap().is_some());
459 }
460
461 #[tokio::test]
462 async fn caching_store_populates_cache_on_miss() {
463 let cache = MemorySessionStore::new();
464 let backend = MemorySessionStore::new();
465
466 let mut record = sample_record("warm");
468 backend.create(&mut record).await.unwrap();
469 let id = record.id.clone();
470 assert!(cache.load(&id).await.unwrap().is_none());
471
472 let store = CachingSessionStore::new(cache.clone(), backend);
474 let loaded = store.load(&id).await.unwrap();
475 assert!(loaded.is_some());
476 assert!(cache.load(&id).await.unwrap().is_some());
477 }
478
479 #[tokio::test]
480 async fn caching_store_delete_clears_both() {
481 let cache = MemorySessionStore::new();
482 let backend = MemorySessionStore::new();
483 let store = CachingSessionStore::new(cache.clone(), backend.clone());
484
485 let mut record = sample_record("gone");
486 store.create(&mut record).await.unwrap();
487 store.delete(&record.id).await.unwrap();
488
489 assert!(cache.load(&record.id).await.unwrap().is_none());
490 assert!(backend.load(&record.id).await.unwrap().is_none());
491 }
492}