use serde::{Deserialize, Serialize};
use std::collections::HashMap;
pub const MAX_NODE_IDS_PER_REQUEST: usize = 50;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct TrackedNode {
pub node_id: String,
pub name: String,
pub child_count: usize,
pub structure_hash: String,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct NodeTracker {
tracked_nodes: HashMap<String, TrackedNode>,
}
impl NodeTracker {
pub fn new() -> Self {
NodeTracker { tracked_nodes: HashMap::new() }
}
pub fn track(&mut self, node_id: String, name: String, children: &[(String, String)]) {
let structure_hash = compute_structure_hash(children);
self.tracked_nodes.insert(
node_id.clone(),
TrackedNode { node_id, name, child_count: children.len(), structure_hash },
);
}
pub fn diff(&self, new_nodes: &[(String, String, Vec<(String, String)>)]) -> NodeDiff {
let mut changed = Vec::new();
let mut added = Vec::new();
let mut removed = Vec::new();
let new_node_ids: std::collections::HashSet<_> =
new_nodes.iter().map(|(id, _, _)| id.clone()).collect();
for (node_id, name, children) in new_nodes {
let new_hash = compute_structure_hash(children);
match self.tracked_nodes.get(node_id) {
Some(tracked) if tracked.structure_hash != new_hash => {
changed.push(node_id.clone());
}
None => {
added.push(node_id.clone());
}
_ => {} }
let _ = name; }
for node_id in self.tracked_nodes.keys() {
if !new_node_ids.contains(node_id) {
removed.push(node_id.clone());
}
}
NodeDiff { changed, added, removed }
}
pub fn batch_node_ids(node_ids: &[String]) -> Vec<Vec<String>> {
node_ids.chunks(MAX_NODE_IDS_PER_REQUEST).map(|chunk| chunk.to_vec()).collect()
}
pub fn len(&self) -> usize {
self.tracked_nodes.len()
}
pub fn is_empty(&self) -> bool {
self.tracked_nodes.is_empty()
}
}
#[derive(Clone, Debug)]
pub struct NodeDiff {
pub changed: Vec<String>,
pub added: Vec<String>,
pub removed: Vec<String>,
}
impl NodeDiff {
pub fn fetch_ids(&self) -> Vec<String> {
let mut ids = self.changed.clone();
ids.extend(self.added.iter().cloned());
ids
}
pub fn has_changes(&self) -> bool {
!self.changed.is_empty() || !self.added.is_empty() || !self.removed.is_empty()
}
}
fn compute_structure_hash(children: &[(String, String)]) -> String {
use std::hash::{Hash, Hasher};
struct Fnv1a(u64);
impl Fnv1a {
fn new() -> Self {
Fnv1a(0xcbf29ce484222325)
}
}
impl Hasher for Fnv1a {
fn write(&mut self, bytes: &[u8]) {
for &b in bytes {
self.0 ^= b as u64;
self.0 = self.0.wrapping_mul(0x00000100000001B3);
}
}
fn finish(&self) -> u64 {
self.0
}
}
let mut hasher = Fnv1a::new();
for (id, name) in children {
id.hash(&mut hasher);
name.hash(&mut hasher);
}
format!("{:016x}", hasher.finish())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_tracker_all_added() {
let tracker = NodeTracker::new();
let new_nodes = vec![
("1:1".to_string(), "Frame1".to_string(), vec![]),
("1:2".to_string(), "Frame2".to_string(), vec![]),
];
let diff = tracker.diff(&new_nodes);
assert_eq!(diff.added.len(), 2);
assert!(diff.changed.is_empty());
assert!(diff.removed.is_empty());
}
#[test]
fn test_no_changes() {
let children = vec![("c1".to_string(), "Child1".to_string())];
let mut tracker = NodeTracker::new();
tracker.track("1:1".to_string(), "Frame1".to_string(), &children);
let new_nodes = vec![("1:1".to_string(), "Frame1".to_string(), children)];
let diff = tracker.diff(&new_nodes);
assert!(!diff.has_changes());
}
#[test]
fn test_child_added() {
let old_children = vec![("c1".to_string(), "Child1".to_string())];
let mut tracker = NodeTracker::new();
tracker.track("1:1".to_string(), "Frame1".to_string(), &old_children);
let new_children = vec![
("c1".to_string(), "Child1".to_string()),
("c2".to_string(), "Child2".to_string()),
];
let new_nodes = vec![("1:1".to_string(), "Frame1".to_string(), new_children)];
let diff = tracker.diff(&new_nodes);
assert_eq!(diff.changed, vec!["1:1"]);
}
#[test]
fn test_node_removed() {
let mut tracker = NodeTracker::new();
tracker.track("1:1".to_string(), "Frame1".to_string(), &[]);
tracker.track("1:2".to_string(), "Frame2".to_string(), &[]);
let new_nodes = vec![("1:1".to_string(), "Frame1".to_string(), vec![])];
let diff = tracker.diff(&new_nodes);
assert_eq!(diff.removed, vec!["1:2"]);
}
#[test]
fn test_batch_node_ids() {
let ids: Vec<String> = (0..120).map(|i| format!("node_{}", i)).collect();
let batches = NodeTracker::batch_node_ids(&ids);
assert_eq!(batches.len(), 3); assert_eq!(batches[0].len(), 50);
assert_eq!(batches[1].len(), 50);
assert_eq!(batches[2].len(), 20);
}
}