mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-09-11 09:06:58 +00:00
📉 perf: start message queries before file map loads (#14188)
* feat(client): remove chat message waterfalls * fix(client): guard sends during conversation detail loads * fix(client): observe new chat message cache * fix(client): preserve streaming messages during prefetch * fix(client): keep pending regeneration during prefetch * fix(client): refresh stale pending chat tails * fix(client): skip message prefetch during active streams * fix(client): clear observed message caches * fix(client): support mocked message cache clients * fix(client): preserve replacement pending tails during message prefetch * perf(client): start message queries before file map loads
This commit is contained in:
parent
329ed48246
commit
55451ee75d
12 changed files with 470 additions and 35 deletions
|
|
@ -1,14 +1,12 @@
|
|||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
import React from 'react';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { MemoryRouter, useNavigate } from 'react-router-dom';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
import type t from 'librechat-data-provider';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { Constants, EModelEndpoint } from 'librechat-data-provider';
|
||||
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import type t from 'librechat-data-provider';
|
||||
import AgentDetail from '../AgentDetail';
|
||||
|
||||
// Mock dependencies
|
||||
|
|
@ -148,6 +146,7 @@ describe('AgentDetail', () => {
|
|||
(useQueryClient as jest.Mock).mockReturnValue({
|
||||
getQueryData: jest.fn(),
|
||||
setQueryData: jest.fn(),
|
||||
removeQueries: jest.fn(),
|
||||
invalidateQueries: jest.fn(),
|
||||
});
|
||||
|
||||
|
|
@ -232,6 +231,7 @@ describe('AgentDetail', () => {
|
|||
const mockQueryClient = {
|
||||
getQueryData: jest.fn().mockReturnValue(null),
|
||||
setQueryData: jest.fn(),
|
||||
removeQueries: jest.fn(),
|
||||
invalidateQueries: jest.fn(),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ function ChatView({ index = 0, project }: { index?: number; project?: TChatProje
|
|||
},
|
||||
[fileMap],
|
||||
),
|
||||
enabled: !!fileMap,
|
||||
enabled: !!conversationId && conversationId !== Constants.SEARCH,
|
||||
},
|
||||
{ isStreaming: isSubmitting },
|
||||
);
|
||||
|
|
|
|||
|
|
@ -31,6 +31,24 @@ type MessageRenderProps = {
|
|||
'currentEditId' | 'setCurrentEditId' | 'siblingIdx' | 'setSiblingIdx' | 'siblingCount'
|
||||
>;
|
||||
|
||||
export function areMessageFilesEqual(
|
||||
prevFiles: TMessage['files'],
|
||||
nextFiles: TMessage['files'],
|
||||
): boolean {
|
||||
if (prevFiles === nextFiles) {
|
||||
return true;
|
||||
}
|
||||
const prevLength = prevFiles?.length ?? 0;
|
||||
const nextLength = nextFiles?.length ?? 0;
|
||||
if (prevLength !== nextLength) {
|
||||
return false;
|
||||
}
|
||||
if (prevLength === 0) {
|
||||
return true;
|
||||
}
|
||||
return prevFiles?.every((file, index) => file === nextFiles?.[index]) ?? true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom comparator for React.memo: compares `message` by key fields instead of reference
|
||||
* because `buildTree` creates new message objects on every streaming update for ALL messages,
|
||||
|
|
@ -82,7 +100,7 @@ function areMessageRenderPropsEqual(prev: MessageRenderProps, next: MessageRende
|
|||
prevMsg.endpoint === nextMsg.endpoint &&
|
||||
prevMsg.iconURL === nextMsg.iconURL &&
|
||||
prevMsg.feedback?.rating === nextMsg.feedback?.rating &&
|
||||
(prevMsg.files?.length ?? 0) === (nextMsg.files?.length ?? 0) &&
|
||||
areMessageFilesEqual(prevMsg.files, nextMsg.files) &&
|
||||
(prevMsg.quotes?.length ?? 0) === (nextMsg.quotes?.length ?? 0)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
import type { TFile } from 'librechat-data-provider';
|
||||
import { areMessageFilesEqual } from '../MessageRender';
|
||||
|
||||
const file = (overrides: Partial<TFile> = {}): TFile =>
|
||||
({
|
||||
file_id: 'file-1',
|
||||
filename: 'sample.pdf',
|
||||
filepath: '/uploads/sample.pdf',
|
||||
type: 'application/pdf',
|
||||
bytes: 100,
|
||||
embedded: false,
|
||||
object: 'file',
|
||||
usage: 1,
|
||||
user: 'user-1',
|
||||
...overrides,
|
||||
}) as TFile;
|
||||
|
||||
describe('areMessageFilesEqual', () => {
|
||||
it('detects when a raw message file is replaced by its hydrated file-map entry', () => {
|
||||
const rawFile = file({ filename: 'raw.pdf', preview: undefined });
|
||||
const hydratedFile = file({ filename: 'hydrated.pdf', preview: '/previews/sample.png' });
|
||||
|
||||
expect(areMessageFilesEqual([rawFile], [hydratedFile])).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps equivalent file entries memoized when buildTree creates a new array', () => {
|
||||
const hydratedFile = file({ preview: '/previews/sample.png' });
|
||||
|
||||
expect(areMessageFilesEqual([hydratedFile], [hydratedFile])).toBe(true);
|
||||
});
|
||||
|
||||
it('detects attachment additions and removals', () => {
|
||||
const hydratedFile = file();
|
||||
|
||||
expect(areMessageFilesEqual([], [hydratedFile])).toBe(false);
|
||||
expect(areMessageFilesEqual([hydratedFile], [])).toBe(false);
|
||||
});
|
||||
|
||||
it('treats absent and empty file lists as equivalent', () => {
|
||||
expect(areMessageFilesEqual(undefined, [])).toBe(true);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue