clache 0.2.0

Small utilities for caching data
Documentation
//! Strongly-typed local cache.
//!
//! [`LocalCache`] is a cache that stores only a **single type**. This avoids frequent runtime
//! checks found in many [`GlobalCache`] operations, making it more performant in both speed and
//! memory.
//!
//! A [`LocalCache`] internally stores its state in an [`Arc`] and [`RwLock`], so it is cheap to
//! clone and can be easily shared with little performance penalty across threads, especially
//! when the cache hits.
//!
//! ## Examples
//!
//! ```rust
//! use std::sync::Arc;
//! use clache::prelude::*;
//!
//! let cache = LocalCache::<String>::new();
//!
//! // Slow first lookup
//! let data = cache.get_or_else("server context", || {
//!     Arc::new("data".to_string())
//! });
//!
//! // ...
//!
//! // Much faster lookup, since "server context" is already in the cache
//! let data = cache.get("server context");
//! ```
//!
//! If dynamic typing is needed, use [`GlobalCache`].
//!
//! [`GlobalCache`]: crate::GlobalCache

use std::{
    collections::HashMap,
    sync::{Arc, RwLock},
};

use crate::fused_rw::FusedRw;

type Item<T> = Arc<FusedRw<Option<Arc<T>>>>;

/// A cache that can store only a single type.
///
/// It is internally reference counted, so cloning it is cheap.
#[derive(Clone)]
pub struct LocalCache<T> {
    cache: Arc<RwLock<HashMap<String, Item<T>>>>,
}

impl<T> LocalCache<T> {
    /// Constructs a new cache with no entries.
    pub fn new() -> Self {
        Self {
            cache: Arc::new(RwLock::new(HashMap::new())),
        }
    }

    /// Checks if a value exists in the cache.
    pub fn is_cached(&self, path: impl AsRef<str>) -> bool {
        self.cache
            .read()
            .expect("Cache cannot poison")
            .get(path.as_ref())
            .is_some_and(|e| e.read().is_some())
    }

    /// Checks if a value exists in the cache. Waiting is done asynchronously.
    pub async fn is_cached_async(&self, path: impl AsRef<str>) -> bool {
        let Some(entry) = self
            .cache
            .read()
            .expect("Cache cannot poison")
            .get(path.as_ref())
            .cloned()
        else {
            return false;
        };

        entry.read_async().await.is_some()
    }

    /// Obtains a value from the cache, if it exists.
    pub fn get(&self, path: impl AsRef<str>) -> Option<Arc<T>> {
        let entry = {
            let guard = self.cache.read().expect("Cache cannot poison");
            let entry = guard.get(path.as_ref())?.clone();
            drop(guard);
            entry
        };

        entry.read().clone()
    }

    /// Obtains a value from the cache, if it exists. Waiting is done asynchronously.
    pub async fn get_async(&self, path: impl AsRef<str>) -> Option<Arc<T>> {
        let entry = {
            let guard = self.cache.read().expect("Cache cannot poison");
            let entry = guard.get(path.as_ref())?.clone();
            drop(guard);
            entry
        };

        entry.read_async().await.clone()
    }

    /// Obtains a value from the cache, or adds `default` if it does not exist.
    pub fn get_or(&self, path: impl AsRef<str>, default: Arc<T>) -> Arc<T> {
        if let Some(good) = self.get(path.as_ref()) {
            return good;
        }

        let entry = {
            let mut guard = self.cache.write().expect("Cache cannot poison");
            guard
                .entry(path.as_ref().to_string())
                .or_insert_with(|| Arc::new(FusedRw::new(None)))
                .clone()
        };

        if let Some(good) = entry.read().as_ref() {
            good.clone()
        } else {
            assert!(
                entry.write().replace(default.clone()).is_none(),
                "Cached value should be None"
            );
            default
        }
    }

    /// Obtains a value from the cache, or adds `default` if it does not exist. Waiting is done
    /// asynchronously.
    pub async fn get_or_async(&self, path: impl AsRef<str>, default: Arc<T>) -> Arc<T> {
        if let Some(good) = self.get_async(path.as_ref()).await {
            return good;
        }

        let entry = {
            let mut guard = self.cache.write().expect("Cache cannot poison");
            guard
                .entry(path.as_ref().to_string())
                .or_insert_with(|| Arc::new(FusedRw::new(None)))
                .clone()
        };

        if let Some(good) = entry.read_async().await.as_ref() {
            good.clone()
        } else {
            assert!(
                entry.write_async().await.replace(default.clone()).is_none(),
                "Cached value should be None"
            );
            default
        }
    }

    /// Obtains a value from the cache, or loads it from a closure if it does not exist.
    pub fn get_or_else<F: FnOnce() -> Arc<T>>(&self, path: impl AsRef<str>, f: F) -> Arc<T> {
        if let Some(good) = self.get(path.as_ref()) {
            return good;
        }

        let entry = {
            let mut guard = self.cache.write().expect("Cache cannot poison");
            guard
                .entry(path.as_ref().to_string())
                .or_insert_with(|| Arc::new(FusedRw::new(None)))
                .clone()
        };

        if let Some(good) = entry.read().as_ref() {
            good.clone()
        } else {
            let loaded = f();
            assert!(
                entry.write().replace(loaded.clone()).is_none(),
                "Cached value should be None"
            );
            loaded
        }
    }

    /// Similar to [`get_or_else`], but for *async* closures.
    ///
    /// Unlike the non-async variants, this **not** an atomic operation. Your closure may be run more
    /// than once, but only the first run will write to the cache.
    ///
    /// [`get_or_else`]: Self::get_or_else
    pub async fn get_or_else_async<F: AsyncFnOnce() -> Arc<T>>(
        &self,
        path: impl AsRef<str>,
        f: F,
    ) -> Arc<T> {
        if let Some(good) = self.get_async(path.as_ref()).await {
            return good;
        }

        let entry = {
            let mut guard = self.cache.write().expect("Cache cannot poison");
            guard
                .entry(path.as_ref().to_string())
                .or_insert_with(|| Arc::new(FusedRw::new(None)))
                .clone()
        };

        if let Some(good) = entry.read_async().await.as_ref() {
            good.clone()
        } else {
            let loaded = f().await;
            assert!(
                entry.write_async().await.replace(loaded.clone()).is_none(),
                "Cached value should be None"
            );
            loaded
        }
    }

    /// Similar to [`get_or_else`], but allows the closure to return [`None`], in which case the
    /// value is not added to the cache and `None` is returned.
    ///
    /// [`get_or_else`]: Self::get_or_else
    pub fn get_or_else_try<F>(&self, path: impl AsRef<str>, f: F) -> Option<Arc<T>>
    where
        F: FnOnce() -> Option<Arc<T>>,
    {
        if let Some(good) = self.get(path.as_ref()) {
            return Some(good);
        }

        let entry = {
            let mut guard = self.cache.write().expect("Cache cannot poison");
            guard
                .entry(path.as_ref().to_string())
                .or_insert_with(|| Arc::new(FusedRw::new(None)))
                .clone()
        };

        if let Some(good) = entry.read().as_ref() {
            Some(good.clone())
        } else {
            let loaded = f()?;
            assert!(
                entry.write().replace(loaded.clone()).is_none(),
                "Cached value should be None"
            );
            Some(loaded)
        }
    }

    /// Similar to [`get_or_else_async`], but allows the closure to return [`None`], in which case
    /// the value is not added to the cache and `None` is returned.
    ///
    /// Unlike the non-async variants, this **not** an atomic operation. Your closure may be run more
    /// than once, but only the first run will write to the cache.
    ///
    /// [`get_or_else_async`]: Self::get_or_else_async
    pub async fn get_or_else_try_async<F: AsyncFnOnce() -> Option<Arc<T>>>(
        &self,
        path: impl AsRef<str>,
        f: F,
    ) -> Option<Arc<T>> {
        if let Some(good) = self.get_async(path.as_ref()).await {
            return Some(good);
        }

        let entry = {
            let mut guard = self.cache.write().expect("Cache cannot poison");
            guard
                .entry(path.as_ref().to_string())
                .or_insert_with(|| Arc::new(FusedRw::new(None)))
                .clone()
        };

        if let Some(good) = entry.read_async().await.as_ref() {
            Some(good.clone())
        } else {
            let loaded = f().await?;
            assert!(
                entry.write_async().await.replace(loaded.clone()).is_none(),
                "Cached value should be None"
            );
            Some(loaded)
        }
    }

    /// Removes a file from the cache, if it exists.
    pub fn uncache(&self, path: impl AsRef<str>) {
        let mut guard = self.cache.write().expect("Cache cannot poison");
        guard.remove(path.as_ref());
    }

    /// Removes all entries from the cache.
    pub fn clear(&self) {
        let mut guard = self.cache.write().expect("Cache cannot poison");
        guard.clear();
    }
}

impl<T> Default for LocalCache<T> {
    fn default() -> Self {
        Self::new()
    }
}