mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-06-24 12:01:42 +02:00
Track message reconciliation and don't fire on stale returns
This commit is contained in:
@@ -137,6 +137,7 @@ export function useConversationMessages(
|
||||
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
const fetchingConversationIdRef = useRef<string | null>(null);
|
||||
const latestReconcileRequestIdRef = useRef(0);
|
||||
const messagesRef = useRef<Message[]>([]);
|
||||
const hasOlderMessagesRef = useRef(false);
|
||||
const hasNewerMessagesRef = useRef(false);
|
||||
@@ -215,7 +216,7 @@ export function useConversationMessages(
|
||||
);
|
||||
|
||||
const reconcileFromBackend = useCallback(
|
||||
(conversation: Conversation, signal: AbortSignal) => {
|
||||
(conversation: Conversation, signal: AbortSignal, requestId: number) => {
|
||||
const conversationId = conversation.id;
|
||||
api
|
||||
.getMessages(
|
||||
@@ -228,6 +229,7 @@ export function useConversationMessages(
|
||||
)
|
||||
.then((data) => {
|
||||
if (fetchingConversationIdRef.current !== conversationId) return;
|
||||
if (latestReconcileRequestIdRef.current !== requestId) return;
|
||||
|
||||
const dataWithPendingAck = data.map((msg) => applyPendingAck(msg));
|
||||
const merged = messageCache.reconcile(messagesRef.current, dataWithPendingAck);
|
||||
@@ -352,7 +354,9 @@ export function useConversationMessages(
|
||||
const triggerReconcile = useCallback(() => {
|
||||
if (!isMessageConversation(activeConversation)) return;
|
||||
const controller = new AbortController();
|
||||
reconcileFromBackend(activeConversation, controller.signal);
|
||||
const requestId = latestReconcileRequestIdRef.current + 1;
|
||||
latestReconcileRequestIdRef.current = requestId;
|
||||
reconcileFromBackend(activeConversation, controller.signal, requestId);
|
||||
}, [activeConversation, reconcileFromBackend]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -365,6 +369,7 @@ export function useConversationMessages(
|
||||
const conversationChanged = prevId !== newId;
|
||||
fetchingConversationIdRef.current = newId;
|
||||
prevConversationIdRef.current = newId;
|
||||
latestReconcileRequestIdRef.current = 0;
|
||||
|
||||
// Preserve around-loaded context on the same conversation when search clears targetMessageId.
|
||||
if (!conversationChanged && !targetMessageId) {
|
||||
@@ -433,7 +438,9 @@ export function useConversationMessages(
|
||||
seenMessageContent.current = new Set(cached.seenContent);
|
||||
setHasOlderMessages(cached.hasOlderMessages);
|
||||
setMessagesLoading(false);
|
||||
reconcileFromBackend(activeConversation, controller.signal);
|
||||
const requestId = latestReconcileRequestIdRef.current + 1;
|
||||
latestReconcileRequestIdRef.current = requestId;
|
||||
reconcileFromBackend(activeConversation, controller.signal, requestId);
|
||||
} else {
|
||||
void fetchLatestMessages(true, controller.signal);
|
||||
}
|
||||
|
||||
@@ -224,6 +224,48 @@ describe('useConversationMessages conversation switch', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('useConversationMessages background reconcile ordering', () => {
|
||||
beforeEach(() => {
|
||||
mockGetMessages.mockReset();
|
||||
messageCache.clear();
|
||||
});
|
||||
|
||||
it('ignores stale reconnect reconcile responses that finish after newer ones', async () => {
|
||||
const conv = createConversation();
|
||||
mockGetMessages.mockResolvedValueOnce([
|
||||
createMessage({ id: 42, text: 'initial snapshot', acked: 0 }),
|
||||
]);
|
||||
|
||||
const { result } = renderHook(() => useConversationMessages(conv));
|
||||
|
||||
await waitFor(() => expect(result.current.messagesLoading).toBe(false));
|
||||
expect(result.current.messages[0].text).toBe('initial snapshot');
|
||||
|
||||
const firstReconcile = createDeferred<Message[]>();
|
||||
const secondReconcile = createDeferred<Message[]>();
|
||||
mockGetMessages
|
||||
.mockReturnValueOnce(firstReconcile.promise)
|
||||
.mockReturnValueOnce(secondReconcile.promise);
|
||||
|
||||
act(() => {
|
||||
result.current.triggerReconcile();
|
||||
result.current.triggerReconcile();
|
||||
});
|
||||
|
||||
secondReconcile.resolve([createMessage({ id: 42, text: 'newer snapshot', acked: 2 })]);
|
||||
await waitFor(() => expect(result.current.messages[0].text).toBe('newer snapshot'));
|
||||
expect(result.current.messages[0].acked).toBe(2);
|
||||
|
||||
firstReconcile.resolve([createMessage({ id: 42, text: 'stale snapshot', acked: 1 })]);
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
expect(result.current.messages[0].text).toBe('newer snapshot');
|
||||
expect(result.current.messages[0].acked).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useConversationMessages forward pagination', () => {
|
||||
beforeEach(() => {
|
||||
mockGetMessages.mockReset();
|
||||
|
||||
Reference in New Issue
Block a user