From a5307d3d6a354cdb1028eb3b2fcf05b81fc5f4c0 Mon Sep 17 00:00:00 2001 From: ddidderr Date: Thu, 21 May 2026 17:17:36 +0200 Subject: [PATCH] fix: suppress failed event for cancelled downloads Manual download cancellation uses the same internal error path as transfer failures after the terminal-event ordering fix. That made the Tauri UI receive DownloadGameFilesFailed and show a red failure state even though the user had asked for the cancellation. Keep a clone of the cancellation token in the download task and check it after the transfer task returns an error. Cancelled downloads still refresh local state and clear active operation tracking, but they no longer emit the failed event. Real, uncancelled errors continue to send DownloadGameFilesFailed. Add unit coverage for both branches so the UI-facing event contract stays explicit. Test Plan: - just fmt - just test - just clippy - git diff --check Refs: manual cancel regression from app-state follow-up --- crates/lanspread-peer/src/handlers.rs | 51 +++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/crates/lanspread-peer/src/handlers.rs b/crates/lanspread-peer/src/handlers.rs index 3ccf4d0..8adca68 100644 --- a/crates/lanspread-peer/src/handlers.rs +++ b/crates/lanspread-peer/src/handlers.rs @@ -321,7 +321,7 @@ pub async fn handle_download_game_files_command( peer_whitelist, file_peer_map, tx_notify_ui_clone.clone(), - cancel_token, + cancel_token.clone(), ) .await; @@ -398,8 +398,17 @@ pub async fn handle_download_game_files_command( } end_download_operation(&ctx_clone, &tx_notify_ui_clone, &download_id).await; download_state_guard.disarm(); - log::error!("Download failed for {download_id}: {e}"); - send_download_failed(&tx_notify_ui_clone, &download_id); + let download_was_cancelled = cancel_token.is_cancelled(); + if download_was_cancelled { + log::info!("Download cancelled for {download_id}: {e}"); + } else { + log::error!("Download failed for {download_id}: {e}"); + } + send_download_failed_unless_cancelled( + &tx_notify_ui_clone, + &download_id, + download_was_cancelled, + ); } } }); @@ -787,6 +796,19 @@ fn send_download_failed(tx_notify_ui: &UnboundedSender, id: &str) { } } +fn send_download_failed_unless_cancelled( + tx_notify_ui: &UnboundedSender, + id: &str, + cancelled: bool, +) -> bool { + if cancelled { + return false; + } + + send_download_failed(tx_notify_ui, id); + true +} + async fn end_download_operation(ctx: &Ctx, tx_notify_ui: &UnboundedSender, id: &str) { end_operation(ctx, tx_notify_ui, id).await; clear_active_download(ctx, id).await; @@ -1097,6 +1119,29 @@ mod tests { ) } + #[test] + fn cancelled_download_error_does_not_emit_failed_event() { + let (tx, mut rx) = mpsc::unbounded_channel(); + + let emitted = send_download_failed_unless_cancelled(&tx, "game", true); + + assert!(!emitted); + assert!(rx.try_recv().is_err()); + } + + #[test] + fn uncancelled_download_error_emits_failed_event() { + let (tx, mut rx) = mpsc::unbounded_channel(); + + let emitted = send_download_failed_unless_cancelled(&tx, "game", false); + + assert!(emitted); + assert!(matches!( + rx.try_recv(), + Ok(PeerEvent::DownloadGameFilesFailed { id }) if id == "game" + )); + } + async fn recv_event(rx: &mut mpsc::UnboundedReceiver) -> PeerEvent { tokio::time::timeout(Duration::from_secs(1), rx.recv()) .await