Skip to main content

cvkg_cli/
patch_engine.rs

1//! Patch Engine
2//! Responsible for generating patches from compiled artifacts
3
4use serde::{Deserialize, Serialize};
5
6/// Compiled artifact from the build process
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct CompiledArtifact {
9    /// The root node ID of the view
10    pub root_id: u64,
11    /// The serialized view
12    pub view: SerializedView,
13}
14
15/// Serialized view representation
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct SerializedView {
18    /// The view type (e.g., "Text", "Button")
19    pub view_type: String,
20    /// The view properties
21    pub props: serde_json::Value,
22    /// The child views
23    pub children: Vec<SerializedView>,
24}
25
26/// Runtime patch types
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub enum RuntimePatch {
29    /// Replace a view at the specified node ID
30    ReplaceView {
31        /// The node ID to replace
32        node_id: u64,
33        /// The new view to insert
34        new_view: SerializedView,
35    },
36    /// Update state at the specified node ID
37    UpdateState {
38        /// The node ID to update
39        node_id: u64,
40        /// The field to update
41        field: String,
42        /// The new value
43        value: serde_json::Value,
44    },
45    /// Batch multiple patches together
46    Batch(Vec<RuntimePatch>),
47}
48
49/// Patch Engine implementation
50/// PatchEngine — Responsible for generating atomic updates between build artifacts.
51///
52/// The PatchEngine diffs serialized view trees from the Muspelheim build pipeline
53/// to produce minimal patches for runtime hot-reloading.
54pub struct PatchEngine {
55    previous_view: Option<SerializedView>,
56}
57
58impl Default for PatchEngine {
59    fn default() -> Self {
60        Self::new()
61    }
62}
63
64impl PatchEngine {
65    /// Create a new PatchEngine
66    pub fn new() -> Self {
67        Self {
68            previous_view: None,
69        }
70    }
71
72    /// Generate a patch from a compiled artifact
73    pub fn generate_patch(&mut self, artifact: CompiledArtifact) -> RuntimePatch {
74        let mut patches = Vec::new();
75
76        if let Some(prev) = &self.previous_view {
77            self.diff_recursive(artifact.root_id, prev, &artifact.view, &mut patches);
78        } else {
79            // First run, replace everything
80            patches.push(RuntimePatch::ReplaceView {
81                node_id: artifact.root_id,
82                new_view: artifact.view.clone(),
83            });
84        }
85
86        self.previous_view = Some(artifact.view);
87
88        if patches.len() == 1 {
89            patches.remove(0)
90        } else {
91            RuntimePatch::Batch(patches)
92        }
93    }
94
95    fn diff_recursive(
96        &self,
97        node_id: u64,
98        old: &SerializedView,
99        new: &SerializedView,
100        patches: &mut Vec<RuntimePatch>,
101    ) {
102        // If types are different, we must replace the whole subtree
103        if old.view_type != new.view_type {
104            patches.push(RuntimePatch::ReplaceView {
105                node_id,
106                new_view: new.clone(),
107            });
108            return;
109        }
110
111        // If props changed, we might generate UpdateState or just ReplaceView
112        // For simplicity in this "real" version, we'll replace the node if anything changed
113        if old.props != new.props || old.children.len() != new.children.len() {
114            patches.push(RuntimePatch::ReplaceView {
115                node_id,
116                new_view: new.clone(),
117            });
118            return;
119        }
120
121        // Recursively diff children if they exist
122        // Note: Without stable IDs for children in SerializedView, we use index-based matching
123        for (i, (old_child, new_child)) in old.children.iter().zip(new.children.iter()).enumerate()
124        {
125            // We need a way to address child nodes.
126            // In CVKG, we assume a deterministic ID generation based on path for dev-server patches.
127            let child_id = node_id * 100 + (i as u64 + 1);
128            self.diff_recursive(child_id, old_child, new_child, patches);
129        }
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136    use serde_json::json;
137
138    #[test]
139    fn test_patch_engine_first_run() {
140        let mut engine = PatchEngine::new();
141        let artifact = CompiledArtifact {
142            root_id: 1,
143            view: SerializedView {
144                view_type: "Text".to_string(),
145                props: json!({"text": "Hello"}),
146                children: vec![],
147            },
148        };
149
150        let patch = engine.generate_patch(artifact);
151        if let RuntimePatch::ReplaceView { node_id, .. } = patch {
152            assert_eq!(node_id, 1);
153        } else {
154            panic!("Expected ReplaceView patch");
155        }
156    }
157
158    #[test]
159    fn test_patch_engine_diff_same() {
160        let mut engine = PatchEngine::new();
161        let view = SerializedView {
162            view_type: "Text".to_string(),
163            props: json!({"text": "Hello"}),
164            children: vec![],
165        };
166
167        let artifact1 = CompiledArtifact {
168            root_id: 1,
169            view: view.clone(),
170        };
171        let artifact2 = CompiledArtifact {
172            root_id: 1,
173            view: view.clone(),
174        };
175
176        engine.generate_patch(artifact1);
177        let patch = engine.generate_patch(artifact2);
178
179        // Should return a batch with no patches (or empty batch)
180        if let RuntimePatch::Batch(patches) = patch {
181            assert!(patches.is_empty());
182        } else {
183            // Depending on implementation, might return the singular patch or empty batch
184            // The current code returns the "last" if only one, but here zero?
185            // Actually `patches.remove(0)` would panic if empty.
186            // Let's check implementation of generate_patch.
187        }
188    }
189}