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
109            .set_resolver(std::sync::Arc::new(resolver));
110    }
111
112    /// Set the module instance
113    pub fn set_module(&mut self, module: ModuleInstance) {
114        self.module = Some(module);
115    }
116
117    /// Set the render callback
118    pub fn set_render_callback<F>(&mut self, callback: F)
119    where
120        F: Fn(&[Patch]) + Send + Sync + 'static,
121    {
122        self.render_callback = Some(Box::new(callback));
123    }
124
125    /// Register an action handler
126    pub fn on_action<F>(&mut self, action_name: impl Into<String>, handler: F)
127    where
128        F: Fn(&Action) + Send + Sync + 'static,
129    {
130        self.actions.on(action_name, handler);
131    }
132
133    /// Render an element tree (initial render or full re-render)
134    pub fn render(&mut self, element: &Element) {
135        // Convert Element to IRNode for reconciliation
136        let ir_node = IRNode::Element(element.clone());
137
138        // Expand components (registry needs mutable access for lazy loading)
139        let expanded = self.component_registry.expand_ir_node(&ir_node);
140
141        // Get current state reference (no clone needed - reconcile only borrows)
142        let state: &serde_json::Value = self
143            .module
144            .as_ref()
145            .map(|m| m.get_state())
146            .unwrap_or(&NULL_STATE);
147
148        // Clear dependency graph before rebuilding
149        self.dependencies.clear();
150
151        // Reconcile and generate patches (dependencies are collected during tree construction)
152        let patches = reconcile_ir(
153            &mut self.tree,
154            &expanded,
155            None,
156            state,
157            &mut self.dependencies,
158        );
159
160        // Send patches to renderer
161        self.emit_patches(patches);
162
163        self.revision += 1;
164    }
165
166    /// Handle a state change notification from the module host
167    /// The host keeps the actual state - we just need to know what changed
168    pub fn notify_state_change(&mut self, change: &StateChange) {
169        // Find affected nodes based on changed paths
170        let mut affected_nodes = indexmap::IndexSet::new();
171        for path in change.paths() {
172            affected_nodes.extend(self.dependencies.get_affected_nodes(path));
173        }
174
175        // Mark affected nodes as dirty
176        self.scheduler
177            .mark_many_dirty(affected_nodes.iter().copied());
178
179        // Re-render dirty subtrees
180        // Note: The host will be asked to provide values during rendering via a callback
181        self.render_dirty();
182    }
183
184    /// Convenience method for backward compatibility / JSON patches
185    pub fn update_state(&mut self, state_patch: serde_json::Value) {
186        // Update module state FIRST before rendering
187        if let Some(module) = &mut self.module {
188            module.update_state(state_patch.clone());
189        }
190
191        // Then notify and render with the new state
192        let change = StateChange::from_json(&state_patch);
193        self.notify_state_change(&change);
194    }
195
196    /// Dispatch an action
197    pub fn dispatch_action(&mut self, action: Action) -> Result<(), EngineError> {
198        self.actions.dispatch(&action)
199    }
200
201    /// Render only dirty nodes
202    fn render_dirty(&mut self) {
203        let patches = crate::render::render_dirty_nodes(
204            &mut self.scheduler,
205            &mut self.tree,
206            self.module.as_ref(),
207        );
208
209        if !patches.is_empty() {
210            self.emit_patches(patches);
211            self.revision += 1;
212        }
213    }
214
215    /// Send patches to the renderer
216    fn emit_patches(&self, patches: Vec<Patch>) {
217        if let Some(ref callback) = self.render_callback {
218            callback(&patches);
219        }
220    }
221
222    /// Get the current revision number
223    pub fn revision(&self) -> u64 {
224        self.revision
225    }
226
227    /// Get access to the component registry
228    pub fn component_registry(&self) -> &ComponentRegistry {
229        &self.component_registry
230    }
231
232    /// Get mutable access to the component registry
233    pub fn component_registry_mut(&mut self) -> &mut ComponentRegistry {
234        &mut self.component_registry
235    }
236
237    /// Get access to the resource cache
238    pub fn resources(&self) -> &ResourceCache {
239        &self.resources
240    }
241
242    /// Get mutable access to the resource cache
243    pub fn resources_mut(&mut self) -> &mut ResourceCache {
244        &mut self.resources
245    }
246}
247
248impl Default for Engine {
249    fn default() -> Self {
250        Self::new()
251    }
252}