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:
+37
-43
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user