v0.0.8: Server-side message deduplication
Server: - DedupTracker in AppState: bounded HashSet (10,000 IDs, FIFO eviction) - send_message: extracts message ID from bincode, drops duplicates - WS handler: dedup on both binary and JSON message frames - extract_message_id() parses all WireMessage variants Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,10 +4,26 @@ use axum::{
|
||||
Json, Router,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use warzone_protocol::message::WireMessage;
|
||||
|
||||
use crate::errors::AppResult;
|
||||
use crate::state::AppState;
|
||||
|
||||
/// Try to extract the message ID from raw bincode-serialized WireMessage bytes.
|
||||
fn extract_message_id(data: &[u8]) -> Option<String> {
|
||||
if let Ok(wire) = bincode::deserialize::<WireMessage>(data) {
|
||||
match wire {
|
||||
WireMessage::KeyExchange { id, .. } => Some(id),
|
||||
WireMessage::Message { id, .. } => Some(id),
|
||||
WireMessage::FileHeader { id, .. } => Some(id),
|
||||
WireMessage::FileChunk { id, .. } => Some(id),
|
||||
WireMessage::Receipt { message_id, .. } => Some(message_id),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Touch the alias TTL for a fingerprint (renew on authenticated action).
|
||||
pub fn renew_alias_ttl(db: &sled::Tree, fp: &str) {
|
||||
let alias_key = format!("fp:{}", fp);
|
||||
@@ -55,6 +71,14 @@ async fn send_message(
|
||||
) -> AppResult<Json<serde_json::Value>> {
|
||||
let to = normalize_fp(&req.to);
|
||||
|
||||
// Dedup: if we have already seen this message ID, silently drop it
|
||||
if let Some(msg_id) = extract_message_id(&req.message) {
|
||||
if state.dedup.check_and_insert(&msg_id) {
|
||||
tracing::debug!("Dedup: dropping duplicate message {}", msg_id);
|
||||
return Ok(Json(serde_json::json!({ "ok": true })));
|
||||
}
|
||||
}
|
||||
|
||||
// Try WebSocket push first (instant delivery)
|
||||
if state.push_to_client(&to, &req.message).await {
|
||||
tracing::info!("Pushed message to {} via WS ({} bytes)", to, req.message.len());
|
||||
|
||||
@@ -17,9 +17,25 @@ use axum::{
|
||||
Router,
|
||||
};
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use warzone_protocol::message::WireMessage;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
/// Try to extract the message ID from raw bincode-serialized WireMessage bytes.
|
||||
fn extract_message_id(data: &[u8]) -> Option<String> {
|
||||
if let Ok(wire) = bincode::deserialize::<WireMessage>(data) {
|
||||
match wire {
|
||||
WireMessage::KeyExchange { id, .. } => Some(id),
|
||||
WireMessage::Message { id, .. } => Some(id),
|
||||
WireMessage::FileHeader { id, .. } => Some(id),
|
||||
WireMessage::FileChunk { id, .. } => Some(id),
|
||||
WireMessage::Receipt { message_id, .. } => Some(message_id),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn routes() -> Router<AppState> {
|
||||
Router::new().route("/ws/:fingerprint", get(ws_handler))
|
||||
}
|
||||
@@ -90,6 +106,14 @@ async fn handle_socket(socket: WebSocket, state: AppState, fingerprint: String)
|
||||
let to_fp = normalize_fp(&header);
|
||||
let message = &data[64..];
|
||||
|
||||
// Dedup: skip if we already processed this message ID
|
||||
if let Some(msg_id) = extract_message_id(message) {
|
||||
if state_clone.dedup.check_and_insert(&msg_id) {
|
||||
tracing::debug!("WS dedup: dropping duplicate binary message {}", msg_id);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Try push to connected client first
|
||||
if !state_clone.push_to_client(&to_fp, message).await {
|
||||
// Queue in DB
|
||||
@@ -110,6 +134,14 @@ async fn handle_socket(socket: WebSocket, state: AppState, fingerprint: String)
|
||||
.filter_map(|v| v.as_u64().map(|n| n as u8))
|
||||
.collect();
|
||||
|
||||
// Dedup: skip if we already processed this message ID
|
||||
if let Some(msg_id) = extract_message_id(&message) {
|
||||
if state_clone.dedup.check_and_insert(&msg_id) {
|
||||
tracing::debug!("WS dedup: dropping duplicate JSON message {}", msg_id);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if !state_clone.push_to_client(&to_fp, &message).await {
|
||||
let key = format!("queue:{}:{}", to_fp, uuid::Uuid::new_v4());
|
||||
let _ = state_clone.db.messages.insert(key.as_bytes(), message);
|
||||
|
||||
Reference in New Issue
Block a user