diff --git a/src-tauri/src/network/connection.rs b/src-tauri/src/network/connection.rs index 55720b8..241ba12 100644 --- a/src-tauri/src/network/connection.rs +++ b/src-tauri/src/network/connection.rs @@ -173,7 +173,9 @@ async fn connect( changed = cursor_data.changed() => { changed.map_err(|_| "cursor sender closed")?; let payload = { cursor_data.borrow_and_update().clone() }; - if let Some(payload) = payload { + if let Some(payload) = payload + && friend_presence.has_online_friends(&remote.id)? + { send(&mut writer, &ClientMessage::Signed { signature: keypair.sign(&message_bytes(&payload)), payload, @@ -183,7 +185,9 @@ async fn connect( changed = foreground_app_data.changed() => { changed.map_err(|_| "foreground-app sender closed")?; let payload = { foreground_app_data.borrow_and_update().clone() }; - if let Some(payload) = payload { + if let Some(payload) = payload + && friend_presence.has_online_friends(&remote.id)? + { send(&mut writer, &ClientMessage::Signed { signature: keypair.sign(&message_bytes(&payload)), payload, diff --git a/src-tauri/src/network/mod.rs b/src-tauri/src/network/mod.rs index 8b81583..91bccf8 100644 --- a/src-tauri/src/network/mod.rs +++ b/src-tauri/src/network/mod.rs @@ -95,6 +95,14 @@ impl Network { } pub fn send_live_data(&self, data: LiveData) { + let active_remotes = match self.friend_presence.active_remotes() { + Ok(remotes) if remotes.is_empty() => return, + Ok(remotes) => remotes, + Err(error) => { + eprintln!("failed to read friend presence for live data: {error}"); + return; + } + }; let sequence = match data.kind() { LiveDataKind::Cursor => self.next_cursor_sequence.fetch_add(1, Ordering::Relaxed), LiveDataKind::ForegroundApp => self @@ -114,7 +122,10 @@ impl Network { eprintln!("failed to lock remote connections for live data"); return; }; - for connection in connections.values() { + for (remote_id, connection) in connections.iter() { + if !active_remotes.contains(remote_id) { + continue; + } match envelope.kind() { LiveDataKind::Cursor => { connection.cursor_sender.send_replace(Some(payload.clone())); diff --git a/src-tauri/src/network/presence.rs b/src-tauri/src/network/presence.rs index be2d13f..d3cc336 100644 --- a/src-tauri/src/network/presence.rs +++ b/src-tauri/src/network/presence.rs @@ -14,6 +14,16 @@ pub(super) struct Change { pub(super) struct FriendPresence(Mutex>>); impl FriendPresence { + pub(super) fn active_remotes(&self) -> Result, String> { + let by_remote = self.0.lock().map_err(|error| error.to_string())?; + Ok(by_remote.keys().cloned().collect()) + } + + pub(super) fn has_online_friends(&self, remote_id: &str) -> Result { + let by_remote = self.0.lock().map_err(|error| error.to_string())?; + Ok(by_remote.contains_key(remote_id)) + } + pub(super) fn remotes_for(&self, friend_id: &str) -> Result, String> { let by_remote = self.0.lock().map_err(|error| error.to_string())?; Ok(by_remote @@ -128,6 +138,67 @@ fn diff( mod tests { use super::{Change, FriendPresence}; + #[test] + fn live_data_routes_follow_each_remotes_online_friends() { + let presence = FriendPresence::default(); + assert!(presence.active_remotes().unwrap().is_empty()); + assert!(!presence.has_online_friends("remote-a").unwrap()); + + presence + .replace("remote-a", vec!["friend-a".into(), "friend-b".into()]) + .unwrap(); + presence.replace("remote-b", Vec::new()).unwrap(); + assert_eq!( + presence.active_remotes().unwrap(), + ["remote-a".to_owned()].into() + ); + assert!(!presence.has_online_friends("remote-b").unwrap()); + + presence + .update("remote-a", "friend-a".into(), false) + .unwrap(); + assert!(presence.has_online_friends("remote-a").unwrap()); + presence + .update("remote-a", "friend-b".into(), false) + .unwrap(); + assert!(!presence.has_online_friends("remote-a").unwrap()); + assert!(presence.active_remotes().unwrap().is_empty()); + + let change = presence + .update("remote-b", "friend-a".into(), true) + .unwrap() + .unwrap(); + assert!( + change.route_added, + "resuming must trigger current-state publication" + ); + assert!(presence.has_online_friends("remote-b").unwrap()); + assert!(!presence.has_online_friends("remote-a").unwrap()); + } + + #[test] + fn disconnect_snapshot_and_friend_removal_disable_live_data_routes() { + let presence = FriendPresence::default(); + for remote in ["disconnected", "empty-snapshot", "removed-friend"] { + presence.update(remote, "friend".into(), true).unwrap(); + } + presence.remove("disconnected").unwrap(); + presence.replace("empty-snapshot", Vec::new()).unwrap(); + assert!(!presence.has_online_friends("disconnected").unwrap()); + assert!(!presence.has_online_friends("empty-snapshot").unwrap()); + assert!(presence.has_online_friends("removed-friend").unwrap()); + presence.retain(&[]).unwrap(); + assert!(presence.active_remotes().unwrap().is_empty()); + assert!(!presence.has_online_friends("removed-friend").unwrap()); + + let change = presence + .replace("disconnected", vec!["friend".into()]) + .unwrap() + .unwrap(); + assert!(change.route_added); + assert!(presence.has_online_friends("disconnected").unwrap()); + } + #[test] fn friend_stays_online_while_any_remote_reports_presence() { let presence = FriendPresence::default();