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