zcache/
lib.rs

1use once_cell::sync::Lazy;
2use std::collections::HashMap;
3use std::future::Future;
4use std::time::{Duration, SystemTime, UNIX_EPOCH};
5use thiserror::Error;
6
7static mut ZCACHE_STORE: Lazy<HashMap<String, (u128, Box<ZEntry>)>> = Lazy::new(HashMap::new);
8
9#[derive(Error, Debug)]
10#[non_exhaustive]
11pub enum ZCacheError {
12    #[error("Failed fetching '{0}' zcache key")]
13    FetchError(String),
14}
15
16#[derive(Debug, Clone)]
17pub enum ZEntry {
18    Int(i64),
19    Float(f64),
20    Text(String),
21    Bool(bool),
22}
23
24pub struct ZCache {}
25
26impl ZCache {
27    pub async fn fetch<F, Fut>(
28        key: &str,
29        expires_in: Option<Duration>,
30        f: F,
31    ) -> Result<ZEntry, ZCacheError>
32    where
33        F: FnOnce() -> Fut,
34        Fut: Future<Output = Option<ZEntry>>,
35    {
36        match Self::read(key) {
37            Some(value) => Ok(value),
38            None => match f().await {
39                Some(value) => {
40                    Self::write(key, value.clone(), expires_in);
41                    Ok(value)
42                }
43                None => Err(ZCacheError::FetchError(key.to_string())),
44            },
45        }
46    }
47
48    pub fn read(key: &str) -> Option<ZEntry> {
49        let key = key.to_string();
50        let result = unsafe { ZCACHE_STORE.get(&key) };
51        match result {
52            Some((valid_until, value)) => {
53                let valid_until = *valid_until;
54                if valid_until == 0 || valid_until > now_in_millis() {
55                    Some(*value.clone())
56                } else {
57                    None
58                }
59            }
60            None => None,
61        }
62    }
63
64    pub fn write(key: &str, value: ZEntry, expires_in: Option<Duration>) {
65        let key = key.to_string();
66
67        let valid_until: u128 = match expires_in {
68            Some(duration) => now_in_millis() + duration.as_millis(),
69            None => 0,
70        };
71        unsafe {
72            ZCACHE_STORE.insert(key, (valid_until, Box::new(value)));
73        }
74    }
75
76    pub fn clear() {
77        unsafe {
78            ZCACHE_STORE = Lazy::new(HashMap::new);
79        }
80    }
81}
82
83fn now_in_millis() -> u128 {
84    SystemTime::now()
85        .duration_since(UNIX_EPOCH)
86        .expect("Time went backwards!")
87        .as_millis()
88}
89
90#[cfg(test)]
91mod tests {
92    use std::ops::Mul;
93    use std::thread::sleep;
94
95    use super::*;
96
97    #[tokio::test]
98    async fn read_write_works() {
99        ZCache::clear();
100        let cacheable = ZEntry::Int(1);
101        let one_second = Duration::from_secs(1);
102        ZCache::write("key1", cacheable, Some(one_second));
103        let result = ZCache::read("key1");
104
105        match result {
106            Some(ZEntry::Int(value)) => assert_eq!(value, 1),
107            _ => panic!("Unexpected value"),
108        }
109
110        sleep(one_second.mul(2));
111        let result = ZCache::read("key1");
112
113        if result.is_some() {
114            panic!("Entry should be expired!");
115        }
116
117        let cacheable = ZEntry::Text("cached text".to_string());
118        ZCache::write("key2", cacheable, None);
119        sleep(one_second.mul(2));
120        let result = ZCache::read("key2");
121        match result {
122            Some(ZEntry::Text(value)) => assert_eq!(value, "cached text".to_string()),
123            _ => panic!("Unexpected value"),
124        }
125    }
126
127    #[tokio::test]
128    async fn fetch_works() {
129        ZCache::clear();
130        let cacheable = ZEntry::Int(1);
131        let result = ZCache::fetch("key1", None, || async { Some(cacheable.clone()) }).await;
132
133        match result {
134            Ok(ZEntry::Int(value)) => assert_eq!(value, 1),
135            _ => panic!("Unexpected value"),
136        }
137    }
138
139    #[tokio::test]
140    async fn fetch_expiry_works() -> Result<(), ZCacheError> {
141        ZCache::clear();
142        let cacheable = ZEntry::Int(1);
143        let one_second = Duration::from_secs(1);
144        let result = ZCache::fetch("key1", Some(one_second), || async {
145            Some(cacheable.clone())
146        })
147        .await;
148        match result {
149            Ok(ZEntry::Int(value)) => assert_eq!(value, 1),
150            _ => panic!("Unexpected value"),
151        }
152
153        match ZCache::fetch("key1", Some(one_second), || async {
154            Some(cacheable.clone())
155        })
156        .await?
157        {
158            ZEntry::Int(value) => value,
159            _ => panic!("Unexpected type"),
160        };
161
162        sleep(one_second.mul(2));
163        let result = ZCache::read("key1");
164
165        if result.is_some() {
166            panic!("Entry should be expired!");
167        }
168        Ok(())
169    }
170}