1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
use once_cell::sync::Lazy;
use std::collections::HashMap;
use std::future::Future;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use thiserror::Error;

static mut ZCACHE_STORE: Lazy<HashMap<String, (u128, Box<ZEntry>)>> = Lazy::new(HashMap::new);

#[derive(Error, Debug)]
#[non_exhaustive]
pub enum ZCacheError {
    #[error("Failed fetching '{0}' zcache key")]
    FetchError(String),
}

#[derive(Debug, Clone)]
pub enum ZEntry {
    Int(i64),
    Float(f64),
    Text(String),
    Bool(bool),
}

pub struct ZCache {}

impl ZCache {
    pub async fn fetch<F, Fut>(
        key: &str,
        expires_in: Option<Duration>,
        f: F,
    ) -> Result<ZEntry, ZCacheError>
    where
        F: FnOnce() -> Fut,
        Fut: Future<Output = Option<ZEntry>>,
    {
        match Self::read(key) {
            Some(value) => Ok(value),
            None => match f().await {
                Some(value) => {
                    Self::write(key, value.clone(), expires_in);
                    Ok(value)
                }
                None => Err(ZCacheError::FetchError(key.to_string())),
            },
        }
    }

    pub fn read(key: &str) -> Option<ZEntry> {
        let key = key.to_string();
        let result = unsafe { ZCACHE_STORE.get(&key) };
        match result {
            Some((valid_until, value)) => {
                let valid_until = *valid_until;
                if valid_until == 0 || valid_until > now_in_millis() {
                    Some(*value.clone())
                } else {
                    None
                }
            }
            None => None,
        }
    }

    pub fn write(key: &str, value: ZEntry, expires_in: Option<Duration>) {
        let key = key.to_string();

        let valid_until: u128 = match expires_in {
            Some(duration) => now_in_millis() + duration.as_millis(),
            None => 0,
        };
        unsafe {
            ZCACHE_STORE.insert(key, (valid_until, Box::new(value)));
        }
    }
}

fn now_in_millis() -> u128 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("Time went backwards!")
        .as_millis()
}

#[cfg(test)]
mod tests {
    use std::ops::Mul;
    use std::thread::sleep;

    use super::*;

    #[tokio::test]
    async fn read_write_works() {
        let cacheable = ZEntry::Int(1);
        let one_second = Duration::from_secs(1);
        ZCache::write("key1", cacheable, Some(one_second));
        let result = ZCache::read("key1");

        match result {
            Some(ZEntry::Int(value)) => assert_eq!(value, 1),
            _ => panic!("Unexpected value"),
        }

        sleep(one_second.mul(2));
        let result = ZCache::read("key1");

        if result.is_some() {
            panic!("Entry should be expired!");
        }

        let cacheable = ZEntry::Text("cached text".to_string());
        ZCache::write("key2", cacheable, None);
        sleep(one_second.mul(2));
        let result = ZCache::read("key2");
        match result {
            Some(ZEntry::Text(value)) => assert_eq!(value, "cached text".to_string()),
            _ => panic!("Unexpected value"),
        }
    }

    #[tokio::test]
    async fn fetch_works() {
        let cacheable = ZEntry::Int(1);
        let result = ZCache::fetch("key1", None, || async { Some(cacheable.clone()) }).await;

        match result {
            Ok(ZEntry::Int(value)) => assert_eq!(value, 1),
            _ => panic!("Unexpected value"),
        }
    }

    #[tokio::test]
    async fn fetch_expiry_works() {
        let cacheable = ZEntry::Int(1);
        let one_second = Duration::from_secs(1);
        let result = ZCache::fetch("key1", Some(one_second), || async {
            Some(cacheable.clone())
        })
        .await;
        match result {
            Ok(ZEntry::Int(value)) => assert_eq!(value, 1),
            _ => panic!("Unexpected value"),
        }

        sleep(one_second.mul(2));
        let result = ZCache::read("key1");

        if result.is_some() {
            panic!("Entry should be expired!");
        }
    }
}