1#![forbid(unsafe_code)]
8
9use std::{fmt, time::Duration};
10
11#[cfg(feature = "memory")]
12mod memory;
13#[cfg(feature = "memory")]
14pub use memory::{MemoryCache, MemoryOptions};
15#[cfg(all(feature = "redis", not(target_arch = "wasm32")))]
16mod redis_backend;
17#[cfg(all(feature = "redis", not(target_arch = "wasm32")))]
18pub use redis_backend::{RedisCache, RedisOptions};
19
20pub type Result<T> = std::result::Result<T, CacheError>;
21
22#[derive(Debug, thiserror::Error)]
23pub enum CacheError {
24 #[error("invalid cache operation: {0}")]
25 Invalid(&'static str),
26 #[error("cache capacity exhausted")]
27 Capacity,
28 #[error("counter would underflow")]
29 Underflow,
30 #[error("cache state is corrupt or belongs to another implementation")]
31 Corrupt,
32 #[error("cache mutex poisoned")]
33 Poisoned,
34 #[error("notification subscription closed; resubscribe and reload durable state")]
35 Closed,
36 #[error("cannot generate cache token: {0}")]
37 Entropy(String),
38 #[cfg(all(feature = "redis", not(target_arch = "wasm32")))]
39 #[error("Redis operation failed: {0}")]
40 Redis(#[from] redis::RedisError),
41 #[error("cache connection timed out")]
42 Timeout,
43}
44
45#[derive(Clone, Copy, PartialEq, Eq, Hash)]
48pub struct Version([u8; 32]);
49impl Version {
50 pub fn from_bytes(bytes: [u8; 32]) -> Self {
51 Self(bytes)
52 }
53 pub fn as_bytes(&self) -> &[u8; 32] {
54 &self.0
55 }
56 #[cfg(any(
57 feature = "memory",
58 all(feature = "redis", not(target_arch = "wasm32"))
59 ))]
60 fn fresh() -> Result<Self> {
61 let mut bytes = [0; 32];
62 getrandom::fill(&mut bytes).map_err(|e| CacheError::Entropy(e.to_string()))?;
63 Ok(Self(bytes))
64 }
65}
66impl fmt::Debug for Version {
67 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68 f.write_str("Version(..)")
69 }
70}
71
72#[derive(Clone, PartialEq, Eq)]
73pub struct Entry {
74 pub value: Vec<u8>,
75 pub version: Version,
76}
77impl fmt::Debug for Entry {
78 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79 f.debug_struct("Entry")
80 .field("bytes", &self.value.len())
81 .field("version", &self.version)
82 .finish()
83 }
84}
85#[derive(Clone)]
86pub struct Replacement {
87 pub value: Vec<u8>,
88 pub ttl: Duration,
89}
90#[derive(Clone, Copy, Debug, PartialEq, Eq)]
91pub enum CasOutcome {
92 Applied(Option<Version>),
93 Conflict,
94}
95
96#[derive(Clone, Copy, Debug, PartialEq, Eq)]
99pub struct Counter {
100 pub value: u64,
101 pub generation: Version,
102}
103#[derive(Clone, Copy, Debug, PartialEq, Eq)]
104pub enum IncrementOutcome {
105 Applied(Counter),
106 Limited { current: u64 },
107}
108
109#[derive(Clone, Debug, PartialEq, Eq)]
110pub enum Notification {
111 Message(Vec<u8>),
113 ResyncRequired,
115}
116
117#[async_trait::async_trait]
118pub trait Subscription: Send {
119 async fn recv(&mut self) -> Result<Notification>;
122}
123
124#[async_trait::async_trait]
127pub trait Cache: Send + Sync {
128 async fn get(&self, key: &str) -> Result<Option<Entry>>;
129 async fn get_many(&self, keys: &[String]) -> Result<Vec<Option<Entry>>> {
134 let mut entries = Vec::with_capacity(keys.len());
135 for key in keys {
136 entries.push(self.get(key).await?);
137 }
138 Ok(entries)
139 }
140 async fn put(&self, key: &str, value: Vec<u8>, ttl: Duration) -> Result<Version>;
141 async fn delete(&self, key: &str) -> Result<bool>;
142 async fn compare_exchange(
144 &self,
145 key: &str,
146 expected: Option<Version>,
147 replacement: Option<Replacement>,
148 ) -> Result<CasOutcome>;
149 async fn counter(&self, key: &str) -> Result<Option<Counter>>;
150 async fn increment(
151 &self,
152 key: &str,
153 amount: u64,
154 limit: u64,
155 ttl: Duration,
156 ) -> Result<IncrementOutcome>;
157 async fn decrement(
161 &self,
162 key: &str,
163 generation: Version,
164 amount: u64,
165 ) -> Result<Option<Counter>>;
166 async fn acquire_permit(&self, key: &str, limit: u32, ttl: Duration)
169 -> Result<Option<Version>>;
170 async fn renew_permit(&self, key: &str, owner: Version, ttl: Duration) -> Result<bool>;
171 async fn release_permit(&self, key: &str, owner: Version) -> Result<bool>;
172 async fn acquire_lease(&self, key: &str, ttl: Duration) -> Result<Option<Version>> {
173 self.acquire_permit(key, 1, ttl).await
174 }
175 async fn publish(&self, topic: &str, payload: Vec<u8>) -> Result<()>;
176 async fn subscribe(&self, topic: &str) -> Result<Box<dyn Subscription>>;
179}
180
181#[derive(Clone, Debug)]
182pub struct Limits {
183 pub max_key_bytes: usize,
184 pub max_value_bytes: usize,
185 pub max_permits_per_key: u32,
186 pub notification_capacity: usize,
187}
188impl Default for Limits {
189 fn default() -> Self {
190 Self {
191 max_key_bytes: 1024,
192 max_value_bytes: 1024 * 1024,
193 max_permits_per_key: 10_000,
194 notification_capacity: 256,
195 }
196 }
197}
198#[cfg(any(
199 feature = "memory",
200 all(feature = "redis", not(target_arch = "wasm32"))
201))]
202impl Limits {
203 fn validate(&self) -> Result<()> {
204 if self.max_key_bytes == 0
205 || self.max_value_bytes == 0
206 || self.max_permits_per_key == 0
207 || self.notification_capacity == 0
208 || self.notification_capacity > 1_048_576
209 {
210 return Err(CacheError::Invalid(
211 "limits must be positive; notification capacity must be <= 1048576",
212 ));
213 }
214 Ok(())
215 }
216 fn key(&self, key: &str) -> Result<()> {
217 if key.is_empty() || key.len() > self.max_key_bytes {
218 return Err(CacheError::Invalid("empty or oversized key/topic"));
219 }
220 Ok(())
221 }
222 fn value(&self, value: &[u8]) -> Result<()> {
223 if value.len() > self.max_value_bytes {
224 return Err(CacheError::Invalid("oversized value/notification"));
225 }
226 Ok(())
227 }
228 fn permits(&self, limit: u32) -> Result<()> {
229 if limit == 0 || limit > self.max_permits_per_key {
230 return Err(CacheError::Invalid("invalid permit limit"));
231 }
232 Ok(())
233 }
234}
235
236#[cfg(any(
239 feature = "memory",
240 all(feature = "redis", not(target_arch = "wasm32"))
241))]
242const MAX_TTL_MS: u64 = (1_u64 << 52) - 1;
243#[cfg(any(
244 feature = "memory",
245 all(feature = "redis", not(target_arch = "wasm32"))
246))]
247fn ttl_ms(ttl: Duration) -> Result<u64> {
248 let ms = ttl.as_nanos().div_ceil(1_000_000);
249 if ms == 0 || ms > u128::from(MAX_TTL_MS) {
250 return Err(CacheError::Invalid(
251 "TTL must be positive and representable in exact milliseconds",
252 ));
253 }
254 Ok(ms as u64)
255}
256#[cfg(any(
257 feature = "memory",
258 all(feature = "redis", not(target_arch = "wasm32"))
259))]
260fn counter_args(amount: u64, limit: u64) -> Result<()> {
261 if amount == 0 || amount > i64::MAX as u64 || limit > i64::MAX as u64 {
262 return Err(CacheError::Invalid(
263 "counter amounts must be positive and values <= i64::MAX",
264 ));
265 }
266 Ok(())
267}