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