Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions rusty/src/core/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand All @@ -29,6 +33,7 @@ pub struct Runtime {
hook_stores: HashMap<ViewId, HookStore>,
dirty_views: HashSet<ViewId>,
event_registry: Arc<RwLock<EventRegistry>>,
nav_handler: Option<Arc<dyn Fn(String, serde_json::Value) + Send + Sync>>,
event_tx: mpsc::Sender<RuntimeMessage>,
event_rx: mpsc::Receiver<RuntimeMessage>,
rebuild_tx: mpsc::Sender<ViewId>,
Expand All @@ -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,
Expand All @@ -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<F>(&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<ViewId> {
self.rebuild_tx.clone()
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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;
Expand Down
18 changes: 16 additions & 2 deletions rusty/src/server/ws.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,22 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>) {
}
}
}
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;
}
}
}
}
}
}
}
Expand Down
Loading