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