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(
52 &mut self,
53 node_id: NodeId,
54 binding: &Binding,
55 module_scope: Option<&str>,
56 ) {
57 use super::BindingSource;
58 let path = match &binding.source {
59 BindingSource::State => {
60 if let Some(scope) = module_scope {
61 format!("mod:{}:{}", scope, binding.full_path())
62 } else {
63 binding.full_path()
64 }
65 }
66 BindingSource::Item => return, BindingSource::DataSource(provider) => {
68 if !self.registered_providers.contains(provider.as_str()) {
70 self.unregistered_provider_refs.insert(provider.clone());
71 }
72 format!("ds:{}:{}", provider, binding.full_path())
73 }
74 };
75
76 self.dependencies
78 .entry(path.clone())
79 .or_default()
80 .insert(node_id);
81
82 self.node_bindings
84 .entry(node_id)
85 .or_default()
86 .insert(path.clone());
87
88 self.add_path_to_prefix_index(&path);
91 }
92
93 fn add_path_to_prefix_index(&mut self, path: &str) {
95 self.prefix_index
97 .entry(path.to_string())
98 .or_default()
99 .insert(path.to_string());
100
101 let mut current = path;
103 while let Some(dot_pos) = current.rfind('.') {
104 current = ¤t[..dot_pos];
105 self.prefix_index
106 .entry(current.to_string())
107 .or_default()
108 .insert(path.to_string());
109 }
110 }
111
112 pub fn remove_node(&mut self, node_id: NodeId) {
114 if let Some(paths) = self.node_bindings.shift_remove(&node_id) {
115 for path in paths {
116 if let Some(nodes) = self.dependencies.get_mut(&path) {
117 nodes.shift_remove(&node_id);
118 }
119 }
120 }
121 }
122
123 pub fn get_dependent_nodes(&self, path: &str) -> Option<&IndexSet<NodeId>> {
125 self.dependencies.get(path)
126 }
127
128 pub fn get_affected_nodes(&self, changed_path: &str) -> IndexSet<NodeId> {
132 let mut affected = IndexSet::new();
133 let mut affected_paths = IndexSet::new();
134
135 if self.dependencies.contains_key(changed_path) {
138 affected_paths.insert(changed_path.to_string());
139 }
140
141 let mut current = changed_path;
143 while let Some(dot_pos) = current.rfind('.') {
144 current = ¤t[..dot_pos];
145 if self.dependencies.contains_key(current) {
146 affected_paths.insert(current.to_string());
147 }
148 }
149
150 if let Some(child_paths) = self.prefix_index.get(changed_path) {
153 affected_paths.extend(child_paths.iter().cloned());
154 }
155
156 for path in affected_paths {
158 if let Some(nodes) = self.dependencies.get(&path) {
159 affected.extend(nodes.iter().copied());
160 }
161 }
162
163 affected
164 }
165
166 pub fn register_data_source_provider(&mut self, name: impl Into<String>) {
169 let name = name.into();
170 self.registered_providers.insert(name.clone());
171 self.unregistered_provider_refs.shift_remove(&name);
173 }
174
175 pub fn unregistered_provider_refs(&self) -> &IndexSet<String> {
179 &self.unregistered_provider_refs
180 }
181
182 pub fn get_data_source_affected_nodes(&self, provider: &str) -> IndexSet<NodeId> {
188 let prefix = format!("ds:{}:", provider);
189 let root = format!("ds:{}", provider);
190 let mut affected = IndexSet::new();
191
192 for (path, nodes) in &self.dependencies {
193 if path == &root || path.starts_with(&prefix) {
194 affected.extend(nodes.iter().copied());
195 }
196 }
197
198 affected
199 }
200
201 pub fn clear(&mut self) {
203 self.dependencies.clear();
204 self.node_bindings.clear();
205 self.prefix_index.clear();
206 self.unregistered_provider_refs.clear();
207 }
208}
209
210impl Default for DependencyGraph {
211 fn default() -> Self {
212 Self::new()
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[test]
221 fn test_prefix_index_basic() {
222 use super::Binding;
223 use crate::ir::Element;
224 use crate::reconcile::tree::InstanceTree;
225
226 let mut graph = DependencyGraph::new();
227
228 let mut tree = InstanceTree::new();
230 let node1 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
231 let node2 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
232 let node3 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
233
234 let binding1 = Binding::state(vec!["user".to_string()]);
236 let binding2 = Binding::state(vec!["user".to_string(), "name".to_string()]);
237 let binding3 = Binding::state(vec!["user".to_string(), "email".to_string()]);
238
239 graph.add_dependency(node1, &binding1, None);
240 graph.add_dependency(node2, &binding2, None);
241 graph.add_dependency(node3, &binding3, None);
242
243 assert!(graph.prefix_index.contains_key("user"));
245 assert!(graph.prefix_index.contains_key("user.name"));
246 assert!(graph.prefix_index.contains_key("user.email"));
247
248 let user_paths = graph.prefix_index.get("user").unwrap();
250 assert_eq!(user_paths.len(), 3);
251 assert!(user_paths.contains("user"));
252 assert!(user_paths.contains("user.name"));
253 assert!(user_paths.contains("user.email"));
254 }
255
256 #[test]
257 fn test_get_affected_nodes_with_prefix_index() {
258 use super::Binding;
259 use crate::ir::Element;
260 use crate::reconcile::tree::InstanceTree;
261
262 let mut graph = DependencyGraph::new();
263
264 let mut tree = InstanceTree::new();
266 let node1 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
267 let node2 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
268 let node3 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
269 let node4 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
270
271 graph.add_dependency(node1, &Binding::state(vec!["user".to_string()]), None);
273 graph.add_dependency(
274 node2,
275 &Binding::state(vec!["user".to_string(), "name".to_string()]),
276 None,
277 );
278 graph.add_dependency(
279 node3,
280 &Binding::state(vec!["user".to_string(), "email".to_string()]),
281 None,
282 );
283 graph.add_dependency(node4, &Binding::state(vec!["posts".to_string()]), None);
284
285 let affected = graph.get_affected_nodes("user");
287 assert_eq!(affected.len(), 3);
288 assert!(affected.contains(&node1));
289 assert!(affected.contains(&node2));
290 assert!(affected.contains(&node3));
291 assert!(!affected.contains(&node4));
292 }
293
294 #[test]
295 fn test_get_affected_nodes_child_change() {
296 use super::Binding;
297 use crate::ir::Element;
298 use crate::reconcile::tree::InstanceTree;
299
300 let mut graph = DependencyGraph::new();
301
302 let mut tree = InstanceTree::new();
304 let node1 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
305 let node2 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
306
307 graph.add_dependency(node1, &Binding::state(vec!["user".to_string()]), None);
308 graph.add_dependency(
309 node2,
310 &Binding::state(vec!["user".to_string(), "name".to_string()]),
311 None,
312 );
313
314 let affected = graph.get_affected_nodes("user.name");
316 assert_eq!(affected.len(), 2);
317 assert!(affected.contains(&node1)); assert!(affected.contains(&node2)); }
320
321 #[test]
322 fn test_get_affected_nodes_exact_match_only() {
323 use super::Binding;
324 use crate::ir::Element;
325 use crate::reconcile::tree::InstanceTree;
326
327 let mut graph = DependencyGraph::new();
328
329 let mut tree = InstanceTree::new();
331 let node1 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
332
333 graph.add_dependency(
334 node1,
335 &Binding::state(vec!["user".to_string(), "name".to_string()]),
336 None,
337 );
338
339 let affected = graph.get_affected_nodes("user.email");
341 assert_eq!(affected.len(), 0);
342 }
343
344 #[test]
345 fn test_prefix_index_performance() {
346 use super::Binding;
347 use crate::ir::Element;
348 use crate::reconcile::tree::InstanceTree;
349
350 let mut graph = DependencyGraph::new();
351 let mut tree = InstanceTree::new();
352
353 for i in 0..100 {
355 let node_id = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
356 let path = vec![
357 "user".to_string(),
358 "profile".to_string(),
359 "details".to_string(),
360 format!("field{}", i),
361 ];
362 let binding = Binding::state(path);
363 graph.add_dependency(node_id, &binding, None);
364 }
365
366 let affected = graph.get_affected_nodes("user");
368 assert_eq!(affected.len(), 100);
369
370 let affected = graph.get_affected_nodes("user.profile");
371 assert_eq!(affected.len(), 100);
372
373 let affected = graph.get_affected_nodes("user.profile.details");
374 assert_eq!(affected.len(), 100);
375 }
376
377 #[test]
378 fn test_clear_clears_prefix_index() {
379 use super::Binding;
380 use crate::ir::Element;
381 use crate::reconcile::tree::InstanceTree;
382
383 let mut graph = DependencyGraph::new();
384
385 let mut tree = InstanceTree::new();
387 let node = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
388
389 graph.add_dependency(
390 node,
391 &Binding::state(vec!["user".to_string(), "name".to_string()]),
392 None,
393 );
394 assert!(!graph.prefix_index.is_empty());
395
396 graph.clear();
397 assert!(graph.prefix_index.is_empty());
398 assert!(graph.dependencies.is_empty());
399 assert!(graph.node_bindings.is_empty());
400 }
401
402 #[test]
403 fn test_get_data_source_affected_nodes() {
404 use super::Binding;
405 use crate::ir::Element;
406 use crate::reconcile::tree::InstanceTree;
407
408 let mut graph = DependencyGraph::new();
409 let mut tree = InstanceTree::new();
410
411 let node1 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
412 let node2 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
413 let node3 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
414 let node4 = tree.create_node(&Element::new("Text"), &serde_json::json!({}));
415
416 graph.add_dependency(
418 node1,
419 &Binding::data_source("spacetime", vec!["messages".to_string()]),
420 None,
421 );
422 graph.add_dependency(
423 node2,
424 &Binding::data_source("spacetime", vec!["users".to_string(), "name".to_string()]),
425 None,
426 );
427 graph.add_dependency(
428 node3,
429 &Binding::data_source("firebase", vec!["docs".to_string()]),
430 None,
431 );
432 graph.add_dependency(node4, &Binding::state(vec!["count".to_string()]), None);
434
435 let affected = graph.get_data_source_affected_nodes("spacetime");
437 assert_eq!(affected.len(), 2);
438 assert!(affected.contains(&node1));
439 assert!(affected.contains(&node2));
440 assert!(!affected.contains(&node3)); assert!(!affected.contains(&node4)); let affected_old = graph.get_affected_nodes("ds:spacetime");
446 assert_eq!(
447 affected_old.len(),
448 0,
449 "get_affected_nodes should miss colon-separated children"
450 );
451 }
452}