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
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:
parent
d8db218df2
commit
8ac13f47b2
@ -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) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user