From 58f2aeff09e18f24561d4aa7996257df1dcd7078 Mon Sep 17 00:00:00 2001 From: Rafabd1 Date: Sat, 14 Mar 2026 01:33:33 -0300 Subject: [PATCH] fix: add idempotency to prevent duplicate messages on network redelivery (#10) - Add HashSet to track received message IDs in AppState - Check for duplicate message IDs before processing in handle_incoming_message - Silently ignore duplicate messages (log in debug builds) - Clear message ID tracking when session closes to allow reusing IDs in new sessions - Prevents duplicate messages from I2P network redelivery from appearing in chat This fixes issue #10 where I2P network redelivery could cause the same message to appear multiple times in the chat. Now messages are tracked by ID and duplicates are automatically filtered out. --- src-tauri/src/commands/session.rs | 17 ++++++++++++++++- src-tauri/src/state.rs | 4 ++++ 2 files changed, 20 insertions(+), 1 deletion(-) 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()), } } }