templar_curator_primitives/policy/refresh_plan/
mod.rs1use alloc::{collections::BTreeSet, vec::Vec};
4use templar_vault_kernel::TargetId;
5
6use super::cooldown::Cooldown;
7use super::target_set::find_first_duplicate;
8
9#[templar_vault_macros::vault_derive(borsh, serde, postcard)]
11#[derive(Clone, Default)]
12pub struct RefreshPlan {
13 pub targets: Vec<TargetId>,
15 pub cooldown: Cooldown,
17}
18
19impl RefreshPlan {
20 #[must_use]
21 pub fn new(targets: Vec<TargetId>) -> Self {
22 Self {
23 targets,
24 cooldown: Cooldown::unlimited(),
25 }
26 }
27
28 #[must_use]
29 pub fn empty() -> Self {
30 Self::default()
31 }
32
33 #[must_use]
34 pub fn with_cooldown(mut self, cooldown_ns: u64) -> Self {
35 self.cooldown = Cooldown::new(cooldown_ns);
36 self
37 }
38
39 #[must_use]
40 pub fn with_last_refresh(mut self, last_refresh_ns: u64) -> Self {
41 self.cooldown = self.cooldown.record(last_refresh_ns);
42 self
43 }
44
45 #[must_use]
46 pub fn is_empty(&self) -> bool {
47 self.targets.is_empty()
48 }
49
50 #[must_use]
51 pub fn len(&self) -> usize {
52 self.targets.len()
53 }
54
55 #[must_use]
56 pub fn is_ready(&self, current_ns: u64) -> bool {
57 self.cooldown.is_ready(current_ns)
58 }
59
60 pub fn validate(&self) -> Result<(), RefreshPlanError> {
66 if self.is_empty() {
67 return Err(RefreshPlanError::EmptyPlan);
68 }
69
70 if let Some(dup) = find_first_duplicate(&self.targets) {
71 return Err(RefreshPlanError::DuplicateTarget { target_id: dup });
72 }
73
74 Ok(())
75 }
76
77 pub fn check_cooldown(&self, current_ns: u64) -> Result<(), RefreshPlanError> {
79 self.cooldown.check(current_ns).map_err(|e| match e {
80 super::cooldown::CooldownError::OnCooldown {
81 last_event_ns,
82 interval_ns,
83 current_ns,
84 } => RefreshPlanError::OnCooldown {
85 last_refresh_ns: last_event_ns,
86 cooldown_ns: interval_ns,
87 current_ns,
88 },
89 })
90 }
91
92 #[must_use]
94 pub fn record_completion(&self, timestamp_ns: u64) -> Self {
95 Self {
96 targets: self.targets.clone(),
97 cooldown: self.cooldown.record(timestamp_ns),
98 }
99 }
100
101 #[must_use]
102 pub fn to_target_list(&self) -> Vec<TargetId> {
103 self.targets.clone()
104 }
105
106 #[must_use]
107 pub fn cooldown_ns(&self) -> u64 {
108 self.cooldown.interval_ns
109 }
110
111 #[must_use]
112 pub fn last_refresh_ns(&self) -> Option<u64> {
113 self.cooldown.last_event_ns
114 }
115}
116
117impl From<Vec<TargetId>> for RefreshPlan {
118 fn from(targets: Vec<TargetId>) -> Self {
119 Self::new(targets)
120 }
121}
122
123#[templar_vault_macros::vault_derive]
125#[derive(Clone, PartialEq, Eq)]
126pub enum RefreshPlanError {
127 EmptyPlan,
129 OnCooldown {
131 last_refresh_ns: Option<u64>,
132 cooldown_ns: u64,
133 current_ns: u64,
134 },
135 DuplicateTarget { target_id: TargetId },
137 TargetNotFound { target_id: TargetId },
139}
140
141pub fn build_refresh_plan(
150 enabled_targets: &[TargetId],
151 cooldown_ns: Option<u64>,
152) -> Result<RefreshPlan, RefreshPlanError> {
153 if enabled_targets.is_empty() {
154 return Err(RefreshPlanError::EmptyPlan);
155 }
156
157 let plan = RefreshPlan::new(enabled_targets.to_vec());
158 let plan = match cooldown_ns {
159 Some(ns) => plan.with_cooldown(ns),
160 None => plan,
161 };
162
163 Ok(plan)
164}
165
166pub fn build_targeted_refresh_plan(
175 targets: &[TargetId],
176 enabled_targets: &[TargetId],
177) -> Result<RefreshPlan, RefreshPlanError> {
178 if targets.is_empty() {
179 return Err(RefreshPlanError::EmptyPlan);
180 }
181
182 if let Some(dup) = find_first_duplicate(targets) {
183 return Err(RefreshPlanError::DuplicateTarget { target_id: dup });
184 }
185
186 let enabled_set: BTreeSet<_> = enabled_targets.iter().copied().collect();
187
188 for target in targets {
190 if !enabled_set.contains(target) {
191 return Err(RefreshPlanError::TargetNotFound { target_id: *target });
192 }
193 }
194
195 Ok(RefreshPlan::new(targets.to_vec()))
196}
197
198#[must_use]
208pub fn filter_stale_targets(
209 all_targets: &[(TargetId, u64)],
210 max_age_ns: u64,
211 current_ns: u64,
212) -> Vec<TargetId> {
213 all_targets
214 .iter()
215 .filter_map(|(target_id, last_refresh)| {
216 let age = current_ns.saturating_sub(*last_refresh);
217 if age > max_age_ns {
218 Some(*target_id)
219 } else {
220 None
221 }
222 })
223 .collect()
224}