use std::collections::{HashMap, HashSet, VecDeque};
#[derive(Debug, Clone, PartialEq)]
pub enum CascadeEvent {
Updated { tile_id: String },
Invalidated { tile_id: String },
Revalidated { tile_id: String },
}
#[derive(Debug, Clone)]
pub struct CascadeEffect {
pub tile_id: String,
pub event: CascadeEvent,
pub depth: usize,
pub reason: String,
}
#[derive(Debug, Clone)]
pub struct CascadeConfig {
pub max_depth: usize,
pub auto_invalidate: bool,
pub auto_revalidate: bool,
}
impl Default for CascadeConfig {
fn default() -> Self {
Self { max_depth: 10, auto_invalidate: true, auto_revalidate: false }
}
}
#[derive(Debug, Clone)]
pub struct CascadableTile {
pub id: String,
pub content: String,
pub dependencies: Vec<String>,
pub valid: bool,
pub version: u32,
}
pub struct CascadeEngine {
tiles: HashMap<String, CascadableTile>,
config: CascadeConfig,
cascade_log: Vec<CascadeEffect>,
}
impl CascadeEngine {
pub fn new(config: CascadeConfig) -> Self {
Self { tiles: HashMap::new(), config, cascade_log: Vec::new() }
}
pub fn with_defaults() -> Self { Self::new(CascadeConfig::default()) }
pub fn register(&mut self, tile: CascadableTile) {
self.tiles.insert(tile.id.clone(), tile);
}
pub fn update_tile(&mut self, tile_id: &str, new_content: &str) -> Vec<CascadeEffect> {
let mut effects = Vec::new();
if let Some(tile) = self.tiles.get_mut(tile_id) {
tile.content = new_content.to_string();
tile.version += 1;
effects.push(CascadeEffect {
tile_id: tile_id.to_string(),
event: CascadeEvent::Updated { tile_id: tile_id.to_string() },
depth: 0,
reason: "direct update".to_string(),
});
}
let downstream_effects = self.propagate(tile_id, 1);
effects.extend(downstream_effects);
self.cascade_log.extend(effects.clone());
effects
}
pub fn invalidate_tile(&mut self, tile_id: &str) -> Vec<CascadeEffect> {
let mut effects = Vec::new();
if let Some(tile) = self.tiles.get_mut(tile_id) {
tile.valid = false;
effects.push(CascadeEffect {
tile_id: tile_id.to_string(),
event: CascadeEvent::Invalidated { tile_id: tile_id.to_string() },
depth: 0,
reason: "direct invalidation".to_string(),
});
}
let downstream = self.propagate(tile_id, 1);
effects.extend(downstream);
self.cascade_log.extend(effects.clone());
effects
}
fn direct_dependents(&self, tile_id: &str) -> Vec<String> {
self.tiles.values()
.filter(|t| t.dependencies.iter().any(|d| d == tile_id))
.map(|t| t.id.clone())
.collect()
}
fn propagate(&mut self, source_id: &str, start_depth: usize) -> Vec<CascadeEffect> {
let mut effects = Vec::new();
let mut queue: VecDeque<(String, usize)> = VecDeque::new();
for dep in self.direct_dependents(source_id) {
queue.push_back((dep, start_depth));
}
while let Some((current_id, depth)) = queue.pop_front() {
if depth > self.config.max_depth { continue; }
if self.config.auto_invalidate {
if let Some(tile) = self.tiles.get_mut(¤t_id) {
if tile.valid {
tile.valid = false;
effects.push(CascadeEffect {
tile_id: current_id.clone(),
event: CascadeEvent::Invalidated { tile_id: current_id.clone() },
depth,
reason: format!("upstream '{}' changed", source_id),
});
for dep in self.direct_dependents(¤t_id) {
queue.push_back((dep, depth + 1));
}
}
}
}
}
effects
}
pub fn cascade_log(&self) -> &[CascadeEffect] { &self.cascade_log }
pub fn invalid_count(&self) -> usize {
self.tiles.values().filter(|t| !t.valid).count()
}
pub fn invalid_tiles(&self) -> Vec<String> {
self.tiles.values().filter(|t| !t.valid).map(|t| t.id.clone()).collect()
}
pub fn revalidate(&mut self, tile_id: &str) -> bool {
if let Some(tile) = self.tiles.get_mut(tile_id) {
if !tile.valid {
tile.valid = true;
self.cascade_log.push(CascadeEffect {
tile_id: tile_id.to_string(),
event: CascadeEvent::Revalidated { tile_id: tile_id.to_string() },
depth: 0,
reason: "manual revalidation".to_string(),
});
return true;
}
}
false
}
pub fn revalidate_all(&mut self) -> usize {
let invalid: Vec<String> = self.invalid_tiles();
let count = invalid.len();
for id in invalid {
self.revalidate(&id);
}
count
}
pub fn tile_count(&self) -> usize { self.tiles.len() }
pub fn has_tile(&self, id: &str) -> bool { self.tiles.contains_key(id) }
pub fn impact_radius(&self, tile_id: &str) -> usize {
let mut visited = HashSet::new();
let mut queue = VecDeque::new();
queue.push_back(tile_id.to_string());
visited.insert(tile_id.to_string());
while let Some(current) = queue.pop_front() {
for dep in self.direct_dependents(¤t) {
if visited.insert(dep.clone()) {
queue.push_back(dep);
}
}
}
visited.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_engine() -> CascadeEngine {
let mut e = CascadeEngine::with_defaults();
e.register(CascadableTile { id: "a".into(), content: "root".into(), dependencies: vec![], valid: true, version: 1 });
e.register(CascadableTile { id: "b".into(), content: "dep on a".into(), dependencies: vec!["a".into()], valid: true, version: 1 });
e.register(CascadableTile { id: "c".into(), content: "dep on b".into(), dependencies: vec!["b".into()], valid: true, version: 1 });
e.register(CascadableTile { id: "d".into(), content: "also dep on a".into(), dependencies: vec!["a".into()], valid: true, version: 1 });
e
}
#[test]
fn test_update_triggers_cascade() {
let mut e = make_engine();
let effects = e.update_tile("a", "new root content");
assert!(effects.iter().any(|e| e.tile_id == "a" && e.depth == 0));
assert!(effects.iter().any(|e| e.tile_id == "b"));
assert!(effects.iter().any(|e| e.tile_id == "d"));
assert!(effects.iter().any(|e| e.tile_id == "c"));
}
#[test]
fn test_invalidate_propagates() {
let mut e = make_engine();
let effects = e.invalidate_tile("b");
assert!(effects.iter().any(|e| e.tile_id == "b" && matches!(e.event, CascadeEvent::Invalidated { .. })));
assert!(effects.iter().any(|e| e.tile_id == "c"));
}
#[test]
fn test_revalidate_single() {
let mut e = make_engine();
e.invalidate_tile("b");
assert_eq!(e.invalid_count(), 2); assert!(e.revalidate("b"));
assert_eq!(e.invalid_count(), 1); }
#[test]
fn test_revalidate_all() {
let mut e = make_engine();
e.invalidate_tile("a");
let count = e.revalidate_all();
assert_eq!(count, 4); assert_eq!(e.invalid_count(), 0);
}
#[test]
fn test_impact_radius() {
let e = make_engine();
assert_eq!(e.impact_radius("a"), 4);
assert_eq!(e.impact_radius("b"), 2);
assert_eq!(e.impact_radius("c"), 1);
}
#[test]
fn test_max_depth_limits_propagation() {
let mut config = CascadeConfig::default();
config.max_depth = 1;
let mut e = CascadeEngine::new(config);
e.register(CascadableTile { id: "a".into(), content: "".into(), dependencies: vec![], valid: true, version: 1 });
e.register(CascadableTile { id: "b".into(), content: "".into(), dependencies: vec!["a".into()], valid: true, version: 1 });
e.register(CascadableTile { id: "c".into(), content: "".into(), dependencies: vec!["b".into()], valid: true, version: 1 });
let effects = e.update_tile("a", "new");
assert!(effects.iter().any(|e| e.tile_id == "b"));
assert!(!effects.iter().any(|e| e.tile_id == "c"));
}
#[test]
fn test_cascade_log() {
let mut e = make_engine();
e.invalidate_tile("a");
assert!(!e.cascade_log().is_empty());
}
#[test]
fn test_no_auto_invalidate() {
let mut config = CascadeConfig::default();
config.auto_invalidate = false;
let mut e = CascadeEngine::new(config);
e.register(CascadableTile { id: "a".into(), content: "".into(), dependencies: vec![], valid: true, version: 1 });
e.register(CascadableTile { id: "b".into(), content: "".into(), dependencies: vec!["a".into()], valid: true, version: 1 });
let effects = e.update_tile("a", "new");
assert_eq!(effects.len(), 1);
assert_eq!(e.invalid_count(), 0);
}
#[test]
fn test_leaf_tile_no_downstream() {
let mut e = CascadeEngine::with_defaults();
e.register(CascadableTile { id: "leaf".into(), content: "".into(), dependencies: vec![], valid: true, version: 1 });
let effects = e.update_tile("leaf", "updated");
assert_eq!(effects.len(), 1);
}
}