use std::collections::HashMap;
use std::sync::{Mutex, OnceLock};
use std::time::{Duration, Instant};
struct Entry {
body: String,
stored: Instant,
}
fn cache() -> &'static Mutex<HashMap<String, Entry>> {
static CACHE: OnceLock<Mutex<HashMap<String, Entry>>> = OnceLock::new();
CACHE.get_or_init(|| Mutex::new(HashMap::new()))
}
const TTL: Duration = Duration::from_secs(120);
const MAX_ENTRIES: usize = 500;
pub fn cache_get(key: &str) -> Option<String> {
let map = cache().lock().ok()?;
let e = map.get(key)?;
(e.stored.elapsed() < TTL).then(|| e.body.clone())
}
pub fn cache_put(key: String, body: String) {
let Ok(mut map) = cache().lock() else { return };
map.retain(|_, e| e.stored.elapsed() < TTL);
if map.len() >= MAX_ENTRIES {
if let Some(oldest) = map
.iter()
.min_by_key(|(_, e)| e.stored)
.map(|(k, _)| k.clone())
{
map.remove(&oldest);
}
}
map.insert(
key,
Entry {
body,
stored: Instant::now(),
},
);
}
fn hits() -> &'static Mutex<HashMap<String, Vec<Instant>>> {
static HITS: OnceLock<Mutex<HashMap<String, Vec<Instant>>>> = OnceLock::new();
HITS.get_or_init(|| Mutex::new(HashMap::new()))
}
const WINDOW: Duration = Duration::from_secs(60);
const MAX_PER_WINDOW: usize = 40;
pub fn rate_check(client: &str) -> Result<(), u64> {
let Ok(mut map) = hits().lock() else {
return Ok(());
};
map.retain(|_, v| v.last().is_some_and(|t| t.elapsed() < WINDOW));
let now = Instant::now();
let v = map.entry(client.to_string()).or_default();
v.retain(|t| t.elapsed() < WINDOW);
if v.len() >= MAX_PER_WINDOW {
let oldest = v.first().copied().unwrap_or(now);
let wait = WINDOW.saturating_sub(oldest.elapsed()).as_secs().max(1);
return Err(wait);
}
v.push(now);
Ok(())
}