Skip to main content

robinpath_modules/modules/
cache_mod.rs

1use robinpath::{RobinPath, Value};
2use std::collections::HashMap;
3use std::sync::Mutex;
4use std::time::{Duration, Instant};
5
6struct CacheEntry {
7    value: Value,
8    expires_at: Option<Instant>,
9}
10
11struct GlobalCache {
12    entries: HashMap<String, CacheEntry>,
13}
14
15impl GlobalCache {
16    fn new() -> Self {
17        Self {
18            entries: HashMap::new(),
19        }
20    }
21
22    fn cleanup(&mut self) {
23        let now = Instant::now();
24        self.entries.retain(|_, entry| {
25            entry.expires_at.map_or(true, |exp| exp > now)
26        });
27    }
28}
29
30static CACHE: std::sync::LazyLock<Mutex<GlobalCache>> =
31    std::sync::LazyLock::new(|| Mutex::new(GlobalCache::new()));
32
33pub fn register(rp: &mut RobinPath) {
34    rp.register_builtin("cache.set", |args, _| {
35        let key = args.first().map(|v| v.to_display_string()).unwrap_or_default();
36        let value = args.get(1).cloned().unwrap_or(Value::Null);
37        let ttl_secs = args.get(2).map(|v| v.to_number());
38        let expires_at = ttl_secs.map(|secs| Instant::now() + Duration::from_secs_f64(secs));
39        let mut cache = CACHE.lock().unwrap();
40        cache.entries.insert(key, CacheEntry { value, expires_at });
41        Ok(Value::Bool(true))
42    });
43
44    rp.register_builtin("cache.get", |args, _| {
45        let key = args.first().map(|v| v.to_display_string()).unwrap_or_default();
46        let default = args.get(1).cloned().unwrap_or(Value::Null);
47        let mut cache = CACHE.lock().unwrap();
48        let now = Instant::now();
49        if let Some(entry) = cache.entries.get(&key) {
50            if entry.expires_at.map_or(true, |exp| exp > now) {
51                return Ok(entry.value.clone());
52            }
53            // Expired — remove it
54            cache.entries.remove(&key);
55        }
56        Ok(default)
57    });
58
59    rp.register_builtin("cache.has", |args, _| {
60        let key = args.first().map(|v| v.to_display_string()).unwrap_or_default();
61        let mut cache = CACHE.lock().unwrap();
62        let now = Instant::now();
63        if let Some(entry) = cache.entries.get(&key) {
64            if entry.expires_at.map_or(true, |exp| exp > now) {
65                return Ok(Value::Bool(true));
66            }
67            cache.entries.remove(&key);
68        }
69        Ok(Value::Bool(false))
70    });
71
72    rp.register_builtin("cache.delete", |args, _| {
73        let key = args.first().map(|v| v.to_display_string()).unwrap_or_default();
74        let mut cache = CACHE.lock().unwrap();
75        let removed = cache.entries.remove(&key).is_some();
76        Ok(Value::Bool(removed))
77    });
78
79    rp.register_builtin("cache.clear", |_args, _| {
80        let mut cache = CACHE.lock().unwrap();
81        cache.entries.clear();
82        Ok(Value::Bool(true))
83    });
84
85    rp.register_builtin("cache.keys", |_args, _| {
86        let mut cache = CACHE.lock().unwrap();
87        cache.cleanup();
88        let keys: Vec<Value> = cache
89            .entries
90            .keys()
91            .map(|k| Value::String(k.clone()))
92            .collect();
93        Ok(Value::Array(keys))
94    });
95
96    rp.register_builtin("cache.values", |_args, _| {
97        let mut cache = CACHE.lock().unwrap();
98        cache.cleanup();
99        let values: Vec<Value> = cache
100            .entries
101            .values()
102            .map(|entry| entry.value.clone())
103            .collect();
104        Ok(Value::Array(values))
105    });
106
107    rp.register_builtin("cache.size", |_args, _| {
108        let mut cache = CACHE.lock().unwrap();
109        cache.cleanup();
110        Ok(Value::Number(cache.entries.len() as f64))
111    });
112
113    rp.register_builtin("cache.ttl", |args, _| {
114        let key = args.first().map(|v| v.to_display_string()).unwrap_or_default();
115        let cache = CACHE.lock().unwrap();
116        if let Some(entry) = cache.entries.get(&key) {
117            match entry.expires_at {
118                Some(exp) => {
119                    let now = Instant::now();
120                    if exp > now {
121                        Ok(Value::Number((exp - now).as_secs_f64()))
122                    } else {
123                        Ok(Value::Number(-1.0))
124                    }
125                }
126                None => Ok(Value::Number(-1.0)), // No TTL = never expires
127            }
128        } else {
129            Ok(Value::Null)
130        }
131    });
132
133    rp.register_builtin("cache.setMany", |args, _| {
134        let obj = args.first().cloned().unwrap_or(Value::Null);
135        let ttl_secs = args.get(1).map(|v| v.to_number());
136        let expires_at = ttl_secs.map(|secs| Instant::now() + Duration::from_secs_f64(secs));
137        if let Value::Object(map) = &obj {
138            let mut cache = CACHE.lock().unwrap();
139            for (key, value) in map {
140                cache.entries.insert(
141                    key.clone(),
142                    CacheEntry {
143                        value: value.clone(),
144                        expires_at,
145                    },
146                );
147            }
148        }
149        Ok(Value::Bool(true))
150    });
151
152    rp.register_builtin("cache.getMany", |args, _| {
153        let keys = args.first().cloned().unwrap_or(Value::Null);
154        if let Value::Array(arr) = &keys {
155            let mut cache = CACHE.lock().unwrap();
156            let now = Instant::now();
157            let mut result = indexmap::IndexMap::new();
158            for key_val in arr {
159                let key = key_val.to_display_string();
160                if let Some(entry) = cache.entries.get(&key) {
161                    if entry.expires_at.map_or(true, |exp| exp > now) {
162                        result.insert(key, entry.value.clone());
163                    } else {
164                        cache.entries.remove(&key);
165                        result.insert(key, Value::Null);
166                    }
167                } else {
168                    result.insert(key, Value::Null);
169                }
170            }
171            Ok(Value::Object(result))
172        } else {
173            Ok(Value::Object(indexmap::IndexMap::new()))
174        }
175    });
176
177    rp.register_builtin("cache.deleteMany", |args, _| {
178        let keys = args.first().cloned().unwrap_or(Value::Null);
179        if let Value::Array(arr) = &keys {
180            let mut cache = CACHE.lock().unwrap();
181            let mut count = 0;
182            for key_val in arr {
183                let key = key_val.to_display_string();
184                if cache.entries.remove(&key).is_some() {
185                    count += 1;
186                }
187            }
188            Ok(Value::Number(count as f64))
189        } else {
190            Ok(Value::Number(0.0))
191        }
192    });
193}