- Fixed the actual race:
Sent messages wait for polling to catch up before rendering. Missing interleaved messages cannot appear above later ones. Messages sort by server timestamp, then ID. Timestamps are assigned atomically during SQLite insertion. Client updated to 20260609.5.
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
cursor: 0,
|
||||
poll: {
|
||||
generation: 0, active: false, controller: null, wakeTimer: null, wakeResolve: null,
|
||||
failures: 0, immediate: false, messages: new Map(), initial: true
|
||||
failures: 0, immediate: false, messages: new Map(), pendingThrough: 0, initial: true
|
||||
},
|
||||
loginChallenge: initialChallenge,
|
||||
verifyToken: new URLSearchParams(location.search).get('verify') || '',
|
||||
@@ -345,7 +345,7 @@
|
||||
const result = await api('messages.php', form({ action: 'send', message }))
|
||||
.catch(() => ({ error: 'Message send failed. Polling will continue.' }));
|
||||
if (result.error) { input.value = message; return toast(result.error); }
|
||||
mergeMessages([result.message], false);
|
||||
acknowledgeSentMessage(result.message);
|
||||
requestPoll();
|
||||
}
|
||||
function startPoll() {
|
||||
@@ -356,6 +356,7 @@
|
||||
state.poll.immediate = true;
|
||||
state.poll.initial = true;
|
||||
state.poll.messages.clear();
|
||||
state.poll.pendingThrough = 0;
|
||||
void pollWorker(state.poll.generation);
|
||||
}
|
||||
function requestPoll() {
|
||||
@@ -376,6 +377,7 @@
|
||||
if (wake) wake();
|
||||
state.poll.immediate = false;
|
||||
state.poll.messages.clear();
|
||||
state.poll.pendingThrough = 0;
|
||||
resetVoicePlaybackQueue();
|
||||
}
|
||||
async function pollWorker(generation) {
|
||||
@@ -439,6 +441,7 @@
|
||||
state.poll.initial = false;
|
||||
const nextCursor = Math.max(requestedSince, merged.maxId);
|
||||
state.cursor = Math.max(state.cursor, nextCursor);
|
||||
if (state.cursor >= state.poll.pendingThrough) state.poll.pendingThrough = 0;
|
||||
const recording = $('#recording');
|
||||
if (recording) {
|
||||
recording.hidden = !result.recording?.length;
|
||||
@@ -447,7 +450,12 @@
|
||||
+ (result.recording.length === 1 ? ' is' : ' are') + ' recording...'
|
||||
: '';
|
||||
}
|
||||
return { ok: true, hasMore: Boolean(result.has_more) && nextCursor > requestedSince };
|
||||
const madeProgress = nextCursor > requestedSince;
|
||||
const catchingUpToSend = state.poll.pendingThrough > nextCursor;
|
||||
return {
|
||||
ok: true,
|
||||
hasMore: madeProgress && (Boolean(result.has_more) || catchingUpToSend),
|
||||
};
|
||||
} catch (error) {
|
||||
if (state.poll.active && state.poll.generation === generation) {
|
||||
$('#cc-conn-dot').className = 'cc-conn-dot offline';
|
||||
@@ -463,6 +471,12 @@
|
||||
if (!Number.isSafeInteger(id) || id < 1) return null;
|
||||
return { ...message, id };
|
||||
}
|
||||
function acknowledgeSentMessage(message) {
|
||||
const acknowledged = normalizeMessage(message);
|
||||
if (!acknowledged) return;
|
||||
if (state.poll.messages.has(acknowledged.id) || acknowledged.id <= state.cursor) return;
|
||||
state.poll.pendingThrough = Math.max(state.poll.pendingThrough, acknowledged.id);
|
||||
}
|
||||
function isOwnMessage(message) {
|
||||
const userId = Number(message?.user_id);
|
||||
return Number.isSafeInteger(userId) && userId > 0
|
||||
@@ -476,6 +490,9 @@
|
||||
message.voice_duration, message.created_at
|
||||
]);
|
||||
}
|
||||
function compareMessagesChronologically(a, b) {
|
||||
return (Number(a.created_at) || 0) - (Number(b.created_at) || 0) || a.id - b.id;
|
||||
}
|
||||
function mergeMessages(messages, incoming) {
|
||||
const incomingIds = new Set();
|
||||
let maxId = state.cursor;
|
||||
@@ -495,7 +512,7 @@
|
||||
if (!list) return;
|
||||
trimMessageStore();
|
||||
list.querySelector('.cc-loading-msg, .cc-empty-day')?.remove();
|
||||
const sorted = [...state.poll.messages.values()].sort((a, b) => a.id - b.id);
|
||||
const sorted = [...state.poll.messages.values()].sort(compareMessagesChronologically);
|
||||
const nodes = new Map();
|
||||
Array.from(list.querySelectorAll('.cc-msg')).forEach(item => {
|
||||
const id = Number(item.dataset.id);
|
||||
@@ -558,8 +575,9 @@
|
||||
}
|
||||
function trimMessageStore() {
|
||||
const limit = Math.max(20, Number(state.config.chat?.max_rendered_messages) || 500);
|
||||
const ids = [...state.poll.messages.keys()].sort((a, b) => a - b);
|
||||
ids.slice(0, Math.max(0, ids.length - limit)).forEach(id => state.poll.messages.delete(id));
|
||||
const messages = [...state.poll.messages.values()].sort(compareMessagesChronologically);
|
||||
messages.slice(0, Math.max(0, messages.length - limit))
|
||||
.forEach(message => state.poll.messages.delete(message.id));
|
||||
}
|
||||
function enqueueVoiceAutoplay(item) {
|
||||
if (!feature('auto_voice_play') || !state.voice.autoPlay) return;
|
||||
@@ -690,7 +708,7 @@
|
||||
.catch(() => ({ error: 'Voice send failed. Polling will continue.' }));
|
||||
state.voice.sending = false;
|
||||
if (result.error) return toast(result.error);
|
||||
mergeMessages([result.message], false);
|
||||
acknowledgeSentMessage(result.message);
|
||||
discardVoice();
|
||||
requestPoll();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user