Files
LocalMind/store/debouncedStorage.ts
evandro 0e5b38fd15 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)
2026-10-10 04:28:38 -03:00

55 lines
1.8 KiB
TypeScript

import AsyncStorage from '@react-native-async-storage/async-storage';
import { AppState } from 'react-native';
import type { StateStorage } from 'zustand/middleware';
export interface DebouncedStorage extends StateStorage {
/** Writes every pending value right away. */
flush: () => Promise<void>;
}
/**
* Wraps a storage so bursts of writes collapse into one trailing write.
*
* The chat store is persisted on every state change and a streamed answer
* changes it once per token, which would serialize every saved session and hit
* AsyncStorage dozens of times per second.
*/
export function createDebouncedStorage(base: StateStorage, delayMs = 400): DebouncedStorage {
const pending = new Map<string, string>();
let timer: ReturnType<typeof setTimeout> | null = null;
const flush = async () => {
if (timer) {
clearTimeout(timer);
timer = null;
}
const entries = [...pending.entries()];
pending.clear();
await Promise.all(entries.map(([name, value]) => base.setItem(name, value)));
};
return {
getItem: (name) => {
const value = pending.get(name);
return value === undefined ? base.getItem(name) : Promise.resolve(value);
},
setItem: (name, value) => {
pending.set(name, value);
if (timer) clearTimeout(timer);
timer = setTimeout(flush, delayMs);
},
removeItem: (name) => {
pending.delete(name);
return base.removeItem(name);
},
flush,
};
}
export const appStorage = createDebouncedStorage(AsyncStorage);
// Do not lose the last tokens when the app is sent to the background or closed.
AppState.addEventListener('change', (state) => {
if (state !== 'active') void appStorage.flush();
});