use std::sync::atomic::{AtomicU32, Ordering};
use crate::error::MemoryError;
pub struct QuestRateLimiter {
calls_this_quest: AtomicU32,
max_calls: u32,
}
impl QuestRateLimiter {
pub fn new(max_calls: u32) -> Self {
Self {
calls_this_quest: AtomicU32::new(0),
max_calls,
}
}
pub fn try_consume(&self) -> Result<(), MemoryError> {
let prev = self.calls_this_quest.fetch_add(1, Ordering::Relaxed);
if prev >= self.max_calls {
Err(MemoryError::RateLimit {
limit: self.max_calls,
})
} else {
Ok(())
}
}
pub fn reset_for_new_quest(&self) {
self.calls_this_quest.store(0, Ordering::Relaxed);
}
pub fn current_calls(&self) -> u32 {
self.calls_this_quest.load(Ordering::Relaxed)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rate_limiter_allows_20_calls() {
let rl = QuestRateLimiter::new(20);
for _ in 0..20 {
assert!(rl.try_consume().is_ok());
}
}
#[test]
fn rate_limiter_rejects_21st_call() {
let rl = QuestRateLimiter::new(20);
for _ in 0..20 {
let _ = rl.try_consume();
}
assert!(matches!(
rl.try_consume(),
Err(MemoryError::RateLimit { limit: 20 })
));
}
#[test]
fn rate_limiter_reset_allows_again() {
let rl = QuestRateLimiter::new(20);
for _ in 0..20 {
let _ = rl.try_consume();
}
rl.reset_for_new_quest();
assert!(rl.try_consume().is_ok());
}
}