hypen_engine/reactive/
graph.rs1use super::Binding;
2use crate::ir::NodeId;
3use indexmap::{IndexMap, IndexSet};
4use std::collections::BTreeMap;
5
6pub struct DependencyGraph {
8 dependencies: IndexMap<String, IndexSet<NodeId>>,
10
11 node_bindings: IndexMap<NodeId, IndexSet<String>>,
13
14 prefix_index: BTreeMap<String, IndexSet<String>>,
18}
19
20impl DependencyGraph {
21 pub fn new() -> Self {
22 Self {
23 dependencies: IndexMap::new(),
24 node_bindings: IndexMap::new(),
25 prefix_index: BTreeMap::new(),
26 }
27 }
28
29 pub fn add_dependency(&mut self, node_id: NodeId, binding: &Binding) {
31 let path = binding.full_path();
32
33 self.dependencies
35 .entry(path.clone())
36 .or_insert_with(IndexSet::new)
37 .insert(node_id);
38
39 self.node_bindings
41 .entry(node_id)
42 .or_insert_with(IndexSet::new)
43 .insert(path.clone());
44
45 self.add_path_to_prefix_index(&path);
48 }
49
50 fn add_path_to_prefix_index(&mut self, path: &str) {
52 self.prefix_index
54 .entry(path.to_string())
55 .or_insert_with(IndexSet::new)
56 .insert(path.to_string());
57
58 let mut current = path;
60 while let Some(dot_pos) = current.rfind('.') {
61 current = ¤t[..dot_pos];
62 self.prefix_index
63 .entry(current.to_string())
64 .or_insert_with(IndexSet::new)
65 .insert(path.to_string());
66 }
67 }
68
69 pub fn remove_node(&mut self, node_id: NodeId) {
71 if let Some(paths) = self.node_bindings.shift_remove(&node_id) {
72 for path in paths {
73 if let Some(nodes) = self.dependencies.get_mut(&path) {
74 nodes.shift_remove(&node_id);
75 }
76 }
77 }
78 }
79
80 pub fn get_dependent_nodes(&self, path: &str) -> Option<&IndexSet<NodeId>> {
82 self.dependencies.get(path)
83 }
84
85 pub fn get_affected_nodes(&self, changed_path: &str) -> IndexSet<NodeId> {
89 let mut affected = IndexSet::new();
90 let mut affected_paths = IndexSet::new();
91
92 if self.dependencies.contains_key(changed_path) {
95 affected_paths.insert(changed_path.to_string());
96 }
97
98 let mut current = changed_path;
100 while let Some(dot_pos) = current.rfind('.') {
101 current = ¤t[..dot_pos];
102 if self.dependencies.contains_key(current) {
103 affected_paths.insert(current.to_string());
104 }
105 }
106
107 if let Some(child_paths) = self.prefix_index.get(changed_path) {
110 affected_paths.extend(child_paths.iter().cloned());
111 }
112
113 for path in affected_paths {
115 if let Some(nodes) = self.dependencies.get(&path) {
116 affected.extend(nodes.iter().copied());
117 }
118 }
119
120 affected
121 }
122
123 pub fn clear(&mut self) {
125 self.dependencies.clear();
126 self.node_bindings.clear();
127 self.prefix_index.clear();
128 }
129}
130
131impl Default for DependencyGraph {
132 fn default() -> Self {
133 Self::new()
134 }
135}
136
137fn is_path_affected(dep_path: &str, changed_path: &str) -> bool {
143 if dep_path == changed_path {
144 return true;
145 }
146
147 if dep_path.starts_with(changed_path) {
150 let remainder = &dep_path[changed_path.len()..];
151 return remainder.starts_with('.');
152 }
153
154 if changed_path.starts_with(dep_path) {
157 let remainder = &changed_path[dep_path.len()..];
158 return remainder.starts_with('.');
159 }
160
161 false
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn test_path_affected() {
170 assert!(is_path_affected("user", "user"));
171 assert!(is_path_affected("user.name", "user"));
172 assert!(is_path_affected("user", "user.name"));
173 assert!(!is_path_affected("user", "username"));
174 assert!(!is_path_affected("user.name", "user.email"));
175 }
176
177 #[test]
178 fn test_prefix_index_basic() {
179 use super::Binding;
180 use crate::reconcile::tree::InstanceTree;
181 use crate::ir::Element;
182
183 let mut graph = DependencyGraph::new();
184
185 let mut tree = InstanceTree::new();
187 let node1 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
188 let node2 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
189 let node3 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
190
191 let binding1 = Binding::state(vec!["user".to_string()]);
193 let binding2 = Binding::state(vec!["user".to_string(), "name".to_string()]);
194 let binding3 = Binding::state(vec!["user".to_string(), "email".to_string()]);
195
196 graph.add_dependency(node1, &binding1);
197 graph.add_dependency(node2, &binding2);
198 graph.add_dependency(node3, &binding3);
199
200 assert!(graph.prefix_index.contains_key("user"));
202 assert!(graph.prefix_index.contains_key("user.name"));
203 assert!(graph.prefix_index.contains_key("user.email"));
204
205 let user_paths = graph.prefix_index.get("user").unwrap();
207 assert_eq!(user_paths.len(), 3);
208 assert!(user_paths.contains("user"));
209 assert!(user_paths.contains("user.name"));
210 assert!(user_paths.contains("user.email"));
211 }
212
213 #[test]
214 fn test_get_affected_nodes_with_prefix_index() {
215 use super::Binding;
216 use crate::reconcile::tree::InstanceTree;
217 use crate::ir::Element;
218
219 let mut graph = DependencyGraph::new();
220
221 let mut tree = InstanceTree::new();
223 let node1 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
224 let node2 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
225 let node3 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
226 let node4 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
227
228 graph.add_dependency(node1, &Binding::state(vec!["user".to_string()]));
230 graph.add_dependency(node2, &Binding::state(vec!["user".to_string(), "name".to_string()]));
231 graph.add_dependency(node3, &Binding::state(vec!["user".to_string(), "email".to_string()]));
232 graph.add_dependency(node4, &Binding::state(vec!["posts".to_string()]));
233
234 let affected = graph.get_affected_nodes("user");
236 assert_eq!(affected.len(), 3);
237 assert!(affected.contains(&node1));
238 assert!(affected.contains(&node2));
239 assert!(affected.contains(&node3));
240 assert!(!affected.contains(&node4));
241 }
242
243 #[test]
244 fn test_get_affected_nodes_child_change() {
245 use super::Binding;
246 use crate::reconcile::tree::InstanceTree;
247 use crate::ir::Element;
248
249 let mut graph = DependencyGraph::new();
250
251 let mut tree = InstanceTree::new();
253 let node1 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
254 let node2 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
255
256 graph.add_dependency(node1, &Binding::state(vec!["user".to_string()]));
257 graph.add_dependency(node2, &Binding::state(vec!["user".to_string(), "name".to_string()]));
258
259 let affected = graph.get_affected_nodes("user.name");
261 assert_eq!(affected.len(), 2);
262 assert!(affected.contains(&node1)); assert!(affected.contains(&node2)); }
265
266 #[test]
267 fn test_get_affected_nodes_exact_match_only() {
268 use super::Binding;
269 use crate::reconcile::tree::InstanceTree;
270 use crate::ir::Element;
271
272 let mut graph = DependencyGraph::new();
273
274 let mut tree = InstanceTree::new();
276 let node1 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
277
278 graph.add_dependency(node1, &Binding::state(vec!["user".to_string(), "name".to_string()]));
279
280 let affected = graph.get_affected_nodes("user.email");
282 assert_eq!(affected.len(), 0);
283 }
284
285 #[test]
286 fn test_prefix_index_performance() {
287 use super::Binding;
288 use crate::reconcile::tree::InstanceTree;
289 use crate::ir::Element;
290
291 let mut graph = DependencyGraph::new();
292 let mut tree = InstanceTree::new();
293
294 for i in 0..100 {
296 let node_id = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
297 let path = vec![
298 "user".to_string(),
299 "profile".to_string(),
300 "details".to_string(),
301 format!("field{}", i),
302 ];
303 let binding = Binding::state(path);
304 graph.add_dependency(node_id, &binding);
305 }
306
307 let affected = graph.get_affected_nodes("user");
309 assert_eq!(affected.len(), 100);
310
311 let affected = graph.get_affected_nodes("user.profile");
312 assert_eq!(affected.len(), 100);
313
314 let affected = graph.get_affected_nodes("user.profile.details");
315 assert_eq!(affected.len(), 100);
316 }
317
318 #[test]
319 fn test_clear_clears_prefix_index() {
320 use super::Binding;
321 use crate::reconcile::tree::InstanceTree;
322 use crate::ir::Element;
323
324 let mut graph = DependencyGraph::new();
325
326 let mut tree = InstanceTree::new();
328 let node = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
329
330 graph.add_dependency(node, &Binding::state(vec!["user".to_string(), "name".to_string()]));
331 assert!(!graph.prefix_index.is_empty());
332
333 graph.clear();
334 assert!(graph.prefix_index.is_empty());
335 assert!(graph.dependencies.is_empty());
336 assert!(graph.node_bindings.is_empty());
337 }
338}