From 8ac13f47b2d36e6f3c21cbdf24953b35d813916f Mon Sep 17 00:00:00 2001 From: Maciej Pienczyn Date: Sat, 28 Mar 2026 17:40:30 +0100 Subject: [PATCH] fix(messages): queue-based sending + simple dedup Root cause: rapid Enter sends caused parallel API calls creating real duplicates in DB. Fix: message queue processes one at a time. - sendContent() adds to queue + shows optimistic UI instantly - _processQueue() sends sequentially (await each before next) - Polling skips own messages (they come via optimistic UI) - Simple ID-only dedup in appendMessage (no complex content matching) Co-Authored-By: Claude Opus 4.6 (1M context) --- static/js/conversations.js | 82 ++++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 39 deletions(-) diff --git a/static/js/conversations.js b/static/js/conversations.js index 907327b..0a037f8 100644 --- a/static/js/conversations.js +++ b/static/js/conversations.js @@ -727,22 +727,8 @@ if (!state.messages[convId]) state.messages[convId] = []; // Dedup: skip if message with this ID already exists - // Dedup: skip if exact ID match OR if optimistic message from same sender within 10s - var dominated = state.messages[convId].some(function(m) { - if (m.id === msg.id) return true; - if (m._optimistic && msg.sender_id && m.sender_id === msg.sender_id) { - // Time-based match: if optimistic msg was sent within 10s, it's the same - var mTime = new Date(m.created_at).getTime(); - var msgTime = new Date(msg.created_at).getTime(); - if (Math.abs(mTime - msgTime) < 10000) { - m.id = msg.id; - m._optimistic = false; - return true; - } - } - return false; - }); - if (dominated) return; + // Simple dedup by ID (string or number) + if (msg.id && state.messages[convId].some(function(m) { return m.id === msg.id; })) return; state.messages[convId].push(msg); if (convId !== state.currentConversationId) return; @@ -1288,21 +1274,48 @@ } }, - // sendContent: called with pre-captured content (from Enter keydown) - sendContent: async function (html, text) { - if (!state.currentConversationId) return; - var convId = state.currentConversationId; - var savedReplyTo = state.replyToMessage; - var savedFiles = state.attachedFiles.slice(); + // Queue-based sending: Enter adds to queue, queue processes one at a time + _sendQueue: [], + _queueProcessing: false, - // Clear state (editor already cleared by keydown handler) + sendContent: function (html, text) { + if (!state.currentConversationId) return; + // Add to queue with current state snapshot + Composer._sendQueue.push({ + convId: state.currentConversationId, + html: html, + text: text, + replyTo: state.replyToMessage, + files: state.attachedFiles.slice() + }); state.attachedFiles = []; state.replyToMessage = null; var replyPreview = document.getElementById('replyPreview'); if (replyPreview) replyPreview.style.display = 'none'; Composer.renderAttachments(); + // Show optimistic message immediately + var tempId = 'temp-' + Date.now() + '-' + Math.random(); + ChatView.appendMessage({ + id: tempId, + conversation_id: state.currentConversationId, + content: html, + sender_id: window.__CURRENT_USER__ ? window.__CURRENT_USER__.id : null, + sender: window.__CURRENT_USER__ || {}, + created_at: new Date().toISOString(), + _optimistic: true + }); + // Process queue + Composer._processQueue(); + }, - return Composer._doSend(convId, html, text, savedReplyTo, savedFiles); + _processQueue: async function () { + if (Composer._queueProcessing || !Composer._sendQueue.length) return; + Composer._queueProcessing = true; + while (Composer._sendQueue.length > 0) { + var item = Composer._sendQueue.shift(); + await Composer._doSend(item.convId, item.html, item.text, item.replyTo, item.files); + } + Composer._queueProcessing = false; }, // send: called from button click — reads from Quill @@ -1329,20 +1342,7 @@ }, _doSend: async function (convId, html, text, savedReplyTo, savedFiles) { - - // Show optimistic message in DOM instantly - var tempId = 'temp-' + Date.now(); - var optimisticMsg = { - id: tempId, - conversation_id: convId, - content: html, - sender_id: window.__CURRENT_USER__ ? window.__CURRENT_USER__.id : null, - sender: window.__CURRENT_USER__ || {}, - created_at: new Date().toISOString(), - _optimistic: true - }; - ChatView.appendMessage(optimisticMsg); - + // Optimistic message already shown by sendContent() — just do the API call try { var fd = new FormData(); if (html && text) fd.append('content', html); @@ -1714,8 +1714,12 @@ if (!data.messages || !data.messages.length) return; // Find messages newer than what we have + var currentUserId = window.__CURRENT_USER__ ? window.__CURRENT_USER__.id : null; var newMsgs = data.messages.filter(function (msg) { - return msg.id > newestId; + if (msg.id <= newestId) return false; + // Skip own messages — already displayed by optimistic UI + if (currentUserId && msg.sender_id === currentUserId) return false; + return true; }); if (newMsgs.length > 0) {