tinycache/
cache.rs

1use super::store;
2use crate::store::StoreErr;
3use serde::{Deserialize, Serialize};
4use std::{fmt::Debug, time::Duration};
5#[cfg(feature = "tracing")]
6use tracing::{debug, warn};
7
8/// cache with optional max age configuration
9#[derive(Clone)]
10pub struct TinyRef {
11    cache_name: String,
12    max_cache_age: Option<Duration>,
13    ignore_cache: bool,
14}
15
16const CACHE_NAME: &str = ".tiny_cache";
17
18impl TinyRef {
19    pub fn with_name(cache_name: impl ToString) -> Self {
20        Self {
21            cache_name: cache_name.to_string(),
22            max_cache_age: None,
23            ignore_cache: false,
24        }
25    }
26    pub fn new() -> Self {
27        Self {
28            cache_name: CACHE_NAME.to_string(),
29            max_cache_age: None,
30            ignore_cache: false,
31        }
32    }
33    /// define max age of the cache
34    pub fn max_age(mut self, max_duration: Duration) -> Self {
35        self.max_cache_age = Some(max_duration);
36        self
37    }
38
39    pub fn no_cache(&self) -> Self {
40        let mut inner = self.clone();
41        inner.ignore_cache = true;
42        inner
43    }
44}
45
46impl TinyRef {
47    /// get cached item from cache or use fun to resolve the item
48    pub fn get_cached_or_fetch<T, Fun>(&self, item_key: impl ToString, fetch_with: Fun) -> T
49    where
50        for<'a> T: Deserialize<'a> + Serialize + Debug,
51        Fun: FnOnce() -> T,
52    {
53        let k = item_key.to_string();
54        if !self.ignore_cache {
55            if let Some(res) = self.read(k.clone()) {
56                return res;
57            }
58        } else {
59            #[cfg(feature = "tracing")]
60            debug!("Ignoring cache on {:?}", k);
61        }
62        let item_from_fut = fetch_with();
63        self.write(item_key, &item_from_fut);
64        item_from_fut
65    }
66    pub fn write<T>(&self, item_key: impl ToString, v: &T)
67    where
68        for<'a> T: Deserialize<'a> + Serialize + Debug,
69    {
70        let item_key = item_key.to_string();
71        if let Err(e) = store::write(self.cache_name.clone(), item_key.clone(), v) {
72            #[cfg(feature = "tracing")]
73            warn!(
74                "failed to write to cache `{}` -> `{}` {:?}",
75                item_key, self.cache_name, e
76            );
77        } else {
78            #[cfg(feature = "tracing")]
79            debug!("SAVE `{}` -> `{}`", item_key, self.cache_name);
80        }
81    }
82    pub fn item_age(&self, item_key: impl ToString) -> Option<Duration> {
83        store::item_age(self.cache_name.clone(), item_key.to_string()).ok()
84    }
85    pub fn read<T>(&self, item_key: impl ToString) -> Option<T>
86    where
87        for<'a> T: Deserialize<'a> + Serialize + Debug,
88    {
89        let item_key = item_key.to_string();
90        // if max age for the cache is defined
91        if let Some(max_age) = self.max_cache_age {
92            if let Some(age) = self.item_age(item_key.clone()) {
93                if age > max_age {
94                    #[cfg(feature = "tracing")]
95                    debug!("CACHE `{}` -> TOO OLD. AGE: {:?}", item_key, age);
96                    self.invalidate(item_key);
97                    return None;
98                }
99            }
100        }
101        match store::read::<T>(self.cache_name.clone(), item_key.clone()) {
102            Ok(v) => Some(v),
103            Err(e) => {
104                if let StoreErr::Ser(e) = e {
105                    // invalidate the cache
106                    #[cfg(feature = "tracing")]
107                    warn!(
108                        "Failed to deserialize {:?}, invalidating cache -> `{}`",
109                        e, item_key
110                    );
111                    self.invalidate(item_key);
112                }
113                None
114            }
115        }
116    }
117    pub fn invalidate(&self, item_key: impl ToString) {
118        if let Err(e) = store::remove(self.cache_name.clone(), item_key.to_string()) {
119            #[cfg(feature = "tracing")]
120            warn!("Failed to invalidate cache {:?}", e);
121        }
122    }
123}
124
125#[cfg(test)]
126mod test {
127    use super::*;
128
129    #[test]
130    fn test_cache() {
131        let tiny = TinyRef::new();
132        #[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
133        pub struct TestStruct {
134            a: String,
135        }
136        let test_struct = TestStruct { a: "hello".into() };
137        let key = "testval";
138
139        // write read
140        tiny.write(key, &test_struct);
141        let stored_struct: TestStruct = tiny.read(key).unwrap();
142        assert_eq!(stored_struct, test_struct);
143
144        // test item age
145        std::thread::sleep(Duration::from_millis(100));
146        let age = tiny.item_age(key).unwrap();
147        println!("age {:?}", age);
148        assert!(age > Duration::from_millis(50));
149
150        // invalidate
151        tiny.invalidate(key);
152        assert_eq!(tiny.read::<TestStruct>(key), None);
153    }
154}