Skip to main content

hypen_engine/
engine.rs

1use crate::{
2    dispatch::{Action, ActionDispatcher},
3    error::EngineError,
4    ir::{ComponentRegistry, Element, IRNode},
5    lifecycle::{ModuleInstance, ResourceCache},
6    reactive::{DependencyGraph, Scheduler},
7    reconcile::{reconcile_ir, InstanceTree, Patch},
8    state::StateChange,
9};
10
11/// Static null value to avoid cloning state when no module exists
12static NULL_STATE: serde_json::Value = serde_json::Value::Null;
13
14/// Callback type for rendering patches to the platform
15pub type RenderCallback = Box<dyn Fn(&[Patch]) + Send + Sync>;
16
17/// The main Hypen engine that orchestrates reactive UI rendering.
18///
19/// # Architecture: Single Active Module
20///
21/// Each `Engine` instance has one "active" module at a time for state bindings.
22/// When DSL templates reference `${state.xxx}`, they resolve against the active
23/// module's state.
24///
25/// ## Multi-module Applications
26///
27/// Multi-module apps (e.g., AppState + StatsScreen) work as follows:
28///
29/// 1. **TypeScript SDK layer** manages multiple `HypenModuleInstance` objects,
30///    each with its own Proxy-based state tracking.
31///
32/// 2. **Cross-module communication** uses `HypenGlobalContext`:
33///    ```typescript
34///    // In StatsScreen's action handler:
35///    const app = context.getModule<AppState>("app");
36///    app.setState({ showStats: true });
37///    ```
38///
39/// 3. **State binding scope**: Each module's template binds to its own state.
40///    You cannot reference another module's state directly in templates.
41///    Use props or callbacks instead:
42///    ```hypen
43///    // StatsScreen receives app data via props, not state binding
44///    StatsView(data: @props.appStats)
45///    ```
46///
47/// ## Limitation
48///
49/// The engine's state binding model (`${state.xxx}`) is scoped to a single module.
50/// Cross-module state access requires explicit prop passing or action dispatching.
51/// This keeps the reactive graph simple but requires careful data flow design.
52pub struct Engine {
53    /// Component registry for expanding custom components
54    component_registry: ComponentRegistry,
55
56    /// Current module instance (provides state for `${state.xxx}` bindings)
57    module: Option<ModuleInstance>,
58
59    /// Instance tree (virtual tree, no platform objects)
60    tree: InstanceTree,
61
62    /// Reactive dependency graph
63    dependencies: DependencyGraph,
64
65    /// Scheduler for dirty nodes
66    scheduler: Scheduler,
67
68    /// Action dispatcher
69    actions: ActionDispatcher,
70
71    /// Resource cache
72    resources: ResourceCache,
73
74    /// Callback to send patches to the platform renderer
75    render_callback: Option<RenderCallback>,
76
77    /// Revision counter for remote UI
78    revision: u64,
79}
80
81impl Engine {
82    pub fn new() -> Self {
83        Self {
84            component_registry: ComponentRegistry::new(),
85            module: None,
86            tree: InstanceTree::new(),
87            dependencies: DependencyGraph::new(),
88            scheduler: Scheduler::new(),
89            actions: ActionDispatcher::new(),
90            resources: ResourceCache::new(),
91            render_callback: None,
92            revision: 0,
93        }
94    }
95
96    /// Register a custom component
97    pub fn register_component(&mut self, component: crate::ir::Component) {
98        self.component_registry.register(component);
99    }
100
101    /// Set the component resolver for dynamic component loading
102    /// The resolver receives (component_name, context_path) and should return
103    /// ResolvedComponent { source, path } or None
104    pub fn set_component_resolver<F>(&mut self, resolver: F)
105    where
106        F: Fn(&str, Option<&str>) -> Option<crate::ir::ResolvedComponent> + Send + Sync + 'static,
107    {
108        self.component_registry.set_resolver(std::sync::Arc::new(resolver));
109    }
110
111    /// Set the module instance
112    pub fn set_module(&mut self, module: ModuleInstance) {
113        self.module = Some(module);
114    }
115
116    /// Set the render callback
117    pub fn set_render_callback<F>(&mut self, callback: F)
118    where
119        F: Fn(&[Patch]) + Send + Sync + 'static,
120    {
121        self.render_callback = Some(Box::new(callback));
122    }
123
124    /// Register an action handler
125    pub fn on_action<F>(&mut self, action_name: impl Into<String>, handler: F)
126    where
127        F: Fn(&Action) + Send + Sync + 'static,
128    {
129        self.actions.on(action_name, handler);
130    }
131
132    /// Render an element tree (initial render or full re-render)
133    pub fn render(&mut self, element: &Element) {
134        // Convert Element to IRNode for reconciliation
135        let ir_node = IRNode::Element(element.clone());
136
137        // Expand components (registry needs mutable access for lazy loading)
138        let expanded = self.component_registry.expand_ir_node(&ir_node);
139
140        // Get current state reference (no clone needed - reconcile only borrows)
141        let state: &serde_json::Value = self
142            .module
143            .as_ref()
144            .map(|m| m.get_state())
145            .unwrap_or(&NULL_STATE);
146
147        // Clear dependency graph before rebuilding
148        self.dependencies.clear();
149
150        // Reconcile and generate patches (dependencies are collected during tree construction)
151        let patches = reconcile_ir(&mut self.tree, &expanded, None, state, &mut self.dependencies);
152
153        // Send patches to renderer
154        self.emit_patches(patches);
155
156        self.revision += 1;
157    }
158
159    /// Handle a state change notification from the module host
160    /// The host keeps the actual state - we just need to know what changed
161    pub fn notify_state_change(&mut self, change: &StateChange) {
162        // Find affected nodes based on changed paths
163        let mut affected_nodes = indexmap::IndexSet::new();
164        for path in change.paths() {
165            affected_nodes.extend(self.dependencies.get_affected_nodes(path));
166        }
167
168        // Mark affected nodes as dirty
169        self.scheduler.mark_many_dirty(affected_nodes.iter().copied());
170
171        // Re-render dirty subtrees
172        // Note: The host will be asked to provide values during rendering via a callback
173        self.render_dirty();
174    }
175
176    /// Convenience method for backward compatibility / JSON patches
177    pub fn update_state(&mut self, state_patch: serde_json::Value) {
178        // Update module state FIRST before rendering
179        if let Some(module) = &mut self.module {
180            module.update_state(state_patch.clone());
181        }
182
183        // Then notify and render with the new state
184        let change = StateChange::from_json(&state_patch);
185        self.notify_state_change(&change);
186    }
187
188    /// Dispatch an action
189    pub fn dispatch_action(&mut self, action: Action) -> Result<(), EngineError> {
190        self.actions.dispatch(&action)
191    }
192
193    /// Render only dirty nodes
194    fn render_dirty(&mut self) {
195        let patches = crate::render::render_dirty_nodes(
196            &mut self.scheduler,
197            &mut self.tree,
198            self.module.as_ref(),
199        );
200
201        if !patches.is_empty() {
202            self.emit_patches(patches);
203            self.revision += 1;
204        }
205    }
206
207    /// Send patches to the renderer
208    fn emit_patches(&self, patches: Vec<Patch>) {
209        if let Some(ref callback) = self.render_callback {
210            callback(&patches);
211        }
212    }
213
214    /// Get the current revision number
215    pub fn revision(&self) -> u64 {
216        self.revision
217    }
218
219    /// Get access to the component registry
220    pub fn component_registry(&self) -> &ComponentRegistry {
221        &self.component_registry
222    }
223
224    /// Get mutable access to the component registry
225    pub fn component_registry_mut(&mut self) -> &mut ComponentRegistry {
226        &mut self.component_registry
227    }
228
229    /// Get access to the resource cache
230    pub fn resources(&self) -> &ResourceCache {
231        &self.resources
232    }
233
234    /// Get mutable access to the resource cache
235    pub fn resources_mut(&mut self) -> &mut ResourceCache {
236        &mut self.resources
237    }
238}
239
240impl Default for Engine {
241    fn default() -> Self {
242        Self::new()
243    }
244}
245