1use std::collections::{HashMap, HashSet, VecDeque};
11
12#[derive(Debug, Clone, PartialEq)]
14pub enum CascadeEvent {
15 Updated { tile_id: String },
17 Invalidated { tile_id: String },
19 Revalidated { tile_id: String },
21}
22
23#[derive(Debug, Clone)]
25pub struct CascadeEffect {
26 pub tile_id: String,
27 pub event: CascadeEvent,
28 pub depth: usize,
29 pub reason: String,
30}
31
32#[derive(Debug, Clone)]
34pub struct CascadeConfig {
35 pub max_depth: usize,
37 pub auto_invalidate: bool,
39 pub auto_revalidate: bool,
41}
42
43impl Default for CascadeConfig {
44 fn default() -> Self {
45 Self { max_depth: 10, auto_invalidate: true, auto_revalidate: false }
46 }
47}
48
49#[derive(Debug, Clone)]
51pub struct CascadableTile {
52 pub id: String,
53 pub content: String,
54 pub dependencies: Vec<String>,
55 pub valid: bool,
56 pub version: u32,
57}
58
59pub struct CascadeEngine {
61 tiles: HashMap<String, CascadableTile>,
62 config: CascadeConfig,
63 cascade_log: Vec<CascadeEffect>,
64}
65
66impl CascadeEngine {
67 pub fn new(config: CascadeConfig) -> Self {
68 Self { tiles: HashMap::new(), config, cascade_log: Vec::new() }
69 }
70
71 pub fn with_defaults() -> Self { Self::new(CascadeConfig::default()) }
72
73 pub fn register(&mut self, tile: CascadableTile) {
75 self.tiles.insert(tile.id.clone(), tile);
76 }
77
78 pub fn update_tile(&mut self, tile_id: &str, new_content: &str) -> Vec<CascadeEffect> {
80 let mut effects = Vec::new();
81 if let Some(tile) = self.tiles.get_mut(tile_id) {
82 tile.content = new_content.to_string();
83 tile.version += 1;
84 effects.push(CascadeEffect {
85 tile_id: tile_id.to_string(),
86 event: CascadeEvent::Updated { tile_id: tile_id.to_string() },
87 depth: 0,
88 reason: "direct update".to_string(),
89 });
90 }
91 let downstream_effects = self.propagate(tile_id, 1);
93 effects.extend(downstream_effects);
94 self.cascade_log.extend(effects.clone());
95 effects
96 }
97
98 pub fn invalidate_tile(&mut self, tile_id: &str) -> Vec<CascadeEffect> {
100 let mut effects = Vec::new();
101 if let Some(tile) = self.tiles.get_mut(tile_id) {
102 tile.valid = false;
103 effects.push(CascadeEffect {
104 tile_id: tile_id.to_string(),
105 event: CascadeEvent::Invalidated { tile_id: tile_id.to_string() },
106 depth: 0,
107 reason: "direct invalidation".to_string(),
108 });
109 }
110 let downstream = self.propagate(tile_id, 1);
111 effects.extend(downstream);
112 self.cascade_log.extend(effects.clone());
113 effects
114 }
115
116 fn direct_dependents(&self, tile_id: &str) -> Vec<String> {
118 self.tiles.values()
119 .filter(|t| t.dependencies.iter().any(|d| d == tile_id))
120 .map(|t| t.id.clone())
121 .collect()
122 }
123
124 fn propagate(&mut self, source_id: &str, start_depth: usize) -> Vec<CascadeEffect> {
126 let mut effects = Vec::new();
127 let mut queue: VecDeque<(String, usize)> = VecDeque::new();
128 for dep in self.direct_dependents(source_id) {
129 queue.push_back((dep, start_depth));
130 }
131 while let Some((current_id, depth)) = queue.pop_front() {
132 if depth > self.config.max_depth { continue; }
133 if self.config.auto_invalidate {
134 if let Some(tile) = self.tiles.get_mut(¤t_id) {
135 if tile.valid {
136 tile.valid = false;
137 effects.push(CascadeEffect {
138 tile_id: current_id.clone(),
139 event: CascadeEvent::Invalidated { tile_id: current_id.clone() },
140 depth,
141 reason: format!("upstream '{}' changed", source_id),
142 });
143 for dep in self.direct_dependents(¤t_id) {
145 queue.push_back((dep, depth + 1));
146 }
147 }
148 }
149 }
150 }
151 effects
152 }
153
154 pub fn cascade_log(&self) -> &[CascadeEffect] { &self.cascade_log }
156
157 pub fn invalid_count(&self) -> usize {
159 self.tiles.values().filter(|t| !t.valid).count()
160 }
161
162 pub fn invalid_tiles(&self) -> Vec<String> {
164 self.tiles.values().filter(|t| !t.valid).map(|t| t.id.clone()).collect()
165 }
166
167 pub fn revalidate(&mut self, tile_id: &str) -> bool {
169 if let Some(tile) = self.tiles.get_mut(tile_id) {
170 if !tile.valid {
171 tile.valid = true;
172 self.cascade_log.push(CascadeEffect {
173 tile_id: tile_id.to_string(),
174 event: CascadeEvent::Revalidated { tile_id: tile_id.to_string() },
175 depth: 0,
176 reason: "manual revalidation".to_string(),
177 });
178 return true;
179 }
180 }
181 false
182 }
183
184 pub fn revalidate_all(&mut self) -> usize {
186 let invalid: Vec<String> = self.invalid_tiles();
187 let count = invalid.len();
188 for id in invalid {
189 self.revalidate(&id);
190 }
191 count
192 }
193
194 pub fn tile_count(&self) -> usize { self.tiles.len() }
196
197 pub fn has_tile(&self, id: &str) -> bool { self.tiles.contains_key(id) }
199
200 pub fn impact_radius(&self, tile_id: &str) -> usize {
202 let mut visited = HashSet::new();
203 let mut queue = VecDeque::new();
204 queue.push_back(tile_id.to_string());
205 visited.insert(tile_id.to_string());
206 while let Some(current) = queue.pop_front() {
207 for dep in self.direct_dependents(¤t) {
208 if visited.insert(dep.clone()) {
209 queue.push_back(dep);
210 }
211 }
212 }
213 visited.len()
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 fn make_engine() -> CascadeEngine {
222 let mut e = CascadeEngine::with_defaults();
223 e.register(CascadableTile { id: "a".into(), content: "root".into(), dependencies: vec![], valid: true, version: 1 });
224 e.register(CascadableTile { id: "b".into(), content: "dep on a".into(), dependencies: vec!["a".into()], valid: true, version: 1 });
225 e.register(CascadableTile { id: "c".into(), content: "dep on b".into(), dependencies: vec!["b".into()], valid: true, version: 1 });
226 e.register(CascadableTile { id: "d".into(), content: "also dep on a".into(), dependencies: vec!["a".into()], valid: true, version: 1 });
227 e
228 }
229
230 #[test]
231 fn test_update_triggers_cascade() {
232 let mut e = make_engine();
233 let effects = e.update_tile("a", "new root content");
234 assert!(effects.iter().any(|e| e.tile_id == "a" && e.depth == 0));
235 assert!(effects.iter().any(|e| e.tile_id == "b"));
237 assert!(effects.iter().any(|e| e.tile_id == "d"));
238 assert!(effects.iter().any(|e| e.tile_id == "c"));
239 }
240
241 #[test]
242 fn test_invalidate_propagates() {
243 let mut e = make_engine();
244 let effects = e.invalidate_tile("b");
245 assert!(effects.iter().any(|e| e.tile_id == "b" && matches!(e.event, CascadeEvent::Invalidated { .. })));
247 assert!(effects.iter().any(|e| e.tile_id == "c"));
248 }
249
250 #[test]
251 fn test_revalidate_single() {
252 let mut e = make_engine();
253 e.invalidate_tile("b");
254 assert_eq!(e.invalid_count(), 2); assert!(e.revalidate("b"));
256 assert_eq!(e.invalid_count(), 1); }
258
259 #[test]
260 fn test_revalidate_all() {
261 let mut e = make_engine();
262 e.invalidate_tile("a");
263 let count = e.revalidate_all();
264 assert_eq!(count, 4); assert_eq!(e.invalid_count(), 0);
266 }
267
268 #[test]
269 fn test_impact_radius() {
270 let e = make_engine();
271 assert_eq!(e.impact_radius("a"), 4);
272 assert_eq!(e.impact_radius("b"), 2);
273 assert_eq!(e.impact_radius("c"), 1);
274 }
275
276 #[test]
277 fn test_max_depth_limits_propagation() {
278 let mut config = CascadeConfig::default();
279 config.max_depth = 1;
280 let mut e = CascadeEngine::new(config);
281 e.register(CascadableTile { id: "a".into(), content: "".into(), dependencies: vec![], valid: true, version: 1 });
282 e.register(CascadableTile { id: "b".into(), content: "".into(), dependencies: vec!["a".into()], valid: true, version: 1 });
283 e.register(CascadableTile { id: "c".into(), content: "".into(), dependencies: vec!["b".into()], valid: true, version: 1 });
284 let effects = e.update_tile("a", "new");
285 assert!(effects.iter().any(|e| e.tile_id == "b"));
287 assert!(!effects.iter().any(|e| e.tile_id == "c"));
288 }
289
290 #[test]
291 fn test_cascade_log() {
292 let mut e = make_engine();
293 e.invalidate_tile("a");
294 assert!(!e.cascade_log().is_empty());
295 }
296
297 #[test]
298 fn test_no_auto_invalidate() {
299 let mut config = CascadeConfig::default();
300 config.auto_invalidate = false;
301 let mut e = CascadeEngine::new(config);
302 e.register(CascadableTile { id: "a".into(), content: "".into(), dependencies: vec![], valid: true, version: 1 });
303 e.register(CascadableTile { id: "b".into(), content: "".into(), dependencies: vec!["a".into()], valid: true, version: 1 });
304 let effects = e.update_tile("a", "new");
305 assert_eq!(effects.len(), 1);
307 assert_eq!(e.invalid_count(), 0);
308 }
309
310 #[test]
311 fn test_leaf_tile_no_downstream() {
312 let mut e = CascadeEngine::with_defaults();
313 e.register(CascadableTile { id: "leaf".into(), content: "".into(), dependencies: vec![], valid: true, version: 1 });
314 let effects = e.update_tile("leaf", "updated");
315 assert_eq!(effects.len(), 1);
316 }
317}