Skip to main content

axess_core/
store.rs

1//! Shared key/value-with-TTL store abstraction.
2//!
3//! Provides a single `Store<K, V>` trait so `SessionStore` (and any
4//! adopter-defined kv-shaped store) can share one set of backends
5//! instead of each shipping its own copy of the SQL/Valkey glue.
6//! Per-backend serialization is owned by the concrete backend wrapper
7//! (`SessionCodec`, `BindingsCodec`); there is no cross-backend
8//! `Codec<V>` trait: the dialect-specific SQL bodies were too thin to
9//! justify a `sqlx::Database` + `Codec<V>` bound surface (see
10//! `session/storage/sql_helpers.rs` for the close-out rationale).
11//!
12//! Stores with non-kv semantics, `IdentityStore`, `FactorStore`,
13//! `DeviceStore`, `SessionRegistry`, stay distinct because their
14//! domain primitives (secondary indexes, set membership, pub/sub on
15//! revocation) don't fit a flat key/value put.
16
17use std::future::Future;
18use std::time::Duration;
19
20/// In-memory [`Store`] backend: `DashMap` + monotonic `Instant`
21/// deadlines for TTL. Codec-free (values stored by `Clone` rather
22/// than serialised) so adopters reaching for `MemoryStore` for tests
23/// don't pay an encode/decode round-trip per operation. Per-store
24/// wrappers like `MemoryRefreshTokenStore` are thin newtypes around
25/// this backend.
26///
27/// Gated behind `cfg(any(test, feature = "memory"))` so production
28/// builds without the `memory` feature cannot ship an in-memory store
29/// by accident.
30#[cfg(any(test, feature = "memory"))]
31pub mod memory;
32#[cfg(any(test, feature = "memory"))]
33pub use memory::MemoryStore;
34
35/// Generic key/value-with-TTL store. Implemented by per-backend
36/// wrappers (`MemoryStore`, `SqlStore`, `ValkeyStore`) so each
37/// per-store newtype (e.g. `SqliteSessionStore`) reduces to a thin
38/// delegating shim.
39///
40/// The trait carries no `cycle` / index-query primitives; those are
41/// per-store specifics that live on the domain trait
42/// (`SessionStore::cycle`, `RefreshTokenStore::revoke_family`, …)
43/// which wraps the underlying `Store` and dispatches to backend-specific
44/// helpers for the non-kv operations.
45pub trait Store<K, V>: Send + Sync + Clone + 'static
46where
47    K: Send + Sync + ?Sized,
48    V: Send + Sync,
49{
50    /// Backend-specific error. Use the shared [`StoreError`] enum for
51    /// new backends; legacy wrappers may continue to surface
52    /// `SqlStoreError` / `ValkeyStoreError` / `PostgresStoreError`
53    /// until each is consolidated.
54    type Error: std::error::Error + Send + Sync + 'static;
55
56    /// Fetch the value for `key`. `Ok(None)` when the key is absent
57    /// (including TTL-expired); `Err` only on backend failure.
58    fn get(&self, key: &K) -> impl Future<Output = Result<Option<V>, Self::Error>> + Send;
59
60    /// Insert or replace the value at `key` with the given TTL.
61    fn put(
62        &self,
63        key: &K,
64        value: &V,
65        ttl: Duration,
66    ) -> impl Future<Output = Result<(), Self::Error>> + Send;
67
68    /// Remove the entry at `key`. Idempotent; does not error if absent.
69    fn delete(&self, key: &K) -> impl Future<Output = Result<(), Self::Error>> + Send;
70
71    /// Bulk-evict every TTL-expired entry. Returns the number reclaimed.
72    /// Backends with native TTL eviction (Valkey/Redis) may implement
73    /// this as a no-op returning `Ok(0)`; backends owning their own
74    /// row table (SQLite, Postgres, in-memory) actually delete.
75    fn prune_expired(&self) -> impl Future<Output = Result<u64, Self::Error>> + Send;
76}
77
78/// Generic store error. Adopters and per-backend `SqlStore` /
79/// `ValkeyStore` impls use this in place of the legacy per-store
80/// errors (`SqlStoreError`, `ValkeyStoreError`, …) once they have
81/// been migrated.
82///
83/// The type parameter `B` is the backend's native error type
84/// (`sqlx::Error`, `fred::error::Error`, `Infallible` for memory).
85#[derive(Debug, thiserror::Error)]
86pub enum StoreError<B>
87where
88    B: std::error::Error + Send + Sync + 'static,
89{
90    /// Underlying backend reported an error (network, protocol,
91    /// schema, …).
92    #[error("backend: {0}")]
93    Backend(#[source] B),
94}