From 34a693121ce4ac61ac1a4d64368165cb5ec58ab9 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 22 May 2026 07:24:35 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=B5=20fix:=20Preserve=20Streaming=20Me?= =?UTF-8?q?ssages=20During=20Stale=20Refetch=20(#13247)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Messages/__tests__/queries.test.ts | 82 +++++++++++++++++++ client/src/data-provider/Messages/queries.ts | 64 +++++++++++---- 2 files changed, 131 insertions(+), 15 deletions(-) create mode 100644 client/src/data-provider/Messages/__tests__/queries.test.ts diff --git a/client/src/data-provider/Messages/__tests__/queries.test.ts b/client/src/data-provider/Messages/__tests__/queries.test.ts new file mode 100644 index 0000000000..c8fd0e99a1 --- /dev/null +++ b/client/src/data-provider/Messages/__tests__/queries.test.ts @@ -0,0 +1,82 @@ +import type { TMessage } from 'librechat-data-provider'; +import { getStableMessages } from '../queries'; + +const message = (overrides: Partial): TMessage => + ({ + messageId: 'message-id', + conversationId: 'convo-id', + parentMessageId: '00000000-0000-0000-0000-000000000000', + text: '', + sender: 'User', + isCreatedByUser: true, + createdAt: '2026-05-21T12:00:00.000Z', + updatedAt: '2026-05-21T12:00:00.000Z', + ...overrides, + }) as TMessage; + +describe('getStableMessages', () => { + it('keeps cache when an empty result races with unhydrated stream messages', () => { + const currentMessages = [ + message({ messageId: 'persisted-1' }), + message({ messageId: 'user-2', createdAt: undefined, updatedAt: undefined }), + message({ + messageId: 'user-2_', + isCreatedByUser: false, + createdAt: undefined, + updatedAt: undefined, + }), + ]; + + const result = getStableMessages({ + pathname: '/c/convo-id', + result: [], + currentMessages, + }); + + expect(result).toBe(currentMessages); + }); + + it('keeps cache when a one-message result returns for a larger cache', () => { + const currentMessages = [ + message({ messageId: 'persisted-1' }), + message({ messageId: 'persisted-2' }), + ]; + + const result = getStableMessages({ + pathname: '/c/convo-id', + result: [currentMessages[0]], + currentMessages, + }); + + expect(result).toBe(currentMessages); + }); + + it('accepts fewer persisted messages when the cache is fully hydrated', () => { + const currentMessages = [ + message({ messageId: 'persisted-1' }), + message({ messageId: 'persisted-2' }), + message({ messageId: 'persisted-3' }), + ]; + const serverMessages = [currentMessages[0], currentMessages[1]]; + + const result = getStableMessages({ + pathname: '/c/convo-id', + result: serverMessages, + currentMessages, + }); + + expect(result).toBe(serverMessages); + }); + + it('does not preserve cache on the new conversation route', () => { + const currentMessages = [message({ messageId: 'user-1', createdAt: undefined })]; + + const result = getStableMessages({ + pathname: '/c/new', + result: [], + currentMessages, + }); + + expect(result).toEqual([]); + }); +}); diff --git a/client/src/data-provider/Messages/queries.ts b/client/src/data-provider/Messages/queries.ts index bf0df06b4b..0799b4adac 100644 --- a/client/src/data-provider/Messages/queries.ts +++ b/client/src/data-provider/Messages/queries.ts @@ -5,6 +5,39 @@ import { QueryKeys, dataService } from 'librechat-data-provider'; import type * as t from 'librechat-data-provider'; import { logger } from '~/utils'; +type StableMessagesParams = { + pathname: string; + result: t.TMessage[]; + currentMessages?: t.TMessage[]; +}; + +function hasUnhydratedMessage(messages: t.TMessage[]) { + return messages.some((message) => { + const messageId = message.messageId ?? ''; + return message.createdAt == null || message.updatedAt == null || messageId.endsWith('_'); + }); +} + +export function getStableMessages({ + pathname, + result, + currentMessages, +}: StableMessagesParams): t.TMessage[] { + if (pathname.includes('/c/new') || !currentMessages?.length) { + return result; + } + + if (result.length >= currentMessages.length) { + return result; + } + + if (result.length === 1 || hasUnhydratedMessage(currentMessages)) { + return currentMessages; + } + + return result; +} + export const useGetMessagesByConvoId = ( id: string, config?: UseQueryOptions, @@ -15,22 +48,23 @@ export const useGetMessagesByConvoId = ( [QueryKeys.messages, id], async () => { const result = await dataService.getMessagesByConvoId(id); - if (!location.pathname.includes('/c/new') && result?.length === 1) { - const currentMessages = queryClient.getQueryData([QueryKeys.messages, id]); - if (currentMessages?.length === 1) { - return result; - } - if (currentMessages && currentMessages?.length > 1) { - logger.warn( - 'messages', - `Messages query for convo ${id} returned fewer than cache; path: "${location.pathname}"`, - result, - currentMessages, - ); - return currentMessages; - } + const currentMessages = queryClient.getQueryData([QueryKeys.messages, id]); + const stableMessages = getStableMessages({ + pathname: location.pathname, + result, + currentMessages, + }); + + if (stableMessages === currentMessages) { + logger.warn( + 'messages', + `Messages query for convo ${id} returned fewer than cache; path: "${location.pathname}"`, + result, + currentMessages, + ); } - return result; + + return stableMessages; }, { refetchOnWindowFocus: false,