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}