templar_curator_primitives/policy/cooldown/
mod.rs

1//! Cooldown tracking for rate-limiting operations.
2//!
3//! This module provides a reusable [`Cooldown`] type for tracking time-based
4//! rate limits. It's used by both [`RefreshPlan`](super::refresh_plan::RefreshPlan)
5//! and [`MarketLock`](super::market_lock::MarketLock) for expiry semantics.
6
7use templar_vault_kernel::{TimeGate, TimestampNs};
8
9/// Tracks cooldown state for rate-limited operations.
10///
11/// A cooldown enforces a minimum interval between operations. It tracks
12/// when the last operation occurred and the required interval before
13/// the next operation is allowed.
14#[templar_vault_macros::vault_derive(borsh, serde, postcard)]
15#[derive(Clone, Default, PartialEq, Eq)]
16pub struct Cooldown {
17    /// Timestamp of the last operation (nanoseconds), if any.
18    pub last_event_ns: Option<u64>,
19    /// Required interval between operations (nanoseconds).
20    /// Zero means no cooldown (always ready).
21    pub interval_ns: u64,
22}
23
24impl Cooldown {
25    fn gate(&self) -> TimeGate {
26        if self.is_unlimited() {
27            return TimeGate::ready_now();
28        }
29
30        match self.last_event_ns {
31            Some(last) => TimeGate::schedule_from(TimestampNs(last), TimestampNs(self.interval_ns)),
32            None => TimeGate::ready_now(),
33        }
34    }
35
36    #[must_use]
37    pub fn new(interval_ns: u64) -> Self {
38        Self {
39            last_event_ns: None,
40            interval_ns,
41        }
42    }
43
44    #[must_use]
45    pub fn unlimited() -> Self {
46        Self {
47            last_event_ns: None,
48            interval_ns: 0,
49        }
50    }
51
52    #[must_use]
53    pub fn is_unlimited(&self) -> bool {
54        self.interval_ns == 0
55    }
56
57    /// Check if an operation is allowed at the given timestamp.
58    ///
59    /// Returns `true` if:
60    /// - No cooldown is configured (interval_ns == 0), or
61    /// - No previous operation has occurred, or
62    /// - Sufficient time has elapsed since the last operation
63    #[must_use]
64    pub fn is_ready(&self, current_ns: u64) -> bool {
65        self.gate().is_ready(TimestampNs(current_ns))
66    }
67
68    /// Check cooldown and return an error if not ready.
69    pub fn check(&self, current_ns: u64) -> Result<(), CooldownError> {
70        if self.is_ready(current_ns) {
71            Ok(())
72        } else {
73            Err(CooldownError::OnCooldown {
74                last_event_ns: self.last_event_ns,
75                interval_ns: self.interval_ns,
76                current_ns,
77            })
78        }
79    }
80
81    #[must_use]
82    pub fn record(&self, timestamp_ns: u64) -> Self {
83        Self {
84            last_event_ns: Some(timestamp_ns),
85            interval_ns: self.interval_ns,
86        }
87    }
88
89    #[must_use]
90    pub fn ready_at(&self) -> Option<u64> {
91        self.gate().ready_at_ns().map(Into::into)
92    }
93
94    #[must_use]
95    pub fn remaining(&self, current_ns: u64) -> u64 {
96        self.gate().remaining(TimestampNs(current_ns)).into()
97    }
98}
99
100/// Errors that can occur during cooldown checks.
101#[templar_vault_macros::vault_derive]
102#[derive(Clone, PartialEq, Eq)]
103pub enum CooldownError {
104    /// Operation is still on cooldown.
105    OnCooldown {
106        last_event_ns: Option<u64>,
107        interval_ns: u64,
108        current_ns: u64,
109    },
110}