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] = [];
|
if (!state.messages[convId]) state.messages[convId] = [];
|
||||||
|
|
||||||
// Dedup: skip if message with this ID already exists
|
// Dedup: skip if message with this ID already exists
|
||||||
// Dedup: skip if exact ID match OR if optimistic message from same sender within 10s
|
// Simple dedup by ID (string or number)
|
||||||
var dominated = state.messages[convId].some(function(m) {
|
if (msg.id && state.messages[convId].some(function(m) { return m.id === msg.id; })) return;
|
||||||
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;
|
|
||||||
state.messages[convId].push(msg);
|
state.messages[convId].push(msg);
|
||||||
|
|
||||||
if (convId !== state.currentConversationId) return;
|
if (convId !== state.currentConversationId) return;
|
||||||
@ -1288,21 +1274,48 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// sendContent: called with pre-captured content (from Enter keydown)
|
// Queue-based sending: Enter adds to queue, queue processes one at a time
|
||||||
sendContent: async function (html, text) {
|
_sendQueue: [],
|
||||||
if (!state.currentConversationId) return;
|
_queueProcessing: false,
|
||||||
var convId = state.currentConversationId;
|
|
||||||
var savedReplyTo = state.replyToMessage;
|
|
||||||
var savedFiles = state.attachedFiles.slice();
|
|
||||||
|
|
||||||
// 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.attachedFiles = [];
|
||||||
state.replyToMessage = null;
|
state.replyToMessage = null;
|
||||||
var replyPreview = document.getElementById('replyPreview');
|
var replyPreview = document.getElementById('replyPreview');
|
||||||
if (replyPreview) replyPreview.style.display = 'none';
|
if (replyPreview) replyPreview.style.display = 'none';
|
||||||
Composer.renderAttachments();
|
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
|
// send: called from button click — reads from Quill
|
||||||
@ -1329,20 +1342,7 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
_doSend: async function (convId, html, text, savedReplyTo, savedFiles) {
|
_doSend: async function (convId, html, text, savedReplyTo, savedFiles) {
|
||||||
|
// Optimistic message already shown by sendContent() — just do the API call
|
||||||
// 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);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var fd = new FormData();
|
var fd = new FormData();
|
||||||
if (html && text) fd.append('content', html);
|
if (html && text) fd.append('content', html);
|
||||||
@ -1714,8 +1714,12 @@
|
|||||||
if (!data.messages || !data.messages.length) return;
|
if (!data.messages || !data.messages.length) return;
|
||||||
|
|
||||||
// Find messages newer than what we have
|
// Find messages newer than what we have
|
||||||
|
var currentUserId = window.__CURRENT_USER__ ? window.__CURRENT_USER__.id : null;
|
||||||
var newMsgs = data.messages.filter(function (msg) {
|
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) {
|
if (newMsgs.length > 0) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user