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}