perf(chat): debounce persisted writes and re-render only the streaming message
- the Zustand persist middleware serialized every saved session and wrote to AsyncStorage on each streamed token (appendToMessage -> set); writes are now debounced (400 ms) and flushed when the app leaves the foreground - every token re-rendered every MessageBubble, re-parsing the Markdown of the whole conversation; rows are memoized so only the changed message renders (covered by MessageList.test.tsx)
This commit is contained in:
1 parent
200fb6afd7
commit
0e5b38fd15
6 files changed
+252
-13
No files matched your search
@@ -0,0 +1,72 @@
|
||||
import { createDebouncedStorage } from '../store/debouncedStorage';
|
||||
|
||||
const makeBase = () => ({
|
||||
getItem: jest.fn(async (_name: string): Promise<string | null> => 'from-disk'),
|
||||
setItem: jest.fn(async (_name: string, _value: string) => {}),
|
||||
removeItem: jest.fn(async (_name: string) => {}),
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
describe('createDebouncedStorage', () => {
|
||||
it('collapses a burst of writes into one trailing write with the latest value', () => {
|
||||
const base = makeBase();
|
||||
const storage = createDebouncedStorage(base, 400);
|
||||
|
||||
storage.setItem('k', '1');
|
||||
jest.advanceTimersByTime(300);
|
||||
storage.setItem('k', '2');
|
||||
jest.advanceTimersByTime(300);
|
||||
storage.setItem('k', '3');
|
||||
expect(base.setItem).not.toHaveBeenCalled();
|
||||
|
||||
jest.advanceTimersByTime(400);
|
||||
expect(base.setItem).toHaveBeenCalledTimes(1);
|
||||
expect(base.setItem).toHaveBeenCalledWith('k', '3');
|
||||
});
|
||||
|
||||
it('reads a pending value before it reaches the disk, and falls back to the base storage', async () => {
|
||||
const base = makeBase();
|
||||
const storage = createDebouncedStorage(base);
|
||||
|
||||
await expect(storage.getItem('k')).resolves.toBe('from-disk');
|
||||
storage.setItem('k', 'fresh');
|
||||
await expect(storage.getItem('k')).resolves.toBe('fresh');
|
||||
});
|
||||
|
||||
it('flush writes immediately and cancels the timer', async () => {
|
||||
const base = makeBase();
|
||||
const storage = createDebouncedStorage(base);
|
||||
|
||||
storage.setItem('a', '1');
|
||||
storage.setItem('b', '2');
|
||||
await storage.flush();
|
||||
expect(base.setItem.mock.calls).toEqual([['a', '1'], ['b', '2']]);
|
||||
|
||||
jest.advanceTimersByTime(1000);
|
||||
expect(base.setItem).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('flush with nothing pending does not write', async () => {
|
||||
const base = makeBase();
|
||||
await createDebouncedStorage(base).flush();
|
||||
expect(base.setItem).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('removeItem drops a pending write and removes from the base storage', async () => {
|
||||
const base = makeBase();
|
||||
const storage = createDebouncedStorage(base);
|
||||
|
||||
storage.setItem('k', '1');
|
||||
await storage.removeItem('k');
|
||||
jest.advanceTimersByTime(1000);
|
||||
|
||||
expect(base.setItem).not.toHaveBeenCalled();
|
||||
expect(base.removeItem).toHaveBeenCalledWith('k');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user