Skip to main content

hypen_engine/lifecycle/
module.rs

1use serde::{Deserialize, Serialize};
2use std::sync::Arc;
3
4/// Metadata describing a stateful module.
5///
6/// A module is the unit of state and behavior in Hypen. It declares which
7/// actions it handles, which state keys it exposes, and optional persistence
8/// or versioning settings.
9///
10/// Modules are instantiated as [`ModuleInstance`] which holds the live state.
11///
12/// # Example
13///
14/// ```rust
15/// use hypen_engine::Module;
16///
17/// let module = Module::new("Counter")
18///     .with_actions(vec!["increment".into(), "decrement".into()])
19///     .with_state_keys(vec!["count".into()]);
20/// ```
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct Module {
23    /// Module name (e.g. `"Counter"`, `"ProfilePage"`)
24    pub name: String,
25
26    /// Action names this module handles (e.g. `["increment", "decrement"]`)
27    pub actions: Vec<String>,
28
29    /// Top-level state keys this module exposes (e.g. `["count", "user"]`)
30    pub state_keys: Vec<String>,
31
32    /// Whether state should be persisted across sessions
33    pub persist: bool,
34
35    /// Optional schema version for state migration
36    pub version: Option<u32>,
37}
38
39impl Module {
40    pub fn new(name: impl Into<String>) -> Self {
41        Self {
42            name: name.into(),
43            actions: Vec::new(),
44            state_keys: Vec::new(),
45            persist: false,
46            version: None,
47        }
48    }
49
50    pub fn with_actions(mut self, actions: Vec<String>) -> Self {
51        self.actions = actions;
52        self
53    }
54
55    pub fn with_state_keys(mut self, state_keys: Vec<String>) -> Self {
56        self.state_keys = state_keys;
57        self
58    }
59
60    pub fn with_persist(mut self, persist: bool) -> Self {
61        self.persist = persist;
62        self
63    }
64
65    pub fn with_version(mut self, version: u32) -> Self {
66        self.version = Some(version);
67        self
68    }
69}
70
71/// Lifecycle hooks for a module.
72///
73/// Implement this trait to receive notifications when a module is created,
74/// destroyed, or when its state changes. This is primarily used by SDK
75/// implementations to bridge engine lifecycle events to host language callbacks.
76pub trait ModuleLifecycle {
77    /// Called when the module is first mounted.
78    fn on_created(&mut self);
79
80    /// Called when the module is unmounted and about to be dropped.
81    fn on_destroyed(&mut self);
82
83    /// Called when state is updated from the host.
84    fn on_state_changed(&mut self, state: serde_json::Value);
85}
86
87/// Callback type for lifecycle events
88pub type LifecycleCallback = Box<dyn Fn() + Send + Sync>;
89
90/// A live module instance holding the current state and lifecycle callbacks.
91///
92/// State is wrapped in `Arc` for O(1) clone performance — critical because
93/// state is accessed on every render cycle. With `Arc`, "cloning" is just a
94/// reference count increment instead of a deep copy.
95///
96/// # Lifecycle
97///
98/// ```text
99/// new() → mount() → [update_state()...] → unmount()
100///          ↑                                    |
101///          └────────────────────────────────────┘
102///                   (can re-mount)
103/// ```
104///
105/// Mount/unmount are idempotent — calling `mount()` twice only fires `on_created` once.
106pub struct ModuleInstance {
107    /// Module metadata (name, action names, state keys)
108    pub module: Module,
109
110    /// Current state snapshot (Arc-wrapped for O(1) clone)
111    state: Arc<serde_json::Value>,
112
113    /// Whether the module is currently mounted
114    pub mounted: bool,
115
116    /// Callback for when module is created
117    on_created: Option<LifecycleCallback>,
118
119    /// Callback for when module is destroyed
120    on_destroyed: Option<LifecycleCallback>,
121}
122
123impl ModuleInstance {
124    pub fn new(module: Module, initial_state: serde_json::Value) -> Self {
125        Self {
126            module,
127            state: Arc::new(initial_state),
128            mounted: false,
129            on_created: None,
130            on_destroyed: None,
131        }
132    }
133
134    /// Build a `ModuleInstance` from raw configuration fields.
135    ///
136    /// Convenience constructor for FFI bindings (WASM, WASI, UniFFI) that all
137    /// receive a name + actions + state keys + initial state and need to wire
138    /// them into a `Module` + `ModuleInstance` pair.
139    pub fn from_config(
140        name: &str,
141        actions: Vec<String>,
142        state_keys: Vec<String>,
143        initial_state: serde_json::Value,
144    ) -> Self {
145        let module = Module::new(name)
146            .with_actions(actions)
147            .with_state_keys(state_keys);
148        Self::new(module, initial_state)
149    }
150
151    /// Set the on_created lifecycle callback
152    pub fn set_on_created<F>(&mut self, callback: F)
153    where
154        F: Fn() + Send + Sync + 'static,
155    {
156        self.on_created = Some(Box::new(callback));
157    }
158
159    /// Set the on_destroyed lifecycle callback
160    pub fn set_on_destroyed<F>(&mut self, callback: F)
161    where
162        F: Fn() + Send + Sync + 'static,
163    {
164        self.on_destroyed = Some(Box::new(callback));
165    }
166
167    /// Mount the module (call on_created)
168    pub fn mount(&mut self) {
169        if !self.mounted {
170            self.mounted = true;
171            // Call on_created lifecycle hook if registered
172            if let Some(ref callback) = self.on_created {
173                callback();
174            }
175        }
176    }
177
178    /// Unmount the module (call on_destroyed)
179    pub fn unmount(&mut self) {
180        if self.mounted {
181            // Call on_destroyed lifecycle hook if registered
182            if let Some(ref callback) = self.on_destroyed {
183                callback();
184            }
185            self.mounted = false;
186        }
187    }
188
189    /// Update state from a patch
190    ///
191    /// Uses Arc::make_mut for copy-on-write semantics: if this is the only
192    /// reference, mutates in place; otherwise clones first.
193    pub fn update_state(&mut self, patch: serde_json::Value) {
194        // Arc::make_mut provides copy-on-write: clones only if shared
195        let state = Arc::make_mut(&mut self.state);
196        merge_json(state, patch);
197    }
198
199    /// Update state from sparse path-value pairs
200    /// This is more efficient than sending the full state when only a few paths changed
201    ///
202    /// Uses Arc::make_mut for copy-on-write semantics.
203    pub fn update_state_sparse(&mut self, paths: &[String], values: &serde_json::Value) {
204        // values is expected to be an object mapping paths to their new values
205        if let serde_json::Value::Object(map) = values {
206            // Only get mutable access if we have paths to update
207            if paths.iter().any(|p| map.contains_key(p)) {
208                let state = Arc::make_mut(&mut self.state);
209                for path in paths {
210                    if let Some(new_value) = map.get(path) {
211                        set_value_at_path(state, path, new_value.clone());
212                    }
213                }
214            }
215        }
216    }
217
218    /// Get a reference to the current state
219    ///
220    /// For read-only access without cloning. Prefer this over `get_state_shared()`
221    /// when you don't need to store the state beyond the current scope.
222    pub fn get_state(&self) -> &serde_json::Value {
223        &self.state
224    }
225
226    /// Get a shared reference to the current state (O(1) clone)
227    ///
228    /// Use this for the render hot path where state needs to be passed around
229    /// without deep copying. Cloning the Arc is just a reference count increment.
230    pub fn get_state_shared(&self) -> Arc<serde_json::Value> {
231        Arc::clone(&self.state)
232    }
233}
234
235/// Deep merge two JSON values
236fn merge_json(target: &mut serde_json::Value, source: serde_json::Value) {
237    use serde_json::Value;
238
239    match (target, source) {
240        (Value::Object(target_map), Value::Object(source_map)) => {
241            for (key, value) in source_map {
242                if let Some(target_value) = target_map.get_mut(&key) {
243                    merge_json(target_value, value);
244                } else {
245                    target_map.insert(key, value);
246                }
247            }
248        }
249        (target, source) => {
250            *target = source;
251        }
252    }
253}
254
255/// Set a value at a dot-separated path (e.g., "user.profile.name")
256/// Creates intermediate objects if they don't exist
257fn set_value_at_path(target: &mut serde_json::Value, path: &str, value: serde_json::Value) {
258    use serde_json::Value;
259
260    let parts: Vec<&str> = path.split('.').collect();
261    if parts.is_empty() {
262        return;
263    }
264
265    let mut current = target;
266
267    // Navigate to the parent of the final key
268    for part in &parts[..parts.len() - 1] {
269        // Try to parse as array index first
270        if let Ok(index) = part.parse::<usize>() {
271            if let Value::Array(arr) = current {
272                // Extend array if needed
273                while arr.len() <= index {
274                    arr.push(Value::Null);
275                }
276                current = &mut arr[index];
277                continue;
278            }
279        }
280
281        // Otherwise treat as object key
282        if !current.is_object() {
283            *current = Value::Object(serde_json::Map::new());
284        }
285
286        if let Value::Object(map) = current {
287            if !map.contains_key(*part) {
288                map.insert(part.to_string(), Value::Object(serde_json::Map::new()));
289            }
290            current = map.get_mut(*part).unwrap();
291        }
292    }
293
294    // Set the final value
295    let final_key = parts[parts.len() - 1];
296
297    // Try to parse as array index
298    if let Ok(index) = final_key.parse::<usize>() {
299        if let Value::Array(arr) = current {
300            while arr.len() <= index {
301                arr.push(Value::Null);
302            }
303            arr[index] = value;
304            return;
305        }
306    }
307
308    // Set as object property
309    if !current.is_object() {
310        *current = Value::Object(serde_json::Map::new());
311    }
312
313    if let Value::Object(map) = current {
314        map.insert(final_key.to_string(), value);
315    }
316}
317
318#[cfg(test)]
319mod tests {
320    use super::*;
321    use serde_json::json;
322
323    #[test]
324    fn test_merge_json() {
325        let mut target = json!({
326            "user": {
327                "name": "Alice",
328                "age": 30
329            }
330        });
331
332        let patch = json!({
333            "user": {
334                "age": 31
335            }
336        });
337
338        merge_json(&mut target, patch);
339
340        assert_eq!(target["user"]["name"], "Alice");
341        assert_eq!(target["user"]["age"], 31);
342    }
343
344    #[test]
345    fn test_lifecycle_on_created_callback() {
346        use std::sync::{Arc, Mutex};
347
348        let module = Module::new("TestModule");
349        let initial_state = json!({});
350        let mut instance = ModuleInstance::new(module, initial_state);
351
352        // Track if callback was called
353        let called = Arc::new(Mutex::new(false));
354        let called_clone = called.clone();
355
356        instance.set_on_created(move || {
357            *called_clone.lock().unwrap() = true;
358        });
359
360        // Mount should call on_created
361        assert!(!*called.lock().unwrap());
362        instance.mount();
363        assert!(*called.lock().unwrap());
364        assert!(instance.mounted);
365    }
366
367    #[test]
368    fn test_lifecycle_on_destroyed_callback() {
369        use std::sync::{Arc, Mutex};
370
371        let module = Module::new("TestModule");
372        let initial_state = json!({});
373        let mut instance = ModuleInstance::new(module, initial_state);
374
375        let called = Arc::new(Mutex::new(false));
376        let called_clone = called.clone();
377
378        instance.set_on_destroyed(move || {
379            *called_clone.lock().unwrap() = true;
380        });
381
382        // First mount the module
383        instance.mount();
384        assert!(instance.mounted);
385
386        // Unmount should call on_destroyed
387        assert!(!*called.lock().unwrap());
388        instance.unmount();
389        assert!(*called.lock().unwrap());
390        assert!(!instance.mounted);
391    }
392
393    #[test]
394    fn test_lifecycle_callbacks_not_called_when_not_set() {
395        let module = Module::new("TestModule");
396        let initial_state = json!({});
397        let mut instance = ModuleInstance::new(module, initial_state);
398
399        // Should not panic when callbacks are not set
400        instance.mount();
401        assert!(instance.mounted);
402
403        instance.unmount();
404        assert!(!instance.mounted);
405    }
406
407    #[test]
408    fn test_lifecycle_mount_idempotent() {
409        use std::sync::{Arc, Mutex};
410
411        let module = Module::new("TestModule");
412        let initial_state = json!({});
413        let mut instance = ModuleInstance::new(module, initial_state);
414
415        let call_count = Arc::new(Mutex::new(0));
416        let call_count_clone = call_count.clone();
417
418        instance.set_on_created(move || {
419            *call_count_clone.lock().unwrap() += 1;
420        });
421
422        // First mount
423        instance.mount();
424        assert_eq!(*call_count.lock().unwrap(), 1);
425
426        // Second mount should not call callback again
427        instance.mount();
428        assert_eq!(*call_count.lock().unwrap(), 1);
429    }
430
431    #[test]
432    fn test_lifecycle_unmount_idempotent() {
433        use std::sync::{Arc, Mutex};
434
435        let module = Module::new("TestModule");
436        let initial_state = json!({});
437        let mut instance = ModuleInstance::new(module, initial_state);
438
439        let call_count = Arc::new(Mutex::new(0));
440        let call_count_clone = call_count.clone();
441
442        instance.set_on_destroyed(move || {
443            *call_count_clone.lock().unwrap() += 1;
444        });
445
446        // Mount first
447        instance.mount();
448
449        // First unmount
450        instance.unmount();
451        assert_eq!(*call_count.lock().unwrap(), 1);
452
453        // Second unmount should not call callback again
454        instance.unmount();
455        assert_eq!(*call_count.lock().unwrap(), 1);
456    }
457
458    #[test]
459    fn test_lifecycle_full_cycle() {
460        use std::sync::{Arc, Mutex};
461
462        let module = Module::new("TestModule");
463        let initial_state = json!({});
464        let mut instance = ModuleInstance::new(module, initial_state);
465
466        let events = Arc::new(Mutex::new(Vec::new()));
467        let events_created = events.clone();
468        let events_destroyed = events.clone();
469
470        instance.set_on_created(move || {
471            events_created.lock().unwrap().push("created");
472        });
473
474        instance.set_on_destroyed(move || {
475            events_destroyed.lock().unwrap().push("destroyed");
476        });
477
478        // Full lifecycle: mount -> unmount -> mount -> unmount
479        instance.mount();
480        instance.unmount();
481        instance.mount();
482        instance.unmount();
483
484        let events = events.lock().unwrap();
485        assert_eq!(events.len(), 4);
486        assert_eq!(events[0], "created");
487        assert_eq!(events[1], "destroyed");
488        assert_eq!(events[2], "created");
489        assert_eq!(events[3], "destroyed");
490    }
491
492    #[test]
493    fn test_set_value_at_path_simple() {
494        let mut state = json!({
495            "count": 0
496        });
497
498        set_value_at_path(&mut state, "count", json!(42));
499        assert_eq!(state["count"], 42);
500    }
501
502    #[test]
503    fn test_set_value_at_path_nested() {
504        let mut state = json!({
505            "user": {
506                "name": "Alice",
507                "profile": {
508                    "bio": "Developer"
509                }
510            }
511        });
512
513        set_value_at_path(&mut state, "user.profile.bio", json!("Engineer"));
514        assert_eq!(state["user"]["profile"]["bio"], "Engineer");
515        // Other values should be unchanged
516        assert_eq!(state["user"]["name"], "Alice");
517    }
518
519    #[test]
520    fn test_set_value_at_path_creates_intermediate() {
521        let mut state = json!({});
522
523        set_value_at_path(&mut state, "user.profile.name", json!("Bob"));
524        assert_eq!(state["user"]["profile"]["name"], "Bob");
525    }
526
527    #[test]
528    fn test_set_value_at_path_array_index() {
529        let mut state = json!({
530            "items": ["a", "b", "c"]
531        });
532
533        set_value_at_path(&mut state, "items.1", json!("modified"));
534        assert_eq!(state["items"][1], "modified");
535        assert_eq!(state["items"][0], "a");
536        assert_eq!(state["items"][2], "c");
537    }
538
539    #[test]
540    fn test_update_state_sparse_single_path() {
541        let module = Module::new("TestModule");
542        let initial_state = json!({
543            "count": 0,
544            "name": "Alice"
545        });
546        let mut instance = ModuleInstance::new(module, initial_state);
547
548        let paths = vec!["count".to_string()];
549        let values = json!({
550            "count": 42
551        });
552
553        instance.update_state_sparse(&paths, &values);
554
555        assert_eq!(instance.get_state()["count"], 42);
556        assert_eq!(instance.get_state()["name"], "Alice"); // Unchanged
557    }
558
559    #[test]
560    fn test_update_state_sparse_nested_path() {
561        let module = Module::new("TestModule");
562        let initial_state = json!({
563            "user": {
564                "name": "Alice",
565                "age": 30
566            },
567            "settings": {
568                "theme": "dark"
569            }
570        });
571        let mut instance = ModuleInstance::new(module, initial_state);
572
573        let paths = vec!["user.age".to_string()];
574        let values = json!({
575            "user.age": 31
576        });
577
578        instance.update_state_sparse(&paths, &values);
579
580        assert_eq!(instance.get_state()["user"]["age"], 31);
581        assert_eq!(instance.get_state()["user"]["name"], "Alice"); // Unchanged
582        assert_eq!(instance.get_state()["settings"]["theme"], "dark"); // Unchanged
583    }
584
585    #[test]
586    fn test_update_state_sparse_multiple_paths() {
587        let module = Module::new("TestModule");
588        let initial_state = json!({
589            "count": 0,
590            "user": {
591                "name": "Alice"
592            }
593        });
594        let mut instance = ModuleInstance::new(module, initial_state);
595
596        let paths = vec!["count".to_string(), "user.name".to_string()];
597        let values = json!({
598            "count": 100,
599            "user.name": "Bob"
600        });
601
602        instance.update_state_sparse(&paths, &values);
603
604        assert_eq!(instance.get_state()["count"], 100);
605        assert_eq!(instance.get_state()["user"]["name"], "Bob");
606    }
607
608    // ============ Edge Case Tests ============
609
610    #[test]
611    fn test_set_value_at_path_empty_path() {
612        let mut state = json!({"count": 0});
613        // Empty path should do nothing
614        set_value_at_path(&mut state, "", json!(42));
615        assert_eq!(state["count"], 0);
616    }
617
618    #[test]
619    fn test_set_value_at_path_null_value() {
620        let mut state = json!({
621            "user": {
622                "name": "Alice",
623                "email": "alice@example.com"
624            }
625        });
626
627        set_value_at_path(&mut state, "user.email", json!(null));
628        assert_eq!(state["user"]["email"], serde_json::Value::Null);
629        assert_eq!(state["user"]["name"], "Alice"); // Unchanged
630    }
631
632    #[test]
633    fn test_set_value_at_path_type_change() {
634        let mut state = json!({
635            "data": "string value"
636        });
637
638        // Change string to object
639        set_value_at_path(&mut state, "data", json!({"nested": true}));
640        assert_eq!(state["data"]["nested"], true);
641
642        // Change object to array
643        set_value_at_path(&mut state, "data", json!([1, 2, 3]));
644        assert_eq!(state["data"][0], 1);
645
646        // Change array to number
647        set_value_at_path(&mut state, "data", json!(42));
648        assert_eq!(state["data"], 42);
649    }
650
651    #[test]
652    fn test_set_value_at_path_deeply_nested() {
653        let mut state = json!({});
654
655        // Create deeply nested path (6 levels deep)
656        set_value_at_path(&mut state, "a.b.c.d.e.f", json!("deep value"));
657        assert_eq!(state["a"]["b"]["c"]["d"]["e"]["f"], "deep value");
658    }
659
660    #[test]
661    fn test_set_value_at_path_nested_array_object() {
662        let mut state = json!({
663            "users": [
664                {"name": "Alice", "tags": ["admin"]},
665                {"name": "Bob", "tags": ["user"]}
666            ]
667        });
668
669        // Update nested object within array
670        set_value_at_path(&mut state, "users.1.name", json!("Robert"));
671        assert_eq!(state["users"][1]["name"], "Robert");
672        assert_eq!(state["users"][0]["name"], "Alice"); // Unchanged
673
674        // Update nested array within array element
675        set_value_at_path(&mut state, "users.0.tags.0", json!("superadmin"));
676        assert_eq!(state["users"][0]["tags"][0], "superadmin");
677    }
678
679    #[test]
680    fn test_set_value_at_path_extend_array() {
681        let mut state = json!({
682            "items": ["a", "b"]
683        });
684
685        // Setting index 5 should extend array with nulls
686        set_value_at_path(&mut state, "items.5", json!("extended"));
687        assert_eq!(state["items"].as_array().unwrap().len(), 6);
688        assert_eq!(state["items"][5], "extended");
689        assert_eq!(state["items"][2], serde_json::Value::Null);
690        assert_eq!(state["items"][3], serde_json::Value::Null);
691        assert_eq!(state["items"][4], serde_json::Value::Null);
692    }
693
694    #[test]
695    fn test_set_value_at_path_overwrite_primitive_with_nested() {
696        let mut state = json!({
697            "config": 42
698        });
699
700        // Trying to set a nested path where parent is a primitive
701        // Should convert primitive to object
702        set_value_at_path(&mut state, "config.nested.value", json!("test"));
703        assert_eq!(state["config"]["nested"]["value"], "test");
704    }
705
706    #[test]
707    fn test_set_value_at_path_boolean_values() {
708        let mut state = json!({
709            "flags": {
710                "enabled": true,
711                "visible": false
712            }
713        });
714
715        set_value_at_path(&mut state, "flags.enabled", json!(false));
716        set_value_at_path(&mut state, "flags.visible", json!(true));
717        assert_eq!(state["flags"]["enabled"], false);
718        assert_eq!(state["flags"]["visible"], true);
719    }
720
721    #[test]
722    fn test_set_value_at_path_float_values() {
723        let mut state = json!({
724            "coordinates": {
725                "lat": 0.0,
726                "lng": 0.0
727            }
728        });
729
730        set_value_at_path(&mut state, "coordinates.lat", json!(37.7749));
731        set_value_at_path(&mut state, "coordinates.lng", json!(-122.4194));
732
733        // Use approximate comparison for floats
734        let lat = state["coordinates"]["lat"].as_f64().unwrap();
735        let lng = state["coordinates"]["lng"].as_f64().unwrap();
736        assert!((lat - 37.7749).abs() < 0.0001);
737        assert!((lng - (-122.4194)).abs() < 0.0001);
738    }
739
740    #[test]
741    fn test_update_state_sparse_empty_paths() {
742        let module = Module::new("TestModule");
743        let initial_state = json!({
744            "count": 0
745        });
746        let mut instance = ModuleInstance::new(module, initial_state);
747
748        let paths: Vec<String> = vec![];
749        let values = json!({});
750
751        // Should not panic or modify state
752        instance.update_state_sparse(&paths, &values);
753        assert_eq!(instance.get_state()["count"], 0);
754    }
755
756    #[test]
757    fn test_update_state_sparse_path_not_in_values() {
758        let module = Module::new("TestModule");
759        let initial_state = json!({
760            "count": 0,
761            "name": "Alice"
762        });
763        let mut instance = ModuleInstance::new(module, initial_state);
764
765        // Path is specified but not in values - should be skipped
766        let paths = vec!["count".to_string(), "missing".to_string()];
767        let values = json!({
768            "count": 42
769            // "missing" not provided
770        });
771
772        instance.update_state_sparse(&paths, &values);
773        assert_eq!(instance.get_state()["count"], 42);
774        assert_eq!(instance.get_state()["name"], "Alice");
775    }
776
777    #[test]
778    fn test_update_state_sparse_invalid_values_type() {
779        let module = Module::new("TestModule");
780        let initial_state = json!({
781            "count": 0
782        });
783        let mut instance = ModuleInstance::new(module, initial_state);
784
785        // values is not an object - should not crash, just skip
786        let paths = vec!["count".to_string()];
787        let values = json!("not an object");
788
789        instance.update_state_sparse(&paths, &values);
790        // State should remain unchanged
791        assert_eq!(instance.get_state()["count"], 0);
792    }
793
794    #[test]
795    fn test_update_state_sparse_array_values() {
796        let module = Module::new("TestModule");
797        let initial_state = json!({
798            "items": []
799        });
800        let mut instance = ModuleInstance::new(module, initial_state);
801
802        let paths = vec!["items".to_string()];
803        let values = json!({
804            "items": ["a", "b", "c"]
805        });
806
807        instance.update_state_sparse(&paths, &values);
808        assert_eq!(instance.get_state()["items"], json!(["a", "b", "c"]));
809    }
810
811    #[test]
812    fn test_update_state_sparse_complex_nested_update() {
813        let module = Module::new("TestModule");
814        let initial_state = json!({
815            "app": {
816                "ui": {
817                    "theme": "light",
818                    "sidebar": {
819                        "collapsed": false,
820                        "width": 250
821                    }
822                },
823                "data": {
824                    "users": [],
825                    "cache": {}
826                }
827            }
828        });
829        let mut instance = ModuleInstance::new(module, initial_state);
830
831        let paths = vec![
832            "app.ui.theme".to_string(),
833            "app.ui.sidebar.collapsed".to_string(),
834            "app.data.users".to_string(),
835        ];
836        let values = json!({
837            "app.ui.theme": "dark",
838            "app.ui.sidebar.collapsed": true,
839            "app.data.users": [{"id": 1, "name": "Alice"}]
840        });
841
842        instance.update_state_sparse(&paths, &values);
843
844        assert_eq!(instance.get_state()["app"]["ui"]["theme"], "dark");
845        assert_eq!(
846            instance.get_state()["app"]["ui"]["sidebar"]["collapsed"],
847            true
848        );
849        assert_eq!(instance.get_state()["app"]["ui"]["sidebar"]["width"], 250); // Unchanged
850        assert_eq!(
851            instance.get_state()["app"]["data"]["users"][0]["name"],
852            "Alice"
853        );
854        assert!(instance.get_state()["app"]["data"]["cache"].is_object()); // Unchanged
855    }
856
857    #[test]
858    fn test_update_state_sparse_preserves_sibling_keys() {
859        let module = Module::new("TestModule");
860        let initial_state = json!({
861            "user": {
862                "name": "Alice",
863                "email": "alice@example.com",
864                "profile": {
865                    "bio": "Developer",
866                    "avatar": "alice.png",
867                    "social": {
868                        "twitter": "@alice",
869                        "github": "alice"
870                    }
871                }
872            }
873        });
874        let mut instance = ModuleInstance::new(module, initial_state);
875
876        // Only update one deeply nested field
877        let paths = vec!["user.profile.social.twitter".to_string()];
878        let values = json!({
879            "user.profile.social.twitter": "@alice_new"
880        });
881
882        instance.update_state_sparse(&paths, &values);
883
884        // Verify updated field
885        assert_eq!(
886            instance.get_state()["user"]["profile"]["social"]["twitter"],
887            "@alice_new"
888        );
889
890        // Verify all sibling fields are unchanged
891        assert_eq!(instance.get_state()["user"]["name"], "Alice");
892        assert_eq!(instance.get_state()["user"]["email"], "alice@example.com");
893        assert_eq!(instance.get_state()["user"]["profile"]["bio"], "Developer");
894        assert_eq!(
895            instance.get_state()["user"]["profile"]["avatar"],
896            "alice.png"
897        );
898        assert_eq!(
899            instance.get_state()["user"]["profile"]["social"]["github"],
900            "alice"
901        );
902    }
903}