diff --git a/rusty/src/core/runtime.rs b/rusty/src/core/runtime.rs index e1ce210..3ad6fe4 100644 --- a/rusty/src/core/runtime.rs +++ b/rusty/src/core/runtime.rs @@ -16,6 +16,10 @@ pub enum RuntimeMessage { event_name: String, args: serde_json::Value, }, + Navigate { + app_id: String, + state: serde_json::Value, + }, Rebuild { view_id: ViewId, }, @@ -29,6 +33,7 @@ pub struct Runtime { hook_stores: HashMap, dirty_views: HashSet, event_registry: Arc>, + nav_handler: Option>, event_tx: mpsc::Sender, event_rx: mpsc::Receiver, rebuild_tx: mpsc::Sender, @@ -46,6 +51,7 @@ impl Runtime { hook_stores: HashMap::new(), dirty_views: HashSet::new(), event_registry: Arc::new(RwLock::new(EventRegistry::new())), + nav_handler: None, event_tx, event_rx, rebuild_tx, @@ -58,6 +64,14 @@ impl Runtime { self.event_tx.clone() } + /// Register a navigation handler that will be called when Navigate messages are received. + pub fn set_nav_handler(&mut self, handler: F) + where + F: Fn(String, serde_json::Value) + Send + Sync + 'static, + { + self.nav_handler = Some(Arc::new(handler)); + } + /// Get a clone of the rebuild sender (for passing to State handles). pub fn rebuild_sender(&self) -> mpsc::Sender { self.rebuild_tx.clone() @@ -168,6 +182,12 @@ impl Runtime { } let _ = self.build().await; } + Some(RuntimeMessage::Navigate { app_id, state }) => { + if let Some(handler) = &self.nav_handler { + handler(app_id, state); + } + let _ = self.build().await; + } Some(RuntimeMessage::Rebuild { view_id }) => { self.dirty_views.insert(view_id); // For now, rebuild from root (future: targeted subtree rebuild) @@ -534,6 +554,64 @@ mod tests { ); } + #[tokio::test] + async fn test_navigate_dispatches_to_handler() { + let received_app_id = Arc::new(Mutex::new(String::new())); + let received_state = Arc::new(Mutex::new(serde_json::Value::Null)); + let app_id_clone = received_app_id.clone(); + let state_clone = received_state.clone(); + + let mut runtime = Runtime::new(TestView); + runtime.set_nav_handler(move |app_id, state| { + *app_id_clone.lock().unwrap() = app_id; + *state_clone.lock().unwrap() = state; + }); + + let _ = runtime.build().await; + + let tx = runtime.event_sender(); + tx.send(RuntimeMessage::Navigate { + app_id: "my-app".to_string(), + state: serde_json::json!({"path": "/home"}), + }) + .await + .unwrap(); + + tx.send(RuntimeMessage::Shutdown).await.unwrap(); + + runtime.run().await; + + assert_eq!(*received_app_id.lock().unwrap(), "my-app"); + assert_eq!( + *received_state.lock().unwrap(), + serde_json::json!({"path": "/home"}) + ); + } + + #[tokio::test] + async fn test_navigate_without_handler_is_noop() { + let mut runtime = Runtime::new(TestView); + + let _ = runtime.build().await; + + let tx = runtime.event_sender(); + tx.send(RuntimeMessage::Navigate { + app_id: "my-app".to_string(), + state: serde_json::json!({"path": "/about"}), + }) + .await + .unwrap(); + + tx.send(RuntimeMessage::Shutdown).await.unwrap(); + + // Should not panic — navigate without handler is a no-op + runtime.run().await; + + // Verify the runtime still works — tree should exist after rebuild + let tree = runtime.current_tree().await; + assert!(tree.is_some()); + } + #[test] fn test_cleanup_on_view_removal() { use crate::core::view_tree::ViewTree; diff --git a/rusty/src/server/ws.rs b/rusty/src/server/ws.rs index 206a2b9..f6597ab 100644 --- a/rusty/src/server/ws.rs +++ b/rusty/src/server/ws.rs @@ -197,8 +197,22 @@ async fn handle_socket(socket: WebSocket, state: Arc) { } } } - ClientMessage::Navigate { .. } => { - // Navigation handling (future) + ClientMessage::Navigate { app_id, state } => { + let _ = event_tx + .send(RuntimeMessage::Navigate { app_id, state }) + .await; + + // After navigate, get updated tree from this session's runtime + if let Some(tree) = session.runtime.current_tree().await { + if let Some(patches) = session.reconciler.reconcile(&tree) { + if !patches.is_empty() { + let msg = ServerMessage::Update { patches }; + if let Ok(json) = serde_json::to_string(&msg) { + let _ = sender.send(Message::Text(json.into())).await; + } + } + } + } } } }