refactor(core): single SelfPresence self-state builder

Core reconstructed PeerState in five command branches (join, mute
toggle, avatar change, screen-share start/stop), each repeating the full
field list. Factor a SelfPresence struct holding the sticky identity
fields (name + avatar) with a to_state(is_muted, addr, sharing) builder
that folds in the volatile per-announce fields, so the PeerState literal
lives in one place. This is the precondition for adding a broadcast
game-presence field without editing every call site.

No behavior change. +1 unit test (359 lib total path).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-21 15:05:32 -04:00
co-authored by Claude Opus 4.8
parent 7e4f2f2127
commit 9e8c8b4ace
2 changed files with 95 additions and 43 deletions
+37 -43
View File
@@ -7,7 +7,7 @@ use crate::audio::eq::{Eq, EqSettings};
use crate::codec::{AudioEncoder, opus_impl::OpusEncoder}; use crate::codec::{AudioEncoder, opus_impl::OpusEncoder};
use crate::core::jitter::{JitterBuffer, FRAME_SAMPLES}; use crate::core::jitter::{JitterBuffer, FRAME_SAMPLES};
use crate::network::{ use crate::network::{
NetworkTransport, RoomState, PeerState, RoomEvent, ConnEvent, PeerSpeakTicket, NetworkTransport, RoomState, SelfPresence, RoomEvent, ConnEvent, PeerSpeakTicket,
iroh_impl::{IrohTransport, AudioRouter, FileRouter}, iroh_impl::{IrohTransport, AudioRouter, FileRouter},
gossip::IrohGossipState, gossip::IrohGossipState,
}; };
@@ -917,10 +917,14 @@ async fn run_core_loop(
let peer_gate = Arc::new(Mutex::new(HashMap::<EndpointId, f32>::new())); let peer_gate = Arc::new(Mutex::new(HashMap::<EndpointId, f32>::new()));
// Peers locally muted by us: decoded for level metering but not mixed. // Peers locally muted by us: decoded for level metering but not mixed.
let locally_muted = Arc::new(Mutex::new(HashSet::<EndpointId>::new())); let locally_muted = Arc::new(Mutex::new(HashSet::<EndpointId>::new()));
let mut current_name = "Anonymous".to_string(); // Sticky identity fields of our own presence (display name + W4 avatar), set on
// Our chosen avatar (W4), set on Join and changeable via SetAvatar; included // Join and changed via SetName/SetAvatar. Combined with the volatile per-announce
// in every self-state we announce over presence. // fields (mute/addr/share ticket) by `SelfPresence::to_state` — the single place
let mut current_avatar = crate::avatar::Avatar::default(); // our `PeerState` is built. Defaults match the prior `current_name`/`current_avatar`.
let mut presence = SelfPresence {
name: "Anonymous".to_string(),
avatar: crate::avatar::Avatar::default(),
};
let mut network_mode = NetworkMode::default(); let mut network_mode = NetworkMode::default();
// Pixelpass binary override (config), and the ticket of our own active screen // Pixelpass binary override (config), and the ticket of our own active screen
// share (rides our presence so the room — incl. late joiners — can watch). // share (rides our presence so the room — incl. late joiners — can watch).
@@ -1125,8 +1129,8 @@ async fn run_core_loop(
} }
CoreCommand::Join { name, ticket, room_name, input_device, output_device, echo_cancellation, avatar } => { CoreCommand::Join { name, ticket, room_name, input_device, output_device, echo_cancellation, avatar } => {
current_name = name.clone(); presence.name = name.clone();
current_avatar = avatar; presence.avatar = avatar;
// Finalize any recording before tearing down the old session — its // Finalize any recording before tearing down the old session — its
// capture/mixer feeders are about to stop. // capture/mixer feeders are about to stop.
@@ -1206,13 +1210,11 @@ async fn run_core_loop(
// Fresh join starts not sharing; clear any stale share ticket. // Fresh join starts not sharing; clear any stale share ticket.
current_sharing = None; current_sharing = None;
let self_state = PeerState { let self_state = presence.to_state(
name: current_name.clone(), is_muted.load(Ordering::Relaxed),
is_muted: is_muted.load(Ordering::Relaxed), endpoint.addr(),
addr: endpoint.addr(), None,
sharing: None, );
avatar: current_avatar.clone(),
};
// Snapshot THIS room's retained peers (by ticket) as extra bootstrap // Snapshot THIS room's retained peers (by ticket) as extra bootstrap
// targets so a rejoin can dial them (A8) — including after a detour // targets so a rejoin can dial them (A8) — including after a detour
@@ -1917,29 +1919,25 @@ async fn run_core_loop(
is_muted.store(new_state, Ordering::Relaxed); is_muted.store(new_state, Ordering::Relaxed);
if let Some(session) = &active_session { if let Some(session) = &active_session {
let self_state = PeerState { let self_state = presence.to_state(
name: current_name.clone(), new_state,
is_muted: new_state, net.endpoint.addr(),
addr: net.endpoint.addr(), current_sharing.clone(),
sharing: current_sharing.clone(), );
avatar: current_avatar.clone(),
};
let _ = session.room_state.update_self_state(self_state).await; let _ = session.room_state.update_self_state(self_state).await;
} }
} }
CoreCommand::SetAvatar(avatar) => { CoreCommand::SetAvatar(avatar) => {
current_avatar = avatar; presence.avatar = avatar;
// Re-announce presence so the room (incl. late joiners, via the // Re-announce presence so the room (incl. late joiners, via the
// retained presence) picks up the new avatar (W4). // retained presence) picks up the new avatar (W4).
if let Some(session) = &active_session { if let Some(session) = &active_session {
let self_state = PeerState { let self_state = presence.to_state(
name: current_name.clone(), is_muted.load(Ordering::Relaxed),
is_muted: is_muted.load(Ordering::Relaxed), net.endpoint.addr(),
addr: net.endpoint.addr(), current_sharing.clone(),
sharing: current_sharing.clone(), );
avatar: current_avatar.clone(),
};
let _ = session.room_state.update_self_state(self_state).await; let _ = session.room_state.update_self_state(self_state).await;
} }
} }
@@ -2337,13 +2335,11 @@ async fn run_core_loop(
crate::log_msg("Screen share host started"); crate::log_msg("Screen share host started");
session.screenshare_host = Some(child); session.screenshare_host = Some(child);
current_sharing = Some(ticket.clone()); current_sharing = Some(ticket.clone());
let self_state = PeerState { let self_state = presence.to_state(
name: current_name.clone(), is_muted.load(Ordering::Relaxed),
is_muted: is_muted.load(Ordering::Relaxed), net.endpoint.addr(),
addr: net.endpoint.addr(), Some(ticket),
sharing: Some(ticket), );
avatar: current_avatar.clone(),
};
let _ = session.room_state.update_self_state(self_state).await; let _ = session.room_state.update_self_state(self_state).await;
let _ = ui_tx.send(UiEvent::ScreenShareStarted).await; let _ = ui_tx.send(UiEvent::ScreenShareStarted).await;
} }
@@ -2362,13 +2358,11 @@ async fn run_core_loop(
let _ = child.kill().await; let _ = child.kill().await;
crate::log_msg("Screen share host stopped"); crate::log_msg("Screen share host stopped");
} }
let self_state = PeerState { let self_state = presence.to_state(
name: current_name.clone(), is_muted.load(Ordering::Relaxed),
is_muted: is_muted.load(Ordering::Relaxed), net.endpoint.addr(),
addr: net.endpoint.addr(), None,
sharing: None, );
avatar: current_avatar.clone(),
};
let _ = session.room_state.update_self_state(self_state).await; let _ = session.room_state.update_self_state(self_state).await;
} }
let _ = ui_tx.send(UiEvent::ScreenShareStopped).await; let _ = ui_tx.send(UiEvent::ScreenShareStopped).await;
+58
View File
@@ -41,6 +41,43 @@ pub struct PeerState {
pub avatar: crate::avatar::Avatar, pub avatar: crate::avatar::Avatar,
} }
/// The locally-owned, "sticky" pieces of our own presence: the identity fields
/// that change only on explicit user action and persist for the whole core
/// session. The remaining `PeerState` fields are *volatile* — mute state, current
/// `addr`, and the active screen-share ticket are read fresh at each announce — so
/// they are passed into [`SelfPresence::to_state`] rather than stored here.
///
/// This is the single source of truth for building our own `PeerState`: core
/// reconstructs self-state in several command branches (join, mute toggle, avatar
/// change, screen-share start/stop), and centralizing the `PeerState` literal here
/// means a new presence field is added in exactly one place instead of at every
/// call site.
#[derive(Debug, Clone, Default)]
pub struct SelfPresence {
pub name: String,
pub avatar: crate::avatar::Avatar,
}
impl SelfPresence {
/// Combine the sticky identity fields with the volatile per-announce fields
/// (`is_muted`, current `addr`, active-share `sharing` ticket) into a full
/// `PeerState` ready to announce over the gossip presence plane.
pub fn to_state(
&self,
is_muted: bool,
addr: iroh::EndpointAddr,
sharing: Option<String>,
) -> PeerState {
PeerState {
name: self.name.clone(),
is_muted,
addr,
sharing,
avatar: self.avatar.clone(),
}
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum RoomEvent { pub enum RoomEvent {
PeerJoined(EndpointId, PeerState), PeerJoined(EndpointId, PeerState),
@@ -358,6 +395,27 @@ mod tests {
assert_eq!(PeerSpeakTicket::restamp("not-a-ticket", EndpointAddr::from(me)), "not-a-ticket"); assert_eq!(PeerSpeakTicket::restamp("not-a-ticket", EndpointAddr::from(me)), "not-a-ticket");
} }
#[test]
fn self_presence_builds_peer_state_with_volatile_fields() {
let addr = EndpointAddr::from(SecretKey::generate().public());
let presence = SelfPresence {
name: "Alice".to_string(),
avatar: crate::avatar::Avatar::default(),
};
// Volatile fields come from the call; sticky fields from the struct.
let muted = presence.to_state(true, addr.clone(), Some("ticket".to_string()));
assert_eq!(muted.name, "Alice");
assert!(muted.is_muted);
assert_eq!(muted.addr.id, addr.id);
assert_eq!(muted.sharing.as_deref(), Some("ticket"));
assert_eq!(muted.avatar, crate::avatar::Avatar::default());
// The same sticky presence yields different volatile fields per announce.
let unmuted = presence.to_state(false, addr.clone(), None);
assert!(!unmuted.is_muted);
assert_eq!(unmuted.sharing, None);
assert_eq!(unmuted.name, muted.name);
}
#[test] #[test]
fn test_peer_state_serde_round_trip() { fn test_peer_state_serde_round_trip() {
let original = sample_peer_state(); let original = sample_peer_state();