axess_cache/lib.rs
1//! DST-friendly local hot-path cache primitives.
2//!
3//! [`ClockTtlCache`] is a TTL+LRU cache where every time-dependent decision
4//! goes through an injected [`Clock`](axess_clock::Clock) (from
5//! `axess-clock`). It exists so axess can offer hot-path caching without
6//! falling back to the `Instant::now()` + background-task model used by
7//! `moka` and friends, both of which break deterministic-simulation
8//! testing because their TTL eviction is invisible to test-mocked time.
9//!
10//! # Why not use moka directly?
11//!
12//! - **DST**: moka's TTL is wall-clock only; advancing a `MockClock` does
13//! not expire moka entries. Tests covering "entry expires after T" become
14//! impossible to write deterministically.
15//! - **Background scheduler**: moka spawns its own housekeeping task on a
16//! real-time interval. In a test runtime that controls time, this still
17//! advances on real wall-clock, leaking non-determinism.
18//!
19//! `ClockTtlCache` resolves both: TTL is checked on access against
20//! `clock.now()`, capacity-bounded eviction happens at insert time (LRU
21//! oldest-first), and there are no background tasks.
22//!
23//! # Compliance posture
24//!
25//! Caching authentication state (session validity, user identity) is widely
26//! flagged by PSD2/FAPI/SCA-style auditors. Caching *authorization input
27//! data* (the entity graph that Cedar evaluates against) is not. The
28//! decision is recomputed every request against fresh-from-cache entities.
29//! When you reach for this primitive, ask which side of that line you're
30//! on. The same code shape can be used for either, but the tradeoffs
31//! differ. See `axess-core::authz::cache` for the canonical authz use.
32//!
33//! # Quick start
34//!
35//! ```rust
36//! use axess_cache::ClockTtlCache;
37//! use axess_clock::Clock;
38//! use axess_clock::testing::MockClock;
39//! use chrono::{TimeZone, Utc};
40//! use std::num::NonZeroUsize;
41//! use std::sync::Arc;
42//! use std::time::Duration;
43//!
44//! let clock = Arc::new(MockClock::at(Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap()));
45//! let cache: ClockTtlCache<&'static str, u32> = ClockTtlCache::new(
46//! NonZeroUsize::new(128).unwrap(),
47//! Duration::from_secs(60),
48//! clock.clone() as Arc<dyn Clock>,
49//! );
50//!
51//! cache.insert("k", 42);
52//! assert_eq!(cache.get(&"k"), Some(42));
53//!
54//! // Advance past the TTL; entry must be gone on next access.
55//! clock.advance_secs(61);
56//! assert_eq!(cache.get(&"k"), None);
57//! ```
58//!
59//! # Module layout
60//!
61//! - `stats`: [`CacheStats`] public counter snapshot.
62//! - `cache`: [`ClockTtlCache`] core: LRU + TTL + single-flight loader.
63
64#![forbid(unsafe_code)]
65#![deny(missing_docs)]
66
67pub mod cache;
68pub mod stats;
69
70#[cfg(test)]
71mod tests;
72
73pub use cache::ClockTtlCache;
74pub use stats::CacheStats;