1use 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 registered_providers: IndexSet<String>,
24
25 unregistered_provider_refs: IndexSet<String>,
28}
29
30impl DependencyGraph {
31 pub fn new() -> Self {
32 Self {
33 dependencies: IndexMap::new(),
34 node_bindings: IndexMap::new(),
35 prefix_index: BTreeMap::new(),
36 registered_providers: IndexSet::new(),
37 unregistered_provider_refs: IndexSet::new(),
38 }
39 }
40
41 pub fn add_dependency(&mut self, node_id: NodeId, binding: &Binding) {
46 use super::BindingSource;
47 let path = match &binding.source {
48 BindingSource::State => binding.full_path(),
49 BindingSource::Item => return, BindingSource::DataSource(provider) => {
51 if !self.registered_providers.contains(provider.as_str()) {
53 self.unregistered_provider_refs.insert(provider.clone());
54 }
55 format!("ds:{}:{}", provider, binding.full_path())
56 }
57 };
58
59 self.dependencies
61 .entry(path.clone())
62 .or_default()
63 .insert(node_id);
64
65 self.node_bindings
67 .entry(node_id)
68 .or_default()
69 .insert(path.clone());
70
71 self.add_path_to_prefix_index(&path);
74 }
75
76 fn add_path_to_prefix_index(&mut self, path: &str) {
78 self.prefix_index
80 .entry(path.to_string())
81 .or_default()
82 .insert(path.to_string());
83
84 let mut current = path;
86 while let Some(dot_pos) = current.rfind('.') {
87 current = ¤t[..dot_pos];
88 self.prefix_index
89 .entry(current.to_string())
90 .or_default()
91 .insert(path.to_string());
92 }
93 }
94
95 pub fn remove_node(&mut self, node_id: NodeId) {
97 if let Some(paths) = self.node_bindings.shift_remove(&node_id) {
98 for path in paths {
99 if let Some(nodes) = self.dependencies.get_mut(&path) {
100 nodes.shift_remove(&node_id);
101 }
102 }
103 }
104 }
105
106 pub fn get_dependent_nodes(&self, path: &str) -> Option<&IndexSet<NodeId>> {
108 self.dependencies.get(path)
109 }
110
111 pub fn get_affected_nodes(&self, changed_path: &str) -> IndexSet<NodeId> {
115 let mut affected = IndexSet::new();
116 let mut affected_paths = IndexSet::new();
117
118 if self.dependencies.contains_key(changed_path) {
121 affected_paths.insert(changed_path.to_string());
122 }
123
124 let mut current = changed_path;
126 while let Some(dot_pos) = current.rfind('.') {
127 current = ¤t[..dot_pos];
128 if self.dependencies.contains_key(current) {
129 affected_paths.insert(current.to_string());
130 }
131 }
132
133 if let Some(child_paths) = self.prefix_index.get(changed_path) {
136 affected_paths.extend(child_paths.iter().cloned());
137 }
138
139 for path in affected_paths {
141 if let Some(nodes) = self.dependencies.get(&path) {
142 affected.extend(nodes.iter().copied());
143 }
144 }
145
146 affected
147 }
148
149 pub fn register_data_source_provider(&mut self, name: impl Into<String>) {
152 let name = name.into();
153 self.registered_providers.insert(name.clone());
154 self.unregistered_provider_refs.shift_remove(&name);
156 }
157
158 pub fn unregistered_provider_refs(&self) -> &IndexSet<String> {
162 &self.unregistered_provider_refs
163 }
164
165 pub fn get_data_source_affected_nodes(&self, provider: &str) -> IndexSet<NodeId> {
171 let prefix = format!("ds:{}:", provider);
172 let root = format!("ds:{}", provider);
173 let mut affected = IndexSet::new();
174
175 for (path, nodes) in &self.dependencies {
176 if path == &root || path.starts_with(&prefix) {
177 affected.extend(nodes.iter().copied());
178 }
179 }
180
181 affected
182 }
183
184 pub fn clear(&mut self) {
186 self.dependencies.clear();
187 self.node_bindings.clear();
188 self.prefix_index.clear();
189 self.unregistered_provider_refs.clear();
190 }
191}
192
193impl Default for DependencyGraph {
194 fn default() -> Self {
195 Self::new()
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn test_prefix_index_basic() {
205 use super::Binding;
206 use crate::ir::Element;
207 use crate::reconcile::tree::InstanceTree;
208
209 let mut graph = DependencyGraph::new();
210
211 let mut tree = InstanceTree::new();
213 let node1 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
214 let node2 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
215 let node3 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
216
217 let binding1 = Binding::state(vec!["user".to_string()]);
219 let binding2 = Binding::state(vec!["user".to_string(), "name".to_string()]);
220 let binding3 = Binding::state(vec!["user".to_string(), "email".to_string()]);
221
222 graph.add_dependency(node1, &binding1);
223 graph.add_dependency(node2, &binding2);
224 graph.add_dependency(node3, &binding3);
225
226 assert!(graph.prefix_index.contains_key("user"));
228 assert!(graph.prefix_index.contains_key("user.name"));
229 assert!(graph.prefix_index.contains_key("user.email"));
230
231 let user_paths = graph.prefix_index.get("user").unwrap();
233 assert_eq!(user_paths.len(), 3);
234 assert!(user_paths.contains("user"));
235 assert!(user_paths.contains("user.name"));
236 assert!(user_paths.contains("user.email"));
237 }
238
239 #[test]
240 fn test_get_affected_nodes_with_prefix_index() {
241 use super::Binding;
242 use crate::ir::Element;
243 use crate::reconcile::tree::InstanceTree;
244
245 let mut graph = DependencyGraph::new();
246
247 let mut tree = InstanceTree::new();
249 let node1 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
250 let node2 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
251 let node3 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
252 let node4 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
253
254 graph.add_dependency(node1, &Binding::state(vec!["user".to_string()]));
256 graph.add_dependency(
257 node2,
258 &Binding::state(vec!["user".to_string(), "name".to_string()]),
259 );
260 graph.add_dependency(
261 node3,
262 &Binding::state(vec!["user".to_string(), "email".to_string()]),
263 );
264 graph.add_dependency(node4, &Binding::state(vec!["posts".to_string()]));
265
266 let affected = graph.get_affected_nodes("user");
268 assert_eq!(affected.len(), 3);
269 assert!(affected.contains(&node1));
270 assert!(affected.contains(&node2));
271 assert!(affected.contains(&node3));
272 assert!(!affected.contains(&node4));
273 }
274
275 #[test]
276 fn test_get_affected_nodes_child_change() {
277 use super::Binding;
278 use crate::ir::Element;
279 use crate::reconcile::tree::InstanceTree;
280
281 let mut graph = DependencyGraph::new();
282
283 let mut tree = InstanceTree::new();
285 let node1 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
286 let node2 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
287
288 graph.add_dependency(node1, &Binding::state(vec!["user".to_string()]));
289 graph.add_dependency(
290 node2,
291 &Binding::state(vec!["user".to_string(), "name".to_string()]),
292 );
293
294 let affected = graph.get_affected_nodes("user.name");
296 assert_eq!(affected.len(), 2);
297 assert!(affected.contains(&node1)); assert!(affected.contains(&node2)); }
300
301 #[test]
302 fn test_get_affected_nodes_exact_match_only() {
303 use super::Binding;
304 use crate::ir::Element;
305 use crate::reconcile::tree::InstanceTree;
306
307 let mut graph = DependencyGraph::new();
308
309 let mut tree = InstanceTree::new();
311 let node1 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
312
313 graph.add_dependency(
314 node1,
315 &Binding::state(vec!["user".to_string(), "name".to_string()]),
316 );
317
318 let affected = graph.get_affected_nodes("user.email");
320 assert_eq!(affected.len(), 0);
321 }
322
323 #[test]
324 fn test_prefix_index_performance() {
325 use super::Binding;
326 use crate::ir::Element;
327 use crate::reconcile::tree::InstanceTree;
328
329 let mut graph = DependencyGraph::new();
330 let mut tree = InstanceTree::new();
331
332 for i in 0..100 {
334 let node_id = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
335 let path = vec![
336 "user".to_string(),
337 "profile".to_string(),
338 "details".to_string(),
339 format!("field{}", i),
340 ];
341 let binding = Binding::state(path);
342 graph.add_dependency(node_id, &binding);
343 }
344
345 let affected = graph.get_affected_nodes("user");
347 assert_eq!(affected.len(), 100);
348
349 let affected = graph.get_affected_nodes("user.profile");
350 assert_eq!(affected.len(), 100);
351
352 let affected = graph.get_affected_nodes("user.profile.details");
353 assert_eq!(affected.len(), 100);
354 }
355
356 #[test]
357 fn test_clear_clears_prefix_index() {
358 use super::Binding;
359 use crate::ir::Element;
360 use crate::reconcile::tree::InstanceTree;
361
362 let mut graph = DependencyGraph::new();
363
364 let mut tree = InstanceTree::new();
366 let node = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
367
368 graph.add_dependency(
369 node,
370 &Binding::state(vec!["user".to_string(), "name".to_string()]),
371 );
372 assert!(!graph.prefix_index.is_empty());
373
374 graph.clear();
375 assert!(graph.prefix_index.is_empty());
376 assert!(graph.dependencies.is_empty());
377 assert!(graph.node_bindings.is_empty());
378 }
379
380 #[test]
381 fn test_get_data_source_affected_nodes() {
382 use super::Binding;
383 use crate::ir::Element;
384 use crate::reconcile::tree::InstanceTree;
385
386 let mut graph = DependencyGraph::new();
387 let mut tree = InstanceTree::new();
388
389 let node1 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
390 let node2 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
391 let node3 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
392 let node4 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
393
394 graph.add_dependency(
396 node1,
397 &Binding::data_source("spacetime", vec!["messages".to_string()]),
398 );
399 graph.add_dependency(
400 node2,
401 &Binding::data_source(
402 "spacetime",
403 vec!["users".to_string(), "name".to_string()],
404 ),
405 );
406 graph.add_dependency(
407 node3,
408 &Binding::data_source("firebase", vec!["docs".to_string()]),
409 );
410 graph.add_dependency(node4, &Binding::state(vec!["count".to_string()]));
412
413 let affected = graph.get_data_source_affected_nodes("spacetime");
415 assert_eq!(affected.len(), 2);
416 assert!(affected.contains(&node1));
417 assert!(affected.contains(&node2));
418 assert!(!affected.contains(&node3)); assert!(!affected.contains(&node4)); let affected_old = graph.get_affected_nodes("ds:spacetime");
424 assert_eq!(
425 affected_old.len(),
426 0,
427 "get_affected_nodes should miss colon-separated children"
428 );
429 }
430}