rskit-cache 0.2.0-alpha.3

Cache abstraction with explicit store registration and local adapters
Documentation
use std::collections::BTreeMap;
use std::sync::Arc;
use std::time::{Duration, Instant};

use parking_lot::Mutex;
use rskit_errors::{AppError, AppResult, ErrorCode};

use crate::registry::CacheStore;

#[derive(Clone)]
pub(crate) struct Entry {
    value: String,
    expires_at: Option<Instant>,
}

impl Entry {
    fn is_expired(&self, now: Instant) -> bool {
        self.expires_at.is_some_and(|expires_at| expires_at <= now)
    }
}

/// Lean in-process cache store adapter for local development and tests.
pub struct MemoryCache {
    prefix: Option<String>,
    max_entries: Option<usize>,
    pub(crate) entries: Mutex<BTreeMap<String, Entry>>,
    clock: Arc<dyn Fn() -> Instant + Send + Sync>,
}

impl MemoryCache {
    /// Create an empty in-memory cache.
    #[must_use]
    pub fn new(prefix: Option<String>, max_entries: Option<usize>) -> Self {
        Self::new_with_clock(prefix, max_entries, Instant::now)
    }

    /// Create an in-memory cache with an injected clock.
    ///
    /// This is primarily useful for deterministic tests
    /// and simulations that need to advance cache expiry without sleeping.
    #[must_use]
    pub fn new_with_clock(
        prefix: Option<String>,
        max_entries: Option<usize>,
        clock: impl Fn() -> Instant + Send + Sync + 'static,
    ) -> Self {
        Self {
            prefix,
            max_entries: max_entries.filter(|entries| *entries > 0),
            entries: Mutex::new(BTreeMap::new()),
            clock: Arc::new(clock),
        }
    }

    fn now(&self) -> Instant {
        (self.clock)()
    }

    fn key(&self, key: &str) -> String {
        self.prefix
            .as_ref()
            .map_or_else(|| key.to_owned(), |prefix| format!("{prefix}:{key}"))
    }

    fn prune_expired(entries: &mut BTreeMap<String, Entry>, now: Instant) {
        entries.retain(|_, entry| !entry.is_expired(now));
    }

    fn expires_at(now: Instant, ttl: Option<Duration>) -> AppResult<Option<Instant>> {
        let Some(duration) = ttl else {
            return Ok(None);
        };
        now.checked_add(duration).map(Some).ok_or_else(|| {
            AppError::new(
                ErrorCode::InvalidInput,
                "cache TTL is too large to represent safely",
            )
        })
    }
}

#[async_trait::async_trait]
impl CacheStore for MemoryCache {
    async fn get(&self, key: &str) -> AppResult<Option<String>> {
        let mut entries = self.entries.lock();
        Self::prune_expired(&mut entries, self.now());
        let full_key = self.key(key);
        Ok(entries.get(&full_key).map(|entry| entry.value.clone()))
    }

    async fn set(&self, key: &str, val: &str, ttl: Option<Duration>) -> AppResult<()> {
        let mut entries = self.entries.lock();
        let now = self.now();
        Self::prune_expired(&mut entries, now);
        let key = self.key(key);

        if ttl.is_some_and(|ttl| ttl.is_zero()) {
            return Err(AppError::new(
                ErrorCode::InvalidInput,
                "cache TTL must be greater than zero",
            ));
        }
        let expires_at = Self::expires_at(now, ttl)?;

        if let Some(max_entries) = self.max_entries
            && entries.len() >= max_entries
            && !entries.contains_key(&key)
            && let Some(first_key) = entries.keys().next().cloned()
        {
            entries.remove(&first_key);
        }

        entries.insert(
            key,
            Entry {
                value: val.to_owned(),
                expires_at,
            },
        );
        Ok(())
    }

    async fn delete(&self, key: &str) -> AppResult<bool> {
        let mut entries = self.entries.lock();
        Self::prune_expired(&mut entries, self.now());
        let key = self.key(key);
        Ok(entries.remove(&key).is_some())
    }

    async fn exists(&self, key: &str) -> AppResult<bool> {
        self.get(key).await.map(|value| value.is_some())
    }
}

impl Default for MemoryCache {
    fn default() -> Self {
        Self::new(None, None)
    }
}