diff --git a/src/gui/presence.rs b/src/gui/presence.rs index ff0dd00..d951453 100644 --- a/src/gui/presence.rs +++ b/src/gui/presence.rs @@ -10,6 +10,7 @@ //! message wakes the loop even while the window is hidden to the tray — the same //! trick the headless-child reader uses. +use std::sync::Arc; use std::sync::mpsc::{self, Receiver}; use std::thread; @@ -232,32 +233,62 @@ fn run( /// [`SHARE_RETRY`] until all are delivered (or the task is aborted by a /// StartShare/StopShare). Emits one [`PresenceEvent::ShareDelivered`] per peer /// the moment its ACK comes back — that ACK *is* the delivery signal. +/// +/// Each round fires all still-pending peers **concurrently**, so a single +/// offline friend's ~10s connect timeout doesn't serialise the whole round +/// (which it did when peers were tried one at a time). async fn run_share( ep: Endpoint, msg: ControlMsg, mut pending: Vec, ui: tmpsc::Sender, ) { + // The code is immutable for the campaign's life; share it across the + // per-peer tasks via an `Arc` rather than re-cloning the payload each round. + let msg = Arc::new(msg); while !pending.is_empty() { - let mut still = Vec::new(); + let mut round = tokio::task::JoinSet::new(); for peer in pending { - match control::send(&ep, peer, &msg).await { - Ok(()) => { - tracing::info!(%peer, "presence: shared code delivered"); - if ui - .send(PresenceEvent::ShareDelivered { peer }) - .await - .is_err() - { - return; // UI gone — nothing left to report to + let ep = ep.clone(); + let msg = Arc::clone(&msg); + round.spawn(async move { + match control::send(&ep, peer, &msg).await { + Ok(()) => (peer, true), + Err(e) => { + tracing::debug!(%peer, "presence: share not yet delivered: {e:#}"); + (peer, false) } } + }); + } + + let mut still = Vec::new(); + while let Some(joined) = round.join_next().await { + let (peer, delivered) = match joined { + Ok(outcome) => outcome, + // A send task panicking is unexpected; log and drop that peer + // from the campaign rather than abort the whole round. (A + // campaign-level abort drops this future entirely — we never + // observe that as a JoinError here.) Err(e) => { - tracing::debug!(%peer, "presence: share not yet delivered: {e:#}"); - still.push(peer); + tracing::warn!("presence: share task failed: {e}"); + continue; } + }; + if delivered { + tracing::info!(%peer, "presence: shared code delivered"); + if ui + .send(PresenceEvent::ShareDelivered { peer }) + .await + .is_err() + { + return; // UI gone — nothing left to report to + } + } else { + still.push(peer); } } + if still.is_empty() { break; }