#![forbid(unsafe_code)]
#![warn(missing_docs)]
pub mod cache;
pub mod error;
pub mod options;
pub mod metadata;
pub mod validation;
pub use cache::Cache;
#[cfg(feature = "moka")]
pub use cache::MokaCache;
#[cfg(feature = "redis")]
pub use cache::RedisCache;
pub use error::{CachifiedError, Result};
pub use options::{CachifiedOptions, CachifiedOptionsBuilder};
pub use metadata::{CacheMetadata, CacheEntry};
pub use validation::CheckValue;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use std::future::Future;
pub async fn cachified<T, F, Fut, C>(options: CachifiedOptions<T, F, C>) -> Result<T>
where
T: Clone + Send + Sync + 'static,
F: Fn() -> Fut + Send + Sync,
Fut: Future<Output = Result<T>> + Send + 'static,
C: Cache<T> + Clone + 'static,
{
let CachifiedOptions {
cache,
key,
ttl,
stale_while_revalidate,
force_fresh,
fallback_to_cache,
check_value,
get_fresh_value,
} = options;
let now = current_time();
if !force_fresh {
if let Some(entry) = cache.get(&key).await {
if !is_expired(&entry.metadata, now) {
if let Some(ref validator) = check_value {
if validator.check(&entry.value).is_ok() {
return Ok(entry.value);
}
} else {
return Ok(entry.value);
}
} else if let Some(swr_duration) = stale_while_revalidate {
let stale_until = entry.metadata.created_time +
entry.metadata.ttl.unwrap_or(Duration::ZERO) + swr_duration;
if now < stale_until {
let cache_clone = cache.clone();
let key_clone = key.clone();
let fresh_value_future = get_fresh_value();
tokio::spawn(async move {
if let Ok(fresh_value) = fresh_value_future.await {
let metadata = CacheMetadata {
created_time: current_time(),
ttl,
};
let entry = CacheEntry {
value: fresh_value,
metadata,
};
let _ = cache_clone.set(&key_clone, entry).await;
}
});
if let Some(ref validator) = check_value {
if validator.check(&entry.value).is_ok() {
return Ok(entry.value);
}
} else {
return Ok(entry.value);
}
}
}
}
}
match get_fresh_value().await {
Ok(fresh_value) => {
if let Some(ref validator) = check_value {
validator.check(&fresh_value)?;
}
if let Some(ttl_duration) = ttl {
if ttl_duration > Duration::ZERO {
let metadata = CacheMetadata {
created_time: now,
ttl,
};
let entry = CacheEntry {
value: fresh_value.clone(),
metadata,
};
if cache.set(&key, entry).await.is_err() {
}
}
}
Ok(fresh_value)
}
Err(e) => {
if fallback_to_cache {
if let Some(entry) = cache.get(&key).await {
if let Some(ref validator) = check_value {
if validator.check(&entry.value).is_ok() {
return Ok(entry.value);
}
} else {
return Ok(entry.value);
}
}
}
Err(e)
}
}
}
fn current_time() -> Duration {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or(Duration::ZERO)
}
fn is_expired(metadata: &CacheMetadata, now: Duration) -> bool {
if let Some(ttl) = metadata.ttl {
now >= metadata.created_time + ttl
} else {
false }
}
pub struct SoftPurgeOptions {
pub key: String,
pub stale_while_revalidate: Option<Duration>,
}
impl SoftPurgeOptions {
pub fn new(key: impl Into<String>) -> Self {
Self {
key: key.into(),
stale_while_revalidate: None,
}
}
pub fn stale_while_revalidate(mut self, duration: Duration) -> Self {
self.stale_while_revalidate = Some(duration);
self
}
}
pub async fn soft_purge<T, C>(cache: &C, options: SoftPurgeOptions) -> Result<()>
where
T: Clone + Send + Sync + 'static,
C: Cache<T>,
{
let SoftPurgeOptions {
key,
stale_while_revalidate: _,
} = options;
if let Some(mut entry) = cache.get(&key).await {
let now = current_time();
entry.metadata.ttl = Some(Duration::ZERO);
if entry.metadata.is_expired(now) {
entry.metadata.created_time = now;
}
cache.set(&key, entry).await?;
}
Ok(())
}