Skip to main content

hypen_engine/
engine.rs

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