hypen_engine/
engine.rs

1use crate::{
2    dispatch::{Action, ActionDispatcher},
3    ir::{ComponentRegistry, Element},
4    lifecycle::{ComponentLifecycle, ModuleInstance, ResourceCache},
5    reactive::{DependencyGraph, Scheduler},
6    reconcile::{reconcile, InstanceTree, Patch},
7    state::StateChange,
8};
9
10/// Callback type for rendering patches to the platform
11pub type RenderCallback = Box<dyn Fn(&[Patch]) + Send + Sync>;
12
13/// The main Hypen engine that orchestrates everything
14pub struct Engine {
15    /// Component registry for expanding custom components
16    component_registry: ComponentRegistry,
17
18    /// Current module instance
19    module: Option<ModuleInstance>,
20
21    /// Instance tree (virtual tree, no platform objects)
22    tree: InstanceTree,
23
24    /// Reactive dependency graph
25    dependencies: DependencyGraph,
26
27    /// Scheduler for dirty nodes
28    scheduler: Scheduler,
29
30    /// Action dispatcher
31    actions: ActionDispatcher,
32
33    /// Component lifecycle manager
34    lifecycle: ComponentLifecycle,
35
36    /// Resource cache
37    resources: ResourceCache,
38
39    /// Callback to send patches to the platform renderer
40    render_callback: Option<RenderCallback>,
41
42    /// Revision counter for remote UI
43    revision: u64,
44}
45
46impl Engine {
47    pub fn new() -> Self {
48        Self {
49            component_registry: ComponentRegistry::new(),
50            module: None,
51            tree: InstanceTree::new(),
52            dependencies: DependencyGraph::new(),
53            scheduler: Scheduler::new(),
54            actions: ActionDispatcher::new(),
55            lifecycle: ComponentLifecycle::new(),
56            resources: ResourceCache::new(),
57            render_callback: None,
58            revision: 0,
59        }
60    }
61
62    /// Register a custom component
63    pub fn register_component(&mut self, component: crate::ir::Component) {
64        self.component_registry.register(component);
65    }
66
67    /// Set the component resolver for dynamic component loading
68    /// The resolver receives (component_name, context_path) and should return
69    /// ResolvedComponent { source, path } or None
70    pub fn set_component_resolver<F>(&mut self, resolver: F)
71    where
72        F: Fn(&str, Option<&str>) -> Option<crate::ir::ResolvedComponent> + Send + Sync + 'static,
73    {
74        self.component_registry.set_resolver(std::sync::Arc::new(resolver));
75    }
76
77    /// Set the module instance
78    pub fn set_module(&mut self, module: ModuleInstance) {
79        self.module = Some(module);
80    }
81
82    /// Set the render callback
83    pub fn set_render_callback<F>(&mut self, callback: F)
84    where
85        F: Fn(&[Patch]) + Send + Sync + 'static,
86    {
87        self.render_callback = Some(Box::new(callback));
88    }
89
90    /// Register an action handler
91    pub fn on_action<F>(&mut self, action_name: impl Into<String>, handler: F)
92    where
93        F: Fn(&Action) + Send + Sync + 'static,
94    {
95        self.actions.on(action_name, handler);
96    }
97
98    /// Render an element tree (initial render or full re-render)
99    pub fn render(&mut self, element: &Element) {
100        // Expand components (registry needs mutable access for lazy loading)
101        let expanded = self.component_registry.expand(element);
102
103        // Get current state (clone to avoid borrow conflicts)
104        let state = self
105            .module
106            .as_ref()
107            .map(|m| m.get_state().clone())
108            .unwrap_or(serde_json::Value::Null);
109
110        // Clear dependency graph before rebuilding
111        self.dependencies.clear();
112
113        // Reconcile and generate patches (dependencies are collected during tree construction)
114        let patches = reconcile(&mut self.tree, &expanded, None, &state, &mut self.dependencies);
115
116        // Send patches to renderer
117        self.emit_patches(patches);
118
119        self.revision += 1;
120    }
121
122    /// Handle a state change notification from the module host
123    /// The host keeps the actual state - we just need to know what changed
124    pub fn notify_state_change(&mut self, change: &StateChange) {
125        // Find affected nodes based on changed paths
126        let mut affected_nodes = indexmap::IndexSet::new();
127        for path in change.paths() {
128            affected_nodes.extend(self.dependencies.get_affected_nodes(path));
129        }
130
131        // Mark affected nodes as dirty
132        self.scheduler.mark_many_dirty(affected_nodes.iter().copied());
133
134        // Re-render dirty subtrees
135        // Note: The host will be asked to provide values during rendering via a callback
136        self.render_dirty();
137    }
138
139    /// Convenience method for backward compatibility / JSON patches
140    pub fn update_state(&mut self, state_patch: serde_json::Value) {
141        // Update module state FIRST before rendering
142        if let Some(module) = &mut self.module {
143            module.update_state(state_patch.clone());
144        }
145
146        // Then notify and render with the new state
147        let change = StateChange::from_json(&state_patch);
148        self.notify_state_change(&change);
149    }
150
151    /// Dispatch an action
152    pub fn dispatch_action(&mut self, action: Action) -> Result<(), String> {
153        self.actions.dispatch(&action)
154    }
155
156    /// Render only dirty nodes
157    fn render_dirty(&mut self) {
158        let patches = crate::render::render_dirty_nodes(
159            &mut self.scheduler,
160            &mut self.tree,
161            self.module.as_ref(),
162        );
163
164        if !patches.is_empty() {
165            self.emit_patches(patches);
166            self.revision += 1;
167        }
168    }
169
170    /// Send patches to the renderer
171    fn emit_patches(&self, patches: Vec<Patch>) {
172        if let Some(ref callback) = self.render_callback {
173            callback(&patches);
174        }
175    }
176
177    /// Get the current revision number
178    pub fn revision(&self) -> u64 {
179        self.revision
180    }
181
182    /// Get access to the component registry
183    pub fn component_registry(&self) -> &ComponentRegistry {
184        &self.component_registry
185    }
186
187    /// Get mutable access to the component registry
188    pub fn component_registry_mut(&mut self) -> &mut ComponentRegistry {
189        &mut self.component_registry
190    }
191
192    /// Get access to the resource cache
193    pub fn resources(&self) -> &ResourceCache {
194        &self.resources
195    }
196
197    /// Get mutable access to the resource cache
198    pub fn resources_mut(&mut self) -> &mut ResourceCache {
199        &mut self.resources
200    }
201}
202
203impl Default for Engine {
204    fn default() -> Self {
205        Self::new()
206    }
207}
208
209/// Extract changed paths from a state patch
210fn extract_changed_paths(patch: &serde_json::Value) -> Vec<String> {
211    let mut paths = Vec::new();
212    extract_paths_recursive(patch, String::new(), &mut paths);
213    paths
214}
215
216fn extract_paths_recursive(value: &serde_json::Value, prefix: String, paths: &mut Vec<String>) {
217    match value {
218        serde_json::Value::Object(map) => {
219            for (key, val) in map {
220                let path = if prefix.is_empty() {
221                    key.clone()
222                } else {
223                    format!("{}.{}", prefix, key)
224                };
225                paths.push(path.clone());
226                extract_paths_recursive(val, path, paths);
227            }
228        }
229        _ => {
230            if !prefix.is_empty() {
231                paths.push(prefix);
232            }
233        }
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240    use serde_json::json;
241
242    #[test]
243    fn test_extract_changed_paths() {
244        let patch = json!({
245            "user": {
246                "name": "Alice",
247                "age": 30
248            }
249        });
250
251        let paths = extract_changed_paths(&patch);
252        assert!(paths.contains(&"user".to_string()));
253        assert!(paths.contains(&"user.name".to_string()));
254        assert!(paths.contains(&"user.age".to_string()));
255    }
256}