From 869a9b320b20ddd4c2e9493ffe587ba933e665d0 Mon Sep 17 00:00:00 2001 From: dev Date: Tue, 5 May 2026 21:39:16 +0200 Subject: [PATCH] =?UTF-8?q?Nexo=20chat=20=E2=80=94=20dedup=20message=20bet?= =?UTF-8?q?ween=20POST=20response=20and=20WebSocket=20push?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The send flow now hits two paths concurrently: 1. `POST /api/matches/:id/messages` returns the persisted message. 2. The server broadcasts `dating.message.created` to every match member, including the sender. Whichever arrives first adds the message to the timeline; whoever arrives second was supposed to detect the duplicate and skip. The WebSocket handler already gated on `messages.some(id === incoming.id)`, but the POST branch unconditionally appended — so when WS won the race the POST branch produced a duplicate keyed entry and Svelte threw `each_key_duplicate` at indexes 10/11. Fix: the POST branch now performs the same dedup check before appending. Whichever path lands first wins; the other no-ops. The optimistic bubble is still removed by `clientNonce` in both paths. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/web/routes/dating/chat/[matchId]/+page.svelte | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/web/routes/dating/chat/[matchId]/+page.svelte b/src/web/routes/dating/chat/[matchId]/+page.svelte index a0212c3..290d584 100644 --- a/src/web/routes/dating/chat/[matchId]/+page.svelte +++ b/src/web/routes/dating/chat/[matchId]/+page.svelte @@ -146,8 +146,14 @@ try { const sent = await nexo.api.sendMessage(matchId, { body, clientNonce: nonce }); - messages = [...messages, sent]; pending = pending.filter((entry) => entry.clientNonce !== nonce); + // The realtime distributor pushes this same message back — + // whichever arrives first wins; the other branch must skip + // to avoid duplicating the keyed timeline (see the WS + // subscription above). + if (!messages.some((entry) => entry.id === sent.id)) { + messages = [...messages, sent]; + } await tick(); scrollToEnd(); } catch (caught) {