Skip to main content

cachekit/
lib.rs

1//! CacheKit — caching for Rust.
2//!
3//! Supports cachekit.io SaaS, Redis, Memcached, local File, and Cloudflare
4//! Workers backends. Zero-knowledge encryption via AES-256-GCM with HKDF key
5//! derivation.
6//!
7//! # Getting started: intent presets
8//!
9//! The intent presets are the primary entry point — one call that names your
10//! use case and returns a pre-configured [`CacheKitBuilder`] you can still
11//! override before [`build()`](CacheKitBuilder::build):
12//!
13//! | Intent | Backend | L1 | Encryption | Auto-reconnect² | Reliability¹ | Default TTL |
14//! |------------|-----------|------|------------|-----------------|--------------|-------------|
15//! | `CacheKit::minimal`³ | Redis | On (no SWR) | No | No | Off | 300 s |
16//! | `CacheKit::production`³ | Redis | On | No | Yes | On | 600 s |
17//! | `CacheKit::secure`³ ⁵ | Redis | On | AES-256-GCM | Yes | On | 600 s |
18//! | [`io`](CacheKit::io)⁴ | cachekit.io | On | No | n/a (HTTP) | On | 3 600 s |
19//!
20//! ¹ Retry with backoff + jitter, a circuit breaker, and backpressure around
21//! backend ops — see [`reliability`]. Requires the default-on `reliability`
22//! cargo feature.
23//! ² `production`/`secure` re-establish a dropped Redis connection with
24//! exponential backoff (100 ms → 30 s, retrying indefinitely); `minimal` is
25//! fail-fast (a dropped connection stays dead). **Initial** connections fail
26//! fast for every Redis preset — `io` opens no connection at construction: an
27//! empty API key fails at construction, while an invalid key or unreachable
28//! endpoint surfaces at the first request.
29//! Auto-reconnect is connection-level repair, distinct from the per-operation
30//! reliability stack.
31//! ³ Requires the `redis` cargo feature; `secure` also needs the
32//! default-on `encryption` feature.
33//! ⁴ API key by argument, or from `CACHEKIT_API_KEY` via
34//! [`io_from_env`](CacheKit::io_from_env). Neither reads
35//! `CACHEKIT_MASTER_KEY`; unlike [`from_env`](CacheKit::from_env), the `io`
36//! preset never activates encryption from the environment.
37//! ⁵ Master key as a hex string, `secure(url, master_key_hex)`, or from
38//! `CACHEKIT_MASTER_KEY` via `secure_from_env(url)` — the same hex decoding
39//! either way, so one key value derives the same key bytes in every CacheKit
40//! SDK. Use exactly 32 bytes (64 hex chars), the only length every SDK
41//! accepts. `secure_from_env` also reads decrypt-only rotation keys from
42//! `CACHEKIT_PREVIOUS_MASTER_KEYS`, validated as `from_env` validates them.
43//! A missing, non-hex or short key fails at construction, before any Redis
44//! I/O; `secure` never falls back to plaintext.
45//!
46//! ```no_run
47//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
48//! let cache = cachekit::CacheKit::io_from_env()?
49//!     .namespace("myapp")
50//!     .build()?;
51//!
52//! cache.set("greeting", &"Hello, world!").await?;
53//! let val: Option<String> = cache.get("greeting").await?;
54//! # Ok(())
55//! # }
56//! ```
57//!
58//! For full control, drop down to [`CacheKit::builder`] or
59//! [`CacheKit::from_env`].
60//!
61//! # Observability
62//!
63//! Every client counts its reads: [`CacheKit::stats`] answers "is my cache
64//! hitting?" (L1 hits / L2 hits / misses), [`CacheKit::l1_entry_count`]
65//! reports L1 occupancy, and with `reliability` on,
66//! [`CacheKit::circuit_state`] reports the breaker. The same counters feed the
67//! cachekit.io backends' `X-CacheKit-*` telemetry headers automatically. Enable
68//! the `tracing` cargo feature for a `debug` event per operation on the
69//! `cachekit` target (`op`, `outcome`, `key_hash` — never the key) and
70//! breaker transitions on `cachekit::reliability` — see [`metrics`].
71
72// Production code lints — these only fire in src/, not tests/
73#![warn(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
74#![warn(missing_docs)]
75
76// Mutually exclusive feature guards
77#[cfg(all(feature = "workers", feature = "redis"))]
78compile_error!(
79    "features `workers` and `redis` are mutually exclusive — Workers runtime cannot use fred"
80);
81
82#[cfg(all(feature = "workers", feature = "l1"))]
83compile_error!("features `workers` and `l1` are mutually exclusive — moka requires std threads unavailable in wasm32");
84
85#[cfg(all(feature = "workers", feature = "reliability"))]
86compile_error!("features `workers` and `reliability` are mutually exclusive — retry/breaker timers need tokio `time`, unavailable in wasm32");
87
88#[cfg(all(feature = "workers", feature = "memcached"))]
89compile_error!("features `workers` and `memcached` are mutually exclusive — Workers runtime has no TCP sockets");
90
91#[cfg(all(feature = "workers", feature = "file"))]
92compile_error!(
93    "features `workers` and `file` are mutually exclusive — Workers runtime has no filesystem"
94);
95
96/// Pluggable cache backend trait and implementations (CachekitIO, Redis,
97/// Memcached, File, Workers).
98pub mod backend;
99/// High-level cache client with dual-layer (L1/L2) support.
100pub mod client;
101/// Configuration types and environment variable parsing.
102pub mod config;
103/// Error types for cache operations and backend communication.
104pub mod error;
105/// Cold-miss single-flight: dedup concurrent fills of the same key.
106pub mod flight;
107/// Interop mode (interop/v1): cross-SDK cache keys and plain-MessagePack values.
108pub mod interop;
109/// Live hit/miss counters, the SaaS telemetry headers built from them, and
110/// (feature `tracing`) structured events for cache operations.
111pub mod metrics;
112/// Serialization and deserialization of cached values via MessagePack.
113pub mod serializer;
114/// SDK session tracking (session ID and start timestamp).
115pub mod session;
116/// SSRF-safe URL validation for CachekitIO endpoints.
117pub mod url_validator;
118
119/// Intent-based cache presets (`CacheKit::minimal`, `::production`, `::secure`, `::io`).
120mod intents;
121
122/// Client-side AES-256-GCM encryption with HKDF key derivation.
123#[cfg(feature = "encryption")]
124pub mod encryption;
125
126/// In-process L1 cache backed by [`moka`] with per-entry TTL.
127#[cfg(feature = "l1")]
128pub mod l1;
129
130/// Reliability tier: retry with backoff + jitter, circuit breaker.
131#[cfg(all(feature = "reliability", not(target_arch = "wasm32")))]
132pub mod reliability;
133
134// Re-exports
135pub use client::{CacheKit, CacheKitBuilder, SharedBackend, SwrRead, SwrToken};
136pub use config::CachekitConfig;
137pub use error::{BackendError, BackendErrorKind, CachekitError};
138pub use metrics::L1Stats;
139
140#[cfg(feature = "encryption")]
141pub use client::SecureCache;
142#[cfg(feature = "encryption")]
143pub use encryption::EncryptionLayer;
144
145#[cfg(feature = "macros")]
146pub use cachekit_macros::cachekit;
147
148pub use flight::SingleFlight;
149
150#[cfg(all(feature = "reliability", not(target_arch = "wasm32")))]
151pub use reliability::{
152    BackpressureConfig, CircuitBreakerConfig, CircuitState, ReliabilityConfig, RetryConfig,
153};
154
155// ── Shared jitter source ─────────────────────────────────────────────────────
156
157/// Uniform random in `[0, 1)`. uuid v4 is the crate's existing entropy source
158/// (getrandom-backed); jitter needs decorrelation across clients, not crypto
159/// quality — 53 bits is plenty. Used by retry backoff (`reliability`) and the
160/// L1 SWR freshness threshold at entry insertion (`l1`).
161#[cfg(any(
162    feature = "l1",
163    all(feature = "reliability", not(target_arch = "wasm32"))
164))]
165pub(crate) fn random_unit() -> f64 {
166    let bits = uuid::Uuid::new_v4().as_u128() & ((1u128 << 53) - 1);
167    (bits as f64) / ((1u64 << 53) as f64)
168}
169
170// ── SWR background-refresh spawn (macro plumbing) ───────────────────────────
171
172/// Spawn a stale-while-revalidate background refresh onto the ambient tokio
173/// runtime. Macro plumbing for `#[cachekit]` — not public API.
174///
175/// Without a tokio runtime on the current thread the refresh is skipped: the
176/// caller has already been served the stale value, and a later stale read
177/// simply retries. Panicking here would turn a cache optimisation into an
178/// availability bug on non-tokio executors.
179#[cfg(all(feature = "l1", not(feature = "unsync"), not(target_arch = "wasm32")))]
180#[doc(hidden)]
181pub fn __swr_spawn<F>(fut: F)
182where
183    F: std::future::Future<Output = ()> + Send + 'static,
184{
185    if let Ok(handle) = tokio::runtime::Handle::try_current() {
186        drop(handle.spawn(fut));
187    }
188}
189
190/// No-op variant: under `unsync`, on wasm32, or without the `l1` feature the
191/// client never classifies a hit as stale (`SwrRead::Stale` is unreachable),
192/// so the refresh future handed here is dead code by construction. The stub
193/// exists so `#[cachekit]`-generated code compiles under every configuration.
194#[cfg(not(all(feature = "l1", not(feature = "unsync"), not(target_arch = "wasm32"))))]
195#[doc(hidden)]
196pub fn __swr_spawn<F>(_fut: F)
197where
198    F: std::future::Future<Output = ()> + 'static,
199{
200}
201
202/// Convenient glob import for the most common types.
203pub mod prelude {
204    pub use crate::{
205        BackendError, BackendErrorKind, CacheKit, CacheKitBuilder, CachekitConfig, CachekitError,
206        SwrRead, SwrToken,
207    };
208
209    #[cfg(feature = "encryption")]
210    pub use crate::{EncryptionLayer, SecureCache};
211
212    #[cfg(feature = "macros")]
213    pub use crate::cachekit;
214}