use std::sync::{Mutex, OnceLock};
use std::time::{Duration, Instant};
pub const DEFAULT_COOLDOWN: Duration = Duration::from_secs(120);
pub fn is_rate_limited_error(msg: &str) -> bool {
let m = msg.to_lowercase();
m.contains("api rate limit")
|| m.contains("secondary rate limit")
|| m.contains("rate limit exceeded")
|| m.contains("graphql: api rate limit")
}
fn cooldown_until() -> &'static Mutex<Option<Instant>> {
static COOLDOWN: OnceLock<Mutex<Option<Instant>>> = OnceLock::new();
COOLDOWN.get_or_init(|| Mutex::new(None))
}
pub fn in_cooldown_now() -> bool {
let Ok(mut guard) = cooldown_until().lock() else {
return false;
};
if let Some(until) = *guard {
if Instant::now() < until {
return true;
}
*guard = None;
}
false
}
pub fn enter_cooldown() {
enter_cooldown_for(DEFAULT_COOLDOWN);
}
pub fn enter_cooldown_for(duration: Duration) {
let Ok(mut guard) = cooldown_until().lock() else {
return;
};
let Some(new_until) = Instant::now().checked_add(duration) else {
return;
};
match *guard {
Some(existing) if existing >= new_until => {}
_ => *guard = Some(new_until),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread::sleep;
#[test]
fn detects_rate_limit_messages() {
assert!(is_rate_limited_error("API rate limit exceeded"));
assert!(is_rate_limited_error(
"You have exceeded a secondary rate limit"
));
assert!(is_rate_limited_error("GraphQL: API rate limit exceeded"));
assert!(is_rate_limited_error("rate limit exceeded for app"));
assert!(!is_rate_limited_error("404 not found"));
assert!(!is_rate_limited_error(""));
}
#[test]
fn cooldown_lifecycle() {
if let Ok(mut g) = cooldown_until().lock() {
*g = None;
}
enter_cooldown_for(Duration::from_millis(30));
assert!(in_cooldown_now());
sleep(Duration::from_millis(60));
assert!(!in_cooldown_now());
enter_cooldown_for(Duration::from_secs(60));
let first = *cooldown_until().lock().unwrap();
enter_cooldown_for(Duration::from_millis(10));
let after_short = *cooldown_until().lock().unwrap();
assert_eq!(
first, after_short,
"short cooldown must not shorten longer one"
);
if let Ok(mut g) = cooldown_until().lock() {
*g = None;
}
}
#[test]
fn enter_cooldown_for_ignores_overflowing_duration() {
if let Ok(mut g) = cooldown_until().lock() {
*g = None;
}
enter_cooldown_for(Duration::MAX);
assert!(
!in_cooldown_now(),
"overflowing duration must not activate cooldown"
);
}
}