tower_sessions_moka_store/
lib.rs

1use std::time::{Duration as StdDuration, Instant as StdInstant};
2
3use async_trait::async_trait;
4use moka::{future::Cache, Expiry};
5use time::OffsetDateTime;
6use tower_sessions_core::{
7    session::{Id, Record},
8    session_store, SessionStore,
9};
10
11/// A session store that uses Moka, a fast and concurrent caching library.
12///
13/// This store uses Moka's built-in time-based per-entry expiration policy
14/// according to the session's expiry date. Therefore, expired sessions
15/// are automatically removed from the cache.
16#[derive(Debug, Clone)]
17pub struct MokaStore {
18    cache: Cache<Id, Record>,
19}
20
21impl MokaStore {
22    /// Create a new Moka store with the provided maximum capacity.
23    ///
24    /// # Examples
25    ///
26    /// ```rust
27    /// use tower_sessions::MemoryStore;
28    /// use tower_sessions_moka_store::MokaStore;
29    /// let session_store = MokaStore::new(Some(2_000));
30    /// ```
31    pub fn new(max_capacity: Option<u64>) -> Self {
32        // it would be useful to expose more of the CacheBuilder options to the user,
33        // but for now this is the most important one
34        let cache_builder = match max_capacity {
35            Some(capacity) => Cache::builder().max_capacity(capacity),
36            None => Cache::builder(),
37        }
38        .expire_after(SessionExpiry);
39
40        Self {
41            cache: cache_builder.build(),
42        }
43    }
44}
45
46#[async_trait]
47impl SessionStore for MokaStore {
48    async fn create(&self, record: &mut Record) -> session_store::Result<()> {
49        while self.cache.contains_key(&record.id) {
50            record.id = Id::default();
51        }
52        self.cache.insert(record.id, record.clone()).await;
53        Ok(())
54    }
55
56    async fn save(&self, record: &Record) -> session_store::Result<()> {
57        self.cache.insert(record.id, record.clone()).await;
58        Ok(())
59    }
60
61    async fn load(&self, session_id: &Id) -> session_store::Result<Option<Record>> {
62        // expired sessions are automatically removed from the cache,
63        // so it's safe to just call get
64        Ok(self.cache.get(session_id).await)
65    }
66
67    async fn delete(&self, session_id: &Id) -> session_store::Result<()> {
68        self.cache.invalidate(session_id).await;
69        Ok(())
70    }
71}
72
73/// Moka per-entry expiration policy for session records.
74struct SessionExpiry;
75
76impl SessionExpiry {
77    /// Calculates the expiry duration of a record
78    /// by comparing it to the current time.
79    ///
80    /// If the expiry date of the record is in the past,
81    /// returns an empty duration.
82    fn expiry_date_to_duration(record: &Record) -> StdDuration {
83        // we use this to calculate the current time
84        // because it is not possible to convert
85        // StdInstant to OffsetDateTime
86        let now = OffsetDateTime::now_utc();
87        let expiry_date = record.expiry_date;
88
89        if expiry_date > now {
90            (expiry_date - now).unsigned_abs()
91        } else {
92            StdDuration::default()
93        }
94    }
95}
96
97impl Expiry<Id, Record> for SessionExpiry {
98    fn expire_after_create(
99        &self,
100        _id: &Id,
101        record: &Record,
102        _created_at: StdInstant,
103    ) -> Option<StdDuration> {
104        Some(Self::expiry_date_to_duration(record))
105    }
106
107    fn expire_after_update(
108        &self,
109        _id: &Id,
110        record: &Record,
111        _updated_at: StdInstant,
112        _duration_until_expiry: Option<StdDuration>,
113    ) -> Option<StdDuration> {
114        // expiry_date could change, so we calculate it again
115        Some(Self::expiry_date_to_duration(record))
116    }
117}