diff --git a/src-tauri/src/commands/session.rs b/src-tauri/src/commands/session.rs index 9bb7c40..8d30acf 100644 --- a/src-tauri/src/commands/session.rs +++ b/src-tauri/src/commands/session.rs @@ -468,11 +468,24 @@ async fn handle_incoming_message(app: &AppHandle, frame: &[u8]) -> anyhow::Resul return Ok(()); } + let state = app.state::(); + + // Check for duplicate message to provide idempotency on network redelivery + let mut received_ids = state.received_message_ids.lock().await; + if received_ids.contains(&wire.id) { + // Duplicate message - skip silently + #[cfg(debug_assertions)] + log::debug!("duplicate message ignored: {}", wire.id); + drop(received_ids); + return Ok(()); + } + received_ids.insert(wire.id.clone()); + drop(received_ids); + let ct = B64 .decode(&wire.ct) .map_err(|e| anyhow::anyhow!("base64 decode: {}", e))?; - let state = app.state::(); let settings = state.settings.lock().await.clone(); let plaintext_buf = { @@ -515,6 +528,8 @@ pub async fn close_session(state: State<'_, AppState>) -> Result<(), String> { let _ = s.stream_writer.shutdown().await; } state.messages.lock().await.clear(); + // Clear message ID tracking so new session can receive same IDs + state.received_message_ids.lock().await.clear(); Ok(()) } diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index ff5be48..2b66061 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -1,4 +1,5 @@ use serde::Serialize; +use std::collections::HashSet; use tokio::{ io::WriteHalf, net::TcpStream, @@ -81,6 +82,8 @@ pub struct AppState { pub router_sam_port: Mutex>, /// Last known router status — queried by frontend on mount to avoid event race on release. pub router_status: Mutex, + /// Track message IDs we've already received to provide idempotency on network redelivery. + pub received_message_ids: Mutex>, } impl Default for AppState { @@ -93,6 +96,7 @@ impl Default for AppState { i2p: Mutex::new(None), router_sam_port: Mutex::new(None), router_status: Mutex::new("idle".to_string()), + received_message_ids: Mutex::new(HashSet::new()), } } }