fix(messages): queue-based sending + simple dedup
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions

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) <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-03-28 17:40:30 +01:00
parent d8db218df2
commit 8ac13f47b2

View File

@ -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) {