ratelock 0.1.0

Zero-dependency, zero-allocation, lock-free token bucket rate limiter for Rust (std + no_std).
Documentation
const NANOS_PER_SEC: u128 = 1_000_000_000;

#[inline]
pub(crate) fn refill_tokens(elapsed_ns: u64, refill_per_sec: u64) -> u64 {
    if elapsed_ns == 0 || refill_per_sec == 0 {
        return 0;
    }

    let produced = (elapsed_ns as u128).saturating_mul(refill_per_sec as u128) / NANOS_PER_SEC;
    produced.min(u64::MAX as u128) as u64
}

#[inline]
pub(crate) fn elapsed_for_tokens(tokens: u64, refill_per_sec: u64) -> u64 {
    if tokens == 0 || refill_per_sec == 0 {
        return 0;
    }

    let elapsed = (tokens as u128).saturating_mul(NANOS_PER_SEC) / (refill_per_sec as u128);
    elapsed.min(u64::MAX as u128) as u64
}

#[cfg(test)]
mod tests {
    use super::{elapsed_for_tokens, refill_tokens};

    #[test]
    fn refill_tokens_uses_floor_math() {
        assert_eq!(refill_tokens(0, 10), 0);
        assert_eq!(refill_tokens(10, 0), 0);
        assert_eq!(refill_tokens(500_000_000, 3), 1);
        assert_eq!(refill_tokens(999_999_999, 1), 0);
        assert_eq!(refill_tokens(1_000_000_000, 1), 1);
    }

    #[test]
    fn elapsed_for_tokens_matches_inverse_floor() {
        assert_eq!(elapsed_for_tokens(1, 10), 100_000_000);
        assert_eq!(elapsed_for_tokens(0, 10), 0);
        assert_eq!(elapsed_for_tokens(1, 0), 0);
    }
}