Files
peerspeak/src/core/messages.rs
T
molluskandClaude Opus 4.8 ebfc39de46
cargo-deny / cargo-deny (push) Has been cancelled
windows-build / windows-build (push) Has been cancelled
core: route critical commands through a reliable unbounded channel (A15)
CoreController::send put every app->core command on a single bounded
depth-100 channel via try_send and discarded the result. iced slider
drags emit ~60-120 commands/sec, so a drag burst could transiently
saturate the queue exactly when the user hit mute / released PTT /
left a room, silently dropping that critical command and leaving the
mic hot -- a privacy/state mismatch.

Split the queue by drop-tolerance:
- A pure delivery_class(&CoreCommand) classifier in messages.rs maps the
  7 continuous audio sliders to BestEffort and every other (discrete,
  human-paced) command to Reliable. The match has no wildcard arm, so a
  new CoreCommand variant fails to compile until it is classified.
- CoreController now holds two senders: an unbounded reliable channel
  and the existing bounded(100) best-effort channel. send() routes by
  class; Reliable uses unbounded send (fails only if the core loop is
  dead), BestEffort keeps today's bounded try_send.
- run_core_loop takes both receivers and drains them with a biased
  select: reliable first, best-effort second, game-change third.

Unbounded is safe because the only machine-rate producer (slider drags)
stays on the bounded channel; Reliable commands are all human-paced.
command_sender() and the awaiting Shutdown path are unchanged.

Implemented by Codex (gpt-5.5), senior-reviewed and verified here:
454 lib tests pass, clippy --all-targets clean, release builds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 04:19:40 -04:00

333 lines
16 KiB
Rust

use crate::config::{NetworkMode, RecordingMode};
use crate::friends::Friend;
use crate::network::PeerState;
use crate::presence::{FriendPresence, PresenceMode};
use iroh::{EndpointAddr, EndpointId};
#[derive(Debug, Clone)]
pub enum CoreCommand {
/// Join a room. `ticket` is "create" (or empty) to mint a fresh room, else a
/// share ticket to join. `room_name` is the creator's chosen cosmetic label
/// for a NEW room; it's ignored when joining (the label rides in the ticket).
Join { name: String, ticket: String, room_name: String, input_device: Option<String>, output_device: Option<String>, echo_cancellation: bool, avatar: crate::avatar::Avatar },
Leave,
/// Orderly app shutdown: finalize recordings, leave any active room, stop local
/// audio/screen-share work, close the persistent network stack, then ack with
/// [`UiEvent::ShutdownComplete`].
Shutdown,
ToggleMute,
/// Change our avatar (W4) and re-announce it to the room over presence.
SetAvatar(crate::avatar::Avatar),
ToggleDeafen,
SetPttMode(bool),
SetPttActive(bool),
SetPeerVolume(EndpointId, f32),
/// Listener-side per-peer EQ. Local only; never leaves this app instance.
SetPeerEq(EndpointId, crate::audio::eq::EqSettings),
/// Listener-side per-peer pan. Local only; never leaves this app instance.
SetPeerPan(EndpointId, f32),
/// Listener-side per-peer noise gate threshold (normalized RMS, `0.0` = off).
/// Applies the same smooth gate as the mic path to a peer's incoming audio,
/// to suppress their background noise on our end. Local only.
SetPeerGate(EndpointId, f32),
/// Locally mute/unmute a peer: when muted, their audio is decoded (so levels
/// still show) but not mixed into our output.
SetPeerMuted(EndpointId, bool),
SetNoiseGateThreshold(f32),
/// App-internal capture gain (mic), applied before the gate/encode. 1.0 = unity.
SetInputVolume(f32),
/// App-internal playback gain on the mixed output. 1.0 = unity.
SetOutputVolume(f32),
/// Start/stop a standalone capture-only stream that reports the raw mic
/// level via [`UiEvent::MicLevel`], for gate calibration outside a call.
/// Ignored while a room session is active (the in-call meter covers that).
SetMicMonitor { enabled: bool, input_device: Option<String> },
/// Set the relay/discovery posture. Takes effect on the next room join,
/// since the endpoint is (re)built then.
SetNetworkMode(NetworkMode),
/// Start/stop recording the call to a local WAV (your mic + the incoming
/// mix). No-op start if already recording / not in a call.
SetRecording(bool),
/// Set what a recording captures (mixed / per-peer stems / both). Takes
/// effect on the next recording start. Sent at startup from config.
SetRecordingMode(RecordingMode),
/// Broadcast a room text-chat message. No-op when not in a call.
SendChat(String),
/// Send a chat message carrying a file attachment. The app has already read +
/// capped the file and built the descriptor; core makes the bytes available
/// on the file plane and broadcasts the descriptor.
SendChatFile { text: String, attachment: crate::files::ChatAttachment, data: Vec<u8> },
/// Fetch a received attachment's bytes from its sender over the file plane
/// (used for on-demand file/chip downloads; images are auto-fetched on
/// receipt). Replies with `AttachmentReady`/`AttachmentFailed`.
FetchAttachment { from: EndpointId, attachment: crate::files::ChatAttachment },
/// Set the pixelpass binary location (config override, empty = use `$PATH`).
/// Sent at startup so screen-share can resolve the binary.
SetPixelpassPath(Option<String>),
/// Enumerate apps currently producing audio (for the screen-share audio
/// picker, A23). Replies with [`UiEvent::AudioAppsListed`]. Cheap shell-out;
/// safe to call each time the picker opens.
ListAudioApps,
/// Start sharing our screen: spawn a pixelpass host and announce its ticket
/// on our presence so the room can watch. No-op when not in a call.
/// `audio_app` selects which app's audio to capture: `Some(name)` captures
/// only that app (avoiding the call-loopback echo, A23); `None` shares the
/// whole desktop audio (the legacy behavior).
StartScreenShare { audio_app: Option<String> },
/// Stop sharing our screen: kill the pixelpass host and clear the presence
/// ticket. No-op when not sharing.
StopScreenShare,
/// Watch a peer's screen share: spawn a pixelpass viewer for `ticket` and
/// open it in a local player.
ViewShare(String),
/// Mint a fresh persistent identity (W7), discarding the old one. Takes effect
/// on the next room join (the endpoint is rebuilt then). The core replies with
/// an updated [`UiEvent::IdentityStatus`].
RegenerateIdentity,
/// Add a friend (W7). Core owns the friends store: it mutates + persists it and
/// replies with [`UiEvent::FriendsUpdated`]. `addr` seeds `last_addr` if known
/// (e.g. added from a room). Idempotent — re-adding an existing id is a no-op.
AddFriend { id: EndpointId, name: String, addr: Option<EndpointAddr> },
/// Remove a friend by id (W7).
RemoveFriend(EndpointId),
/// Locally rename a friend (W7).
RenameFriend(EndpointId, String),
/// Set our presence posture (W7). Gates the idle listener (answer friends-only /
/// invisible) and the outbound ping scheduler (invisible = fully dark). Sent at
/// startup from config and whenever the user changes it.
SetPresenceMode(PresenceMode),
/// Toggle broadcasting the detected game as presence (game detection). Opt-in,
/// default OFF. Enabling immediately publishes the current game; disabling
/// immediately publishes `game: None`. Detection for the local background runs
/// regardless. Sent at startup from config and on user toggle.
SetGamePresenceEnabled(bool),
/// Set the manual game-detection override (`Auto` / `None` / a forced game).
/// Forwarded to the detector and applied immediately (bypasses debounce).
SetGameOverride(crate::game::ManualOverride),
/// Replace the user process→display-name mappings used by the non-Steam
/// detection fallback. Sent at startup from config and after Settings edits.
SetGameProcessMap(std::collections::BTreeMap<String, String>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeliveryClass {
Reliable,
BestEffort,
}
/// Route a command by how bad it is to drop it. Discrete, human-paced user
/// actions are Reliable (must land). The only high-frequency commands are the
/// continuous audio sliders, where dropping intermediate values is harmless;
/// those are BestEffort.
pub fn delivery_class(cmd: &CoreCommand) -> DeliveryClass {
match cmd {
CoreCommand::SetPeerVolume(_, _)
| CoreCommand::SetPeerPan(_, _)
| CoreCommand::SetPeerGate(_, _)
| CoreCommand::SetPeerEq(_, _)
| CoreCommand::SetInputVolume(_)
| CoreCommand::SetOutputVolume(_)
| CoreCommand::SetNoiseGateThreshold(_) => DeliveryClass::BestEffort,
CoreCommand::Join {
name: _,
ticket: _,
room_name: _,
input_device: _,
output_device: _,
echo_cancellation: _,
avatar: _,
}
| CoreCommand::Leave
| CoreCommand::Shutdown
| CoreCommand::ToggleMute
| CoreCommand::SetAvatar(_)
| CoreCommand::ToggleDeafen
| CoreCommand::SetPttMode(_)
| CoreCommand::SetPttActive(_)
| CoreCommand::SetPeerMuted(_, _)
| CoreCommand::SetMicMonitor {
enabled: _,
input_device: _,
}
| CoreCommand::SetNetworkMode(_)
| CoreCommand::SetRecording(_)
| CoreCommand::SetRecordingMode(_)
| CoreCommand::SendChat(_)
| CoreCommand::SendChatFile {
text: _,
attachment: _,
data: _,
}
| CoreCommand::FetchAttachment {
from: _,
attachment: _,
}
| CoreCommand::SetPixelpassPath(_)
| CoreCommand::ListAudioApps
| CoreCommand::StartScreenShare { audio_app: _ }
| CoreCommand::StopScreenShare
| CoreCommand::ViewShare(_)
| CoreCommand::RegenerateIdentity
| CoreCommand::AddFriend {
id: _,
name: _,
addr: _,
}
| CoreCommand::RemoveFriend(_)
| CoreCommand::RenameFriend(_, _)
| CoreCommand::SetPresenceMode(_)
| CoreCommand::SetGamePresenceEnabled(_)
| CoreCommand::SetGameOverride(_)
| CoreCommand::SetGameProcessMap(_) => DeliveryClass::Reliable,
}
}
#[derive(Debug, Clone)]
pub enum UiEvent {
RoomJoined { ticket: String, self_id: String },
RoomLeft,
/// Clear room-scoped UI state after a failed in-call room switch, without a
/// leave chime. The persistent identity remains unchanged.
RoomReset,
PeerJoined { id: EndpointId, state: PeerState },
PeerLeft { id: EndpointId },
/// The fixed reconnect grace expired and bounded background gossip recovery
/// has started. This is non-terminal and must not play the failure chime.
PeerRecoveryStarted { id: EndpointId },
PeerConnectionFailed { id: EndpointId },
PeerUpdated { id: EndpointId, state: PeerState },
/// Audio link to a peer is being (re)established — show a connecting state.
PeerConnecting { id: EndpointId },
/// Audio link to a peer is up and carrying audio.
PeerConnected { id: EndpointId },
AudioLevels(Vec<(EndpointId, f32)>),
/// Raw (pre-gate, pre-mute) normalized RMS of the local mic, `0.0..=1.0`,
/// for the settings level meter. Throttled to ~10/sec.
MicLevel(f32),
/// Call recording started; carries the absolute WAV path being written.
RecordingStarted { path: String },
/// Call recording stopped; carries the finished WAV path.
RecordingStopped { path: String },
/// A room text-chat message arrived from a peer (never our own — local
/// messages are echoed by the UI on send). `from` is the sender's node id
/// string, used to key their avatar (W4).
ChatMessage { from: String, name: String, text: String, attachment: Option<crate::files::ChatAttachment> },
/// An attachment's bytes are now available (auto-fetched for images, or
/// fetched on demand for files). Keyed by `(from, id)`: the id is
/// attacker-chosen, so a malicious peer can reuse a victim's id — the author
/// disambiguates whose bytes these are and stops content aliasing (Tier C
/// F-12).
AttachmentReady { from: EndpointId, id: crate::files::AttachmentId, data: Vec<u8> },
/// An attachment fetch failed (sender gone, too large, decode error, etc.).
AttachmentFailed { from: EndpointId, id: crate::files::AttachmentId, error: String },
/// The apps currently producing audio, for the screen-share audio picker
/// (A23). Sorted, deduplicated `application.name`s; empty when nothing is
/// playing or enumeration isn't available. `app_audio_supported` reports
/// whether the resolved pixelpass understands `--strict-audio`: when `false`
/// (an older pixelpass) the picker must offer whole-desktop audio only, since
/// a per-app share would pass a flag that older binary rejects (audit P2).
AudioAppsListed { apps: Vec<String>, app_audio_supported: bool },
/// Our own screen share started; the UI flips the Share button to "Stop".
ScreenShareStarted,
/// Our own screen share stopped (or failed to start).
ScreenShareStopped,
/// Per-app screen-share audio routing state (A23). `true` = the app we chose
/// is now reaching viewers; `false` = its audio stopped, so under our strict
/// run viewers currently hear silence. The UI shows a transient warning while
/// `false`. Only meaningful while sharing a specific app (not whole-desktop).
ShareAudioActive(bool),
/// A validly signed peer cannot be admitted because its gossip timestamp is
/// outside the replay freshness window. `peer_ahead` describes the peer's
/// sender-stamped timestamp relative to this machine's clock.
ClockSkewWarning { skew_secs: u64, peer_ahead: bool },
/// Our node identity (W7): the current node id string, and whether it is
/// PERSISTED to disk. Sent once at startup and again after a regenerate.
/// `persisted = false` means the key file couldn't be read/written and we're
/// running on an ephemeral fallback — a degraded state the UI must surface,
/// since the id (and thus friend recognition) won't survive the next launch.
/// `error` carries the reason when degraded, for the UI explainer.
IdentityStatus { node_id: String, persisted: bool, error: Option<String> },
/// The friends list (W7), now owned by core. Sent at startup (after load) and
/// after every add/remove/rename so the GUI renders from this snapshot instead
/// of owning the store. `read_only` is true when `friends.json` failed to load
/// (malformed) — the GUI shows a degraded warning and disables edits so we never
/// overwrite the damaged file (backlog A16).
FriendsUpdated { friends: Vec<Friend>, read_only: bool },
/// A friend's live presence from a successful ping reply (W7): online, or in a
/// joinable gathering (with a one-click ticket). Emitted by the outbound ping
/// scheduler; absence of a recent event = treat as offline.
FriendPresence { id: EndpointId, presence: FriendPresence },
/// Core corrected the committed presence posture. Usually the Discoverable
/// time-box elapsed and the core auto-reverted to `Normal`; on discovery apply
/// failure, this carries the previous truthful mode. The GUI must mirror +
/// persist this so its presence picker matches the endpoint's discovery state.
PresenceModeReverted { mode: PresenceMode },
/// The locally-detected running game changed (game detection). Carries the
/// debounced `DetectedGame` (id + display name + source) or `None` when nothing
/// is detected. The GUI uses the stable `id` to switch the per-game background
/// (W18) and may show a local "Playing …" indicator. Emitted regardless of
/// whether game presence is being broadcast — the broadcast is core's own job.
GameChanged(Option<crate::game::DetectedGame>),
/// Core finished orderly app shutdown and the GUI can exit.
ShutdownComplete,
Error(String),
}
#[cfg(test)]
mod tests {
use super::{delivery_class, CoreCommand, DeliveryClass};
use crate::audio::eq::EqSettings;
use crate::presence::PresenceMode;
use iroh::{EndpointId, SecretKey};
fn endpoint_id() -> EndpointId {
SecretKey::generate().public()
}
#[test]
fn continuous_audio_controls_are_best_effort() {
let peer = endpoint_id();
let commands = [
CoreCommand::SetPeerVolume(peer, 0.7),
CoreCommand::SetPeerPan(peer, -0.2),
CoreCommand::SetPeerGate(peer, 0.1),
CoreCommand::SetPeerEq(peer, EqSettings::default()),
CoreCommand::SetInputVolume(0.8),
CoreCommand::SetOutputVolume(0.9),
CoreCommand::SetNoiseGateThreshold(0.02),
];
for cmd in commands {
assert_eq!(delivery_class(&cmd), DeliveryClass::BestEffort);
}
}
#[test]
fn discrete_user_actions_are_reliable() {
let peer = endpoint_id();
let commands = [
CoreCommand::ToggleMute,
CoreCommand::SetPttActive(false),
CoreCommand::Leave,
CoreCommand::RegenerateIdentity,
CoreCommand::Join {
name: "Peer".to_string(),
ticket: "create".to_string(),
room_name: "Room".to_string(),
input_device: None,
output_device: None,
echo_cancellation: true,
avatar: crate::avatar::Avatar::default(),
},
CoreCommand::SetPeerMuted(peer, true),
CoreCommand::SetPresenceMode(PresenceMode::Normal),
CoreCommand::SendChat("hello".to_string()),
];
for cmd in commands {
assert_eq!(delivery_class(&cmd), DeliveryClass::Reliable);
}
}
}