csm_memory/
singularity_ttl.rs1use std::collections::HashMap;
4
5use crate::singularity::{Concept, Singularity, unix_now_secs};
6use csm_core_lib::hyperdim::HVec10240;
7
8impl Default for Concept {
9 fn default() -> Self {
10 Self {
11 id: String::new(),
12 vector: HVec10240::zero(),
13 metadata: HashMap::new(),
14 created_at: 0,
15 modified_at: 0,
16 expires_at: None,
17 canonical_concept_ids: Vec::new(),
18 }
19 }
20}
21
22impl Singularity {
23 pub fn purge_expired(&mut self, ns: &str) -> usize {
27 self.purge_expired_cascading(ns, false)
28 }
29
30 #[allow(clippy::unwrap_used)]
32 pub fn purge_expired_cascading(&mut self, ns: &str, cascading: bool) -> usize {
33 let now = unix_now_secs();
34 let Some(ns_state) = self.get_namespace(ns) else {
35 return 0;
36 };
37 let mut to_remove: std::collections::HashSet<String> = ns_state
38 .concepts
39 .iter()
40 .filter(|(_, c)| c.expires_at.is_some_and(|exp| exp <= now))
41 .map(|(id, _)| id.clone())
42 .collect();
43
44 if cascading && !to_remove.is_empty() {
45 let mut changed = true;
46 while changed {
47 changed = false;
48 let ns_state = self.get_namespace(ns).unwrap();
49 for (from_id, neighbors) in &ns_state.associations {
50 if to_remove.contains(from_id) {
51 for to_id in neighbors.keys() {
52 if !to_remove.contains(to_id) {
53 to_remove.insert(to_id.clone());
54 changed = true;
55 }
56 }
57 }
58 }
59 }
60 }
61
62 let count = to_remove.len();
63 for id in to_remove {
64 self.delete(ns, &id).ok();
65 }
66 if count > 0 {
67 self.invalidate_cache(ns);
68 }
69 count
70 }
71
72 pub fn is_expired(&self, ns: &str, id: &str) -> bool {
74 let now = unix_now_secs();
75 self.get(ns, id)
76 .is_some_and(|c| c.expires_at.is_some_and(|exp| exp <= now))
77 }
78
79 pub fn active_concept_ids(&self, ns: &str) -> Vec<String> {
81 let now = unix_now_secs();
82 self.get_namespace(ns)
83 .map(|n| {
84 n.concepts
85 .iter()
86 .filter(|(_, c)| c.expires_at.is_none_or(|exp| exp > now))
87 .map(|(id, _)| id.clone())
88 .collect()
89 })
90 .unwrap_or_default()
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
97 use super::*;
98 use crate::singularity::SingularityConfig;
99
100 #[test]
101 fn test_purge_expired() {
102 let mut sing = Singularity::<HVec10240>::new(SingularityConfig::default());
103 let now = unix_now_secs();
104
105 let concept1 = Concept {
106 id: "expired".to_string(),
107 expires_at: Some(now - 100),
108 ..Default::default()
109 };
110
111 let concept2 = Concept {
112 id: "active".to_string(),
113 expires_at: Some(now + 100),
114 ..Default::default()
115 };
116
117 let concept3 = Concept {
118 id: "no_exp".to_string(),
119 expires_at: None,
120 ..Default::default()
121 };
122
123 let ns = "_default";
124 sing.inject("_default", concept1).unwrap();
125 sing.inject("_default", concept2).unwrap();
126 sing.inject("_default", concept3).unwrap();
127
128 assert_eq!(sing.active_concept_ids(ns).len(), 2);
129
130 let purged = sing.purge_expired("_default");
131 assert_eq!(purged, 1);
132
133 assert!(
134 !sing
135 .get_namespace(ns)
136 .unwrap()
137 .concepts
138 .contains_key("expired")
139 );
140 assert!(
141 sing.get_namespace(ns)
142 .unwrap()
143 .concepts
144 .contains_key("active")
145 );
146 assert!(
147 sing.get_namespace(ns)
148 .unwrap()
149 .concepts
150 .contains_key("no_exp")
151 );
152 }
153
154 #[test]
155 fn test_is_expired() {
156 let mut sing = Singularity::<HVec10240>::new(SingularityConfig::default());
157 let now = unix_now_secs();
158
159 let concept1 = Concept {
160 id: "expired".to_string(),
161 expires_at: Some(now - 100),
162 ..Default::default()
163 };
164
165 let concept2 = Concept {
166 id: "active".to_string(),
167 expires_at: Some(now + 100),
168 ..Default::default()
169 };
170
171 let concept3 = Concept {
172 id: "no_exp".to_string(),
173 expires_at: None,
174 ..Default::default()
175 };
176
177 let concept4 = Concept {
178 id: "just_expired".to_string(),
179 expires_at: Some(now),
180 ..Default::default()
181 };
182
183 let ns = "_default";
184 sing.inject("_default", concept1).unwrap();
185 sing.inject("_default", concept2).unwrap();
186 sing.inject("_default", concept3).unwrap();
187 sing.inject("_default", concept4).unwrap();
188
189 assert!(sing.is_expired(ns, "expired"));
190 assert!(!sing.is_expired(ns, "active"));
191 assert!(!sing.is_expired(ns, "no_exp"));
192 assert!(sing.is_expired(ns, "just_expired"));
193 assert!(!sing.is_expired(ns, "nonexistent"));
194 }
195
196 #[test]
197 fn test_active_concept_ids() {
198 let mut sing = Singularity::<HVec10240>::new(SingularityConfig::default());
199 let now = unix_now_secs();
200
201 let concept1 = Concept {
202 id: "expired".to_string(),
203 expires_at: Some(now - 100),
204 ..Default::default()
205 };
206
207 let concept2 = Concept {
208 id: "active".to_string(),
209 expires_at: Some(now + 100),
210 ..Default::default()
211 };
212
213 let concept3 = Concept {
214 id: "no_exp".to_string(),
215 expires_at: None,
216 ..Default::default()
217 };
218
219 let ns = "_default";
220 sing.inject("_default", concept1).unwrap();
221 sing.inject("_default", concept2).unwrap();
222 sing.inject("_default", concept3).unwrap();
223
224 let mut active = sing.active_concept_ids(ns);
225 active.sort();
226
227 let mut expected = vec!["active".to_string(), "no_exp".to_string()];
228 expected.sort();
229
230 assert_eq!(active, expected);
231 }
232}