Skip to main content

hypen_engine/
engine.rs

1use crate::{
2    dispatch::{Action, ActionDispatcher},
3    engine_core::EngineCore,
4    error::EngineError,
5    ir::{ComponentRegistry, IRNode, ResourceRegistry},
6    lifecycle::{ModuleInstance, ResourceCache},
7    reconcile::Patch,
8    state::StateChange,
9};
10use indexmap::IndexMap;
11
12/// Callback type for rendering patches to the platform
13pub type RenderCallback = Box<dyn Fn(&[Patch]) + Send + Sync>;
14
15/// The main Hypen engine that orchestrates reactive UI rendering.
16///
17/// # Architecture: Shared Engine, Namespaced State
18///
19/// Multi-module applications share a **single `Engine` instance**. Each module
20/// registers its state under a lowercase-name prefix (e.g., `search`) so the
21/// engine holds one merged state tree. The SDK layer (`HypenModuleInstance` in
22/// TypeScript, `ModuleInstance` in Kotlin/Swift/Go) manages module lifecycles,
23/// action routing, and cross-module communication -- the engine itself only sees
24/// a flat JSON state and resolves `@{state.xxx}` bindings against it.
25///
26/// ## Multi-module Applications
27///
28/// 1. **One shared engine** -- all modules register their namespaced state into
29///    it. There is NOT one engine per module.
30///
31/// 2. **SDK-managed module instances** -- each SDK maintains multiple module
32///    instances (e.g., `HypenModuleInstance` in TypeScript), each with its own
33///    state tracking (Proxy in TS, ObservableState in Kotlin/Swift/Go).
34///
35/// 3. **Cross-module communication** uses `HypenGlobalContext`:
36///    ```typescript
37///    // In Search's action handler:
38///    const app = context.getModule<AppState>("App");
39///    const currentView = app.getState().currentView;
40///    app.setState({ showSearch: true });
41///    ```
42///
43/// 4. **State binding scope**: Each module's template binds to its own state
44///    via the namespace prefix. Cross-module data flows through the global
45///    context, keeping each module's reactive graph self-contained.
46///
47/// ## Design Rationale
48///
49/// Keeping one shared engine avoids duplicating the parser, reconciler, and
50/// dependency graph per module. Namespaced state keys ensure modules don't
51/// shadow each other's fields while the engine's path-based dependency tracker
52/// naturally scopes re-renders to only the affected module's bindings.
53pub struct Engine {
54    /// Shared core: component registry, resource registry, module state,
55    /// instance tree, dependency graph, scheduler, data sources, etc.
56    core: EngineCore,
57
58    /// Action dispatcher
59    actions: ActionDispatcher,
60
61    /// Resource cache
62    resources: ResourceCache,
63
64    /// Callback to send patches to the platform renderer
65    render_callback: Option<RenderCallback>,
66
67    /// Arc pointer of the primary module's state at the last render.
68    /// Used to detect duplicate notify_state_change calls (e.g., SDK Proxy
69    /// microtask firing after update_state already processed the same change).
70    last_state_ptr: usize,
71}
72
73impl Engine {
74    pub fn new() -> Self {
75        Self {
76            core: EngineCore::new(),
77            actions: ActionDispatcher::new(),
78            resources: ResourceCache::new(),
79            render_callback: None,
80            last_state_ptr: 0,
81        }
82    }
83
84    /// Register a custom component
85    pub fn register_component(&mut self, component: crate::ir::Component) {
86        self.core.register_component(component);
87    }
88
89    /// Set the component resolver for dynamic component loading
90    /// The resolver receives (component_name, context_path) and should return
91    /// ResolvedComponent { source, path } or None
92    pub fn set_component_resolver<F>(&mut self, resolver: F)
93    where
94        F: Fn(&str, Option<&str>) -> Option<crate::ir::ResolvedComponent> + Send + Sync + 'static,
95    {
96        self.core.set_component_resolver(resolver);
97    }
98
99    /// Register a single resource from raw SVG content.
100    pub fn register_resource(&mut self, name: &str, svg: &str) {
101        self.core.register_resource(name, svg);
102    }
103
104    /// Register multiple resources from a name -> SVG map.
105    pub fn register_resources(&mut self, map: IndexMap<String, String>) {
106        self.core.register_resources(map);
107    }
108
109    /// Get access to the resource registry
110    pub fn resource_registry(&self) -> &ResourceRegistry {
111        &self.core.resource_registry
112    }
113
114    /// Get mutable access to the resource registry
115    pub fn resource_registry_mut(&mut self) -> &mut ResourceRegistry {
116        &mut self.core.resource_registry
117    }
118
119    /// Backwards-compatible alias
120    pub fn icon_registry(&self) -> &ResourceRegistry {
121        &self.core.resource_registry
122    }
123
124    /// Backwards-compatible alias
125    pub fn icon_registry_mut(&mut self) -> &mut ResourceRegistry {
126        &mut self.core.resource_registry
127    }
128
129    /// Set the primary module instance (backward-compatible single-module API).
130    pub fn set_module(&mut self, module: ModuleInstance) {
131        self.core.set_module(module);
132    }
133
134    /// Register a named module for multi-module apps.
135    ///
136    /// The engine scopes `@{state.xxx}` bindings to this module's state when
137    /// rendering a component whose source starts with `module <name> { ... }`.
138    /// The `name` should be lowercase (e.g., `"search"`).
139    ///
140    /// Also registers the module's declared actions in the action->module map
141    /// so that `update_state` after `dispatch_action` automatically routes
142    /// to the correct module.
143    pub fn register_module(&mut self, name: impl Into<String>, module: ModuleInstance) {
144        self.core.register_module(name, module);
145    }
146
147
148    /// Get a named module's state (for reconciler lookups).
149    pub fn get_module_state(&self, name: &str) -> Option<&serde_json::Value> {
150        self.core.get_module_state(name)
151    }
152
153    /// Get access to all registered modules (for reconciler).
154    pub fn modules(&self) -> &IndexMap<String, ModuleInstance> {
155        &self.core.modules
156    }
157
158    /// Set the render callback
159    pub fn set_render_callback<F>(&mut self, callback: F)
160    where
161        F: Fn(&[Patch]) + Send + Sync + 'static,
162    {
163        self.render_callback = Some(Box::new(callback));
164    }
165
166    /// Register an action handler
167    pub fn on_action<F>(&mut self, action_name: impl Into<String>, handler: F)
168    where
169        F: Fn(&Action) + Send + Sync + 'static,
170    {
171        self.actions.on(action_name, handler);
172    }
173
174    /// Render an element tree (initial render or full re-render).
175    ///
176    /// This accepts a flat `Element`. For sources that use ForEach, When,
177    /// or If, prefer [`render_ir_node`] which preserves first-class control flow.
178    pub fn render(&mut self, element: &crate::ir::Element) {
179        let ir_node = IRNode::Element(element.clone());
180        self.render_ir_node(&ir_node);
181    }
182
183    /// Render an IRNode tree (initial render or full re-render).
184    ///
185    /// Unlike [`render`], this accepts an `IRNode` (from `ast_to_ir_node()`),
186    /// which preserves ForEach, When, and If as first-class control-flow nodes.
187    /// This is the same path used by the WASM engine.
188    pub fn render_ir_node(&mut self, ir_node: &IRNode) {
189        let patches = self.core.render_ir_node(ir_node);
190        self.emit_patches(patches);
191    }
192
193    /// Handle a state change notification from a host that keeps its own state.
194    ///
195    /// Use this when the host already mutated its own state copy and just
196    /// wants the engine to invalidate and re-render the affected nodes.
197    /// Engine-owned state should go through [`update_state`] / [`update_state_sparse`]
198    /// instead.
199    ///
200    /// Uses a revision guard: if the primary module's state hasn't changed
201    /// since the last render, this skips the redundant render. This catches
202    /// SDK Proxy microtasks re-firing after an explicit `update_state`
203    /// already processed the same change.
204    pub fn notify_state_change(&mut self, change: &StateChange) {
205        if let Some(ref module) = self.core.module {
206            let state_hash = {
207                let arc = module.get_state_shared();
208                std::sync::Arc::as_ptr(&arc) as usize
209            };
210            if self.last_state_ptr == state_hash && self.core.revision > 0 {
211                return;
212            }
213            self.last_state_ptr = state_hash;
214        }
215
216        self.core.schedule_from_state_change(change);
217        self.render_dirty();
218    }
219
220    /// Apply a state patch and re-render affected nodes.
221    ///
222    /// `scope` selects the target module:
223    /// - `None` → primary module set via [`set_module`](Self::set_module)
224    /// - `Some(name)` → named module registered via [`register_module`]
225    ///
226    /// Skips rendering if the patch produces no actual state change.
227    pub fn update_state(&mut self, scope: Option<&str>, state_patch: serde_json::Value) {
228        if self.core.update_state(scope, state_patch) {
229            self.render_dirty();
230        }
231    }
232
233    /// Apply a sparse state patch (path-value pairs) and re-render affected nodes.
234    /// See [`update_state`] for `scope` semantics.
235    pub fn update_state_sparse(
236        &mut self,
237        scope: Option<&str>,
238        paths: &[String],
239        values: &serde_json::Value,
240    ) {
241        if self.core.update_state_sparse(scope, paths, values) {
242            self.render_dirty();
243        }
244    }
245
246    /// Dispatch an action to its registered handler.
247    pub fn dispatch_action(&mut self, action: Action) -> Result<(), EngineError> {
248        self.actions.dispatch(&action)
249    }
250
251    /// Look up which named module (by lowercased name) owns a given action.
252    ///
253    /// Returns `Some(name)` when the action was registered via
254    /// [`register_module`]. Returns `None` for both "primary-module actions"
255    /// (declared on the module set via [`set_module`]) and "unknown actions"
256    /// — in both cases callers should route follow-up `update_state` calls
257    /// to the primary slot (scope `None`), which is the correct default.
258    ///
259    /// Callers that host action handlers outside the engine (e.g., the Rust
260    /// SDK's `RemoteSession`) can use this to route an incoming dispatch to
261    /// the correct module's state without having to call
262    /// [`dispatch_action`](Self::dispatch_action).
263    pub fn action_scope_for(&self, action_name: &str) -> Option<String> {
264        self.core.action_scope_for(action_name)
265    }
266
267    /// Render only dirty nodes
268    fn render_dirty(&mut self) {
269        let patches = self.core.render_dirty();
270        if !patches.is_empty() {
271            self.emit_patches(patches);
272        }
273    }
274
275    /// Send patches to the renderer.
276    /// Filters out Remove patches for elements that were Created in the same
277    /// batch -- this happens when a conditional re-reconciles module-scoped
278    /// children whose stored template doesn't carry module_scope.
279    fn emit_patches(&self, mut patches: Vec<Patch>) {
280        EngineCore::filter_spurious_removes(&mut patches);
281        if let Some(ref callback) = self.render_callback {
282            callback(&patches);
283        }
284    }
285
286    /// Get the current revision number
287    pub fn revision(&self) -> u64 {
288        self.core.revision
289    }
290
291    /// Get access to the component registry
292    pub fn component_registry(&self) -> &ComponentRegistry {
293        &self.core.component_registry
294    }
295
296    /// Get mutable access to the component registry
297    pub fn component_registry_mut(&mut self) -> &mut ComponentRegistry {
298        &mut self.core.component_registry
299    }
300
301    /// Get access to the resource cache
302    pub fn resources(&self) -> &ResourceCache {
303        &self.resources
304    }
305
306    /// Get mutable access to the resource cache
307    pub fn resources_mut(&mut self) -> &mut ResourceCache {
308        &mut self.resources
309    }
310
311    // ── Data Source Context ─────────────────────────────────────────────
312
313    /// Set (or replace) a named data source context.
314    ///
315    /// Registers the provider in the dependency graph (if not already known),
316    /// stores the data, and re-renders every node bound to `@name.*`.
317    /// This is the single entry point for all data source writes --
318    /// sparse merging (if needed) should happen at the SDK layer before
319    /// calling this method with the merged object.
320    ///
321    /// # Example
322    /// ```ignore
323    /// engine.set_context("spacetime", json!({
324    ///     "message": [{ "id": 1, "text": "Hello" }],
325    ///     "user": [{ "id": 1, "name": "Alice" }]
326    /// }));
327    /// ```
328    pub fn set_context(&mut self, name: &str, data: serde_json::Value) {
329        self.core.set_context(name, data);
330        self.render_dirty();
331    }
332
333    /// Remove a data source context entirely.
334    ///
335    /// Drops the provider's state and re-renders bound nodes (they resolve to null).
336    pub fn remove_context(&mut self, name: &str) {
337        self.core.remove_context(name);
338        self.render_dirty();
339    }
340
341    /// Get read access to the data sources map.
342    pub fn data_sources(&self) -> &indexmap::IndexMap<String, serde_json::Value> {
343        &self.core.data_sources
344    }
345}
346
347impl Default for Engine {
348    fn default() -> Self {
349        Self::new()
350    }
351}