#![forbid(unsafe_code)]
use std::{fmt, time::Duration};
#[cfg(feature = "memory")]
mod memory;
#[cfg(feature = "memory")]
pub use memory::{MemoryCache, MemoryOptions};
#[cfg(all(feature = "redis", not(target_arch = "wasm32")))]
mod redis_backend;
#[cfg(all(feature = "redis", not(target_arch = "wasm32")))]
pub use redis_backend::{RedisCache, RedisOptions};
pub type Result<T> = std::result::Result<T, CacheError>;
#[derive(Debug, thiserror::Error)]
pub enum CacheError {
#[error("invalid cache operation: {0}")]
Invalid(&'static str),
#[error("cache capacity exhausted")]
Capacity,
#[error("counter would underflow")]
Underflow,
#[error("cache state is corrupt or belongs to another implementation")]
Corrupt,
#[error("cache mutex poisoned")]
Poisoned,
#[error("notification subscription closed; resubscribe and reload durable state")]
Closed,
#[error("cannot generate cache token: {0}")]
Entropy(String),
#[cfg(all(feature = "redis", not(target_arch = "wasm32")))]
#[error("Redis operation failed: {0}")]
Redis(#[from] redis::RedisError),
#[error("cache connection timed out")]
Timeout,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct Version([u8; 32]);
impl Version {
pub fn from_bytes(bytes: [u8; 32]) -> Self {
Self(bytes)
}
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
#[cfg(any(
feature = "memory",
all(feature = "redis", not(target_arch = "wasm32"))
))]
fn fresh() -> Result<Self> {
let mut bytes = [0; 32];
getrandom::fill(&mut bytes).map_err(|e| CacheError::Entropy(e.to_string()))?;
Ok(Self(bytes))
}
}
impl fmt::Debug for Version {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("Version(..)")
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct Entry {
pub value: Vec<u8>,
pub version: Version,
}
impl fmt::Debug for Entry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Entry")
.field("bytes", &self.value.len())
.field("version", &self.version)
.finish()
}
}
#[derive(Clone)]
pub struct Replacement {
pub value: Vec<u8>,
pub ttl: Duration,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CasOutcome {
Applied(Option<Version>),
Conflict,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Counter {
pub value: u64,
pub generation: Version,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum IncrementOutcome {
Applied(Counter),
Limited { current: u64 },
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Notification {
Message(Vec<u8>),
ResyncRequired,
}
#[async_trait::async_trait]
pub trait Subscription: Send {
async fn recv(&mut self) -> Result<Notification>;
}
#[async_trait::async_trait]
pub trait Cache: Send + Sync {
async fn get(&self, key: &str) -> Result<Option<Entry>>;
async fn get_many(&self, keys: &[String]) -> Result<Vec<Option<Entry>>> {
let mut entries = Vec::with_capacity(keys.len());
for key in keys {
entries.push(self.get(key).await?);
}
Ok(entries)
}
async fn put(&self, key: &str, value: Vec<u8>, ttl: Duration) -> Result<Version>;
async fn delete(&self, key: &str) -> Result<bool>;
async fn compare_exchange(
&self,
key: &str,
expected: Option<Version>,
replacement: Option<Replacement>,
) -> Result<CasOutcome>;
async fn counter(&self, key: &str) -> Result<Option<Counter>>;
async fn increment(
&self,
key: &str,
amount: u64,
limit: u64,
ttl: Duration,
) -> Result<IncrementOutcome>;
async fn decrement(
&self,
key: &str,
generation: Version,
amount: u64,
) -> Result<Option<Counter>>;
async fn acquire_permit(&self, key: &str, limit: u32, ttl: Duration)
-> Result<Option<Version>>;
async fn renew_permit(&self, key: &str, owner: Version, ttl: Duration) -> Result<bool>;
async fn release_permit(&self, key: &str, owner: Version) -> Result<bool>;
async fn acquire_lease(&self, key: &str, ttl: Duration) -> Result<Option<Version>> {
self.acquire_permit(key, 1, ttl).await
}
async fn publish(&self, topic: &str, payload: Vec<u8>) -> Result<()>;
async fn subscribe(&self, topic: &str) -> Result<Box<dyn Subscription>>;
}
#[derive(Clone, Debug)]
pub struct Limits {
pub max_key_bytes: usize,
pub max_value_bytes: usize,
pub max_permits_per_key: u32,
pub notification_capacity: usize,
}
impl Default for Limits {
fn default() -> Self {
Self {
max_key_bytes: 1024,
max_value_bytes: 1024 * 1024,
max_permits_per_key: 10_000,
notification_capacity: 256,
}
}
}
#[cfg(any(
feature = "memory",
all(feature = "redis", not(target_arch = "wasm32"))
))]
impl Limits {
fn validate(&self) -> Result<()> {
if self.max_key_bytes == 0
|| self.max_value_bytes == 0
|| self.max_permits_per_key == 0
|| self.notification_capacity == 0
|| self.notification_capacity > 1_048_576
{
return Err(CacheError::Invalid(
"limits must be positive; notification capacity must be <= 1048576",
));
}
Ok(())
}
fn key(&self, key: &str) -> Result<()> {
if key.is_empty() || key.len() > self.max_key_bytes {
return Err(CacheError::Invalid("empty or oversized key/topic"));
}
Ok(())
}
fn value(&self, value: &[u8]) -> Result<()> {
if value.len() > self.max_value_bytes {
return Err(CacheError::Invalid("oversized value/notification"));
}
Ok(())
}
fn permits(&self, limit: u32) -> Result<()> {
if limit == 0 || limit > self.max_permits_per_key {
return Err(CacheError::Invalid("invalid permit limit"));
}
Ok(())
}
}
#[cfg(any(
feature = "memory",
all(feature = "redis", not(target_arch = "wasm32"))
))]
const MAX_TTL_MS: u64 = (1_u64 << 52) - 1;
#[cfg(any(
feature = "memory",
all(feature = "redis", not(target_arch = "wasm32"))
))]
fn ttl_ms(ttl: Duration) -> Result<u64> {
let ms = ttl.as_nanos().div_ceil(1_000_000);
if ms == 0 || ms > u128::from(MAX_TTL_MS) {
return Err(CacheError::Invalid(
"TTL must be positive and representable in exact milliseconds",
));
}
Ok(ms as u64)
}
#[cfg(any(
feature = "memory",
all(feature = "redis", not(target_arch = "wasm32"))
))]
fn counter_args(amount: u64, limit: u64) -> Result<()> {
if amount == 0 || amount > i64::MAX as u64 || limit > i64::MAX as u64 {
return Err(CacheError::Invalid(
"counter amounts must be positive and values <= i64::MAX",
));
}
Ok(())
}