pavex_session/
store_.rs

1use crate::SessionId;
2use errors::{
3    ChangeIdError, CreateError, DeleteError, DeleteExpiredError, LoadError, UpdateError,
4    UpdateTtlError,
5};
6use serde_json::Value;
7use std::{borrow::Cow, collections::HashMap, num::NonZeroUsize};
8
9/// Where server-side session records are stored.
10///
11/// It is a thin wrapper
12/// [around your chosen storage backend implementation][`SessionStorageBackend`],
13/// removing the need to specify the concrete type of the storage backend
14/// everywhere in your code.
15#[derive(Debug)]
16pub struct SessionStore(Box<dyn SessionStorageBackend>);
17
18impl SessionStore {
19    /// Creates a new session store using the provided backend.
20    pub fn new<Backend>(backend: Backend) -> Self
21    where
22        Backend: SessionStorageBackend + 'static,
23    {
24        Self(Box::new(backend))
25    }
26
27    /// Creates a new session record in the store using the provided ID.
28    pub async fn create(
29        &self,
30        id: &SessionId,
31        record: SessionRecordRef<'_>,
32    ) -> Result<(), CreateError> {
33        self.0.create(id, record).await
34    }
35
36    /// Update the state of an existing session in the store.
37    ///
38    /// It overwrites the existing record with the provided one.
39    pub async fn update(
40        &self,
41        id: &SessionId,
42        record: SessionRecordRef<'_>,
43    ) -> Result<(), UpdateError> {
44        self.0.update(id, record).await
45    }
46
47    /// Update the TTL of an existing session record in the store.
48    ///
49    /// It leaves the session state unchanged.
50    pub async fn update_ttl(
51        &self,
52        id: &SessionId,
53        ttl: std::time::Duration,
54    ) -> Result<(), UpdateTtlError> {
55        self.0.update_ttl(id, ttl).await
56    }
57
58    /// Loads an existing session record from the store using the provided ID.
59    ///
60    /// If a session with the given ID exists, it is returned. If the session
61    /// does not exist or has been invalidated (e.g., expired), `None` is
62    /// returned.
63    pub async fn load(&self, id: &SessionId) -> Result<Option<SessionRecord>, LoadError> {
64        self.0.load(id).await
65    }
66
67    /// Deletes a session record from the store using the provided ID.
68    ///
69    /// If the session exists, it is removed from the store.
70    pub async fn delete(&self, id: &SessionId) -> Result<(), DeleteError> {
71        self.0.delete(id).await
72    }
73
74    /// Change the session id associated with an existing session record.
75    ///
76    /// The server-side state is left unchanged.
77    pub async fn change_id(
78        &self,
79        old_id: &SessionId,
80        new_id: &SessionId,
81    ) -> Result<(), ChangeIdError> {
82        self.0.change_id(old_id, new_id).await
83    }
84
85    /// Deletes expired session records from the store.
86    pub async fn delete_expired(
87        &self,
88        batch_size: Option<NonZeroUsize>,
89    ) -> Result<usize, DeleteExpiredError> {
90        self.0.delete_expired(batch_size).await
91    }
92}
93
94#[async_trait::async_trait]
95/// The interface of a session storage backend.
96pub trait SessionStorageBackend: std::fmt::Debug + Send + Sync {
97    /// Creates a new session record in the store using the provided ID.
98    async fn create(&self, id: &SessionId, record: SessionRecordRef<'_>)
99    -> Result<(), CreateError>;
100
101    /// Update the state of an existing session in the store.
102    ///
103    /// It overwrites the existing record with the provided one.
104    async fn update(&self, id: &SessionId, record: SessionRecordRef<'_>)
105    -> Result<(), UpdateError>;
106
107    /// Update the TTL of an existing session record in the store.
108    ///
109    /// It leaves the session state unchanged.
110    async fn update_ttl(
111        &self,
112        id: &SessionId,
113        ttl: std::time::Duration,
114    ) -> Result<(), UpdateTtlError>;
115
116    /// Loads an existing session record from the store using the provided ID.
117    ///
118    /// If a session with the given ID exists, it is returned. If the session
119    /// does not exist or has been invalidated (e.g., expired), `None` is
120    /// returned.
121    async fn load(&self, session_id: &SessionId) -> Result<Option<SessionRecord>, LoadError>;
122
123    /// Deletes a session record from the store using the provided ID.
124    ///
125    /// If the session exists, it is removed from the store.
126    async fn delete(&self, session_id: &SessionId) -> Result<(), DeleteError>;
127
128    /// Change the session id associated with an existing session record.
129    ///
130    /// The server-side state is left unchanged.
131    async fn change_id(&self, old_id: &SessionId, new_id: &SessionId) -> Result<(), ChangeIdError>;
132
133    /// Deletes expired session records from the store.
134    ///
135    /// If `batch_size` is provided, the query will delete at most `batch_size` expired sessions.
136    /// In either case, if successful, the method returns the number of expired sessions that
137    /// have been deleted.
138    ///
139    /// # When should you delete in batches?
140    ///
141    /// If there are a lot of expired sessions in the database, deleting them all at once can
142    /// cause performance issues. By deleting in batches, you can limit the number of sessions
143    /// deleted in a single query, reducing the impact.
144    ///
145    /// # Do I need to call this method?
146    ///
147    /// It depends on the storage backend you are using. Some backends (e.g. Redis) have
148    /// built-in support for expiring keys, so you may not need to call this method at all.
149    ///
150    /// If you're adding support for a new backend that has built-in support for expiring keys,
151    /// you can simply return `Ok(0)` from this method.
152    async fn delete_expired(
153        &self,
154        batch_size: Option<NonZeroUsize>,
155    ) -> Result<usize, DeleteExpiredError>;
156}
157
158/// A server-side session record that's going to be stored in the
159/// chosen storage backend.
160#[derive(Debug)]
161pub struct SessionRecordRef<'session> {
162    /// The set of key-value pairs attached to a session.
163    pub state: Cow<'session, HashMap<Cow<'static, str>, Value>>,
164    /// The session time-to-live.
165    pub ttl: std::time::Duration,
166}
167
168impl SessionRecordRef<'_> {
169    pub(crate) fn empty(ttl: std::time::Duration) -> Self {
170        Self {
171            state: Cow::Owned(HashMap::new()),
172            ttl,
173        }
174    }
175}
176
177/// A server-side session record that was retrieved from the
178/// chosen storage backend.
179#[derive(Debug)]
180pub struct SessionRecord {
181    /// The set of key-value pairs attached to a session.
182    pub state: HashMap<Cow<'static, str>, Value>,
183    /// The session time-to-live.
184    pub ttl: std::time::Duration,
185}
186
187/// Errors that can occur when interacting with a session storage backend.
188pub mod errors {
189    use crate::SessionId;
190
191    #[non_exhaustive]
192    #[derive(Debug, thiserror::Error)]
193    /// The error returned by [`SessionStorageBackend::create`][super::SessionStorageBackend::create].
194    pub enum CreateError {
195        /// Failed to serialize the session state.
196        #[error("Failed to serialize the session state.")]
197        SerializationError(#[from] serde_json::Error),
198        #[error(transparent)]
199        /// A session with the same ID already exists.
200        DuplicateId(#[from] DuplicateIdError),
201        /// Something else went wrong when creating a new session record.
202        #[error("Something went wrong when creating a new session record.")]
203        Other(#[source] anyhow::Error),
204    }
205
206    #[non_exhaustive]
207    #[derive(Debug, thiserror::Error)]
208    /// The error returned by [`SessionStorageBackend::update`][super::SessionStorageBackend::update].
209    pub enum UpdateError {
210        #[error("Failed to serialize the session state.")]
211        /// Failed to serialize the session state.
212        SerializationError(#[from] serde_json::Error),
213        #[error(transparent)]
214        /// There is no session with the given ID.
215        UnknownIdError(#[from] UnknownIdError),
216        /// Something else went wrong when updating the session record.
217        #[error("Something went wrong when updating the session record.")]
218        Other(#[source] anyhow::Error),
219    }
220
221    #[non_exhaustive]
222    #[derive(Debug, thiserror::Error)]
223    /// The error returned by [`SessionStorageBackend::update_ttl`][super::SessionStorageBackend::update_ttl].
224    pub enum UpdateTtlError {
225        #[error(transparent)]
226        /// There is no session with the given ID.
227        UnknownId(#[from] UnknownIdError),
228        /// Something else went wrong when updating the session record.
229        #[error("Something went wrong when updating the TTL of the session record.")]
230        Other(#[source] anyhow::Error),
231    }
232
233    #[non_exhaustive]
234    #[derive(Debug, thiserror::Error)]
235    /// The error returned by [`SessionStorageBackend::load`][super::SessionStorageBackend::load].
236    pub enum LoadError {
237        #[error("Failed to deserialize the session state.")]
238        /// Failed to deserialize the session state.
239        DeserializationError(#[source] anyhow::Error),
240        /// Something else went wrong when loading the session record.
241        #[error("Something went wrong when loading the session record.")]
242        Other(#[source] anyhow::Error),
243    }
244
245    #[non_exhaustive]
246    #[derive(Debug, thiserror::Error)]
247    /// The error returned by [`SessionStorageBackend::delete`][super::SessionStorageBackend::delete].
248    pub enum DeleteError {
249        #[error(transparent)]
250        /// There is no session with the given ID.
251        UnknownId(#[from] UnknownIdError),
252        /// Something else went wrong when deleting the session record.
253        #[error("Something went wrong when deleting the session record.")]
254        Other(#[source] anyhow::Error),
255    }
256
257    #[non_exhaustive]
258    #[derive(Debug, thiserror::Error)]
259    /// The error returned by [`SessionStorageBackend::change_id`][super::SessionStorageBackend::change_id].
260    pub enum ChangeIdError {
261        #[error(transparent)]
262        /// There is no session with the given ID.
263        UnknownId(#[from] UnknownIdError),
264        #[error(transparent)]
265        /// There is already a session associated with the new ID>
266        DuplicateId(#[from] DuplicateIdError),
267        /// Something else went wrong when deleting the session record.
268        #[error("Something went wrong when changing the session id for a session record.")]
269        Other(#[source] anyhow::Error),
270    }
271
272    /// The error returned by [`SessionStorageBackend::delete_expired`][super::SessionStorageBackend::delete_expired].
273    #[derive(Debug, thiserror::Error)]
274    #[error("Something went wrong when deleting expired sessions")]
275    pub struct DeleteExpiredError(#[from] anyhow::Error);
276
277    #[derive(thiserror::Error)]
278    #[error("There is no session with the given id")]
279    /// There is no session with the given ID.
280    pub struct UnknownIdError {
281        pub id: SessionId,
282    }
283
284    impl std::fmt::Debug for UnknownIdError {
285        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286            f.write_str("UnknownIdError")
287        }
288    }
289
290    #[derive(thiserror::Error)]
291    #[error("A session with the same ID already exists.")]
292    /// A session with the same ID already exists.
293    pub struct DuplicateIdError {
294        pub id: SessionId,
295    }
296
297    impl std::fmt::Debug for DuplicateIdError {
298        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
299            f.write_str("DuplicateIdError")
300        }
301    }
302}