fix(ui): silent background state poll - no refetch churn when vault unchanged
This commit is contained in:
parent
5714349ea3
commit
3d1c30662a
3 changed files with 43 additions and 4 deletions
|
|
@ -107,7 +107,17 @@ export function AppProvider({ children }: { children: ReactNode }) {
|
|||
|
||||
const refresh = useCallback(async () => {
|
||||
const fresh = await api.getState();
|
||||
setState(fresh);
|
||||
// The 5s poll must be silent when nothing changed: a fresh object
|
||||
// identity every tick would re-render every screen and refire effects
|
||||
// keyed on state slices (e.g. Home's publications loader flickering
|
||||
// between "Loading…" and done forever).
|
||||
setState((prev) => {
|
||||
try {
|
||||
return JSON.stringify(prev) === JSON.stringify(fresh) ? prev : fresh;
|
||||
} catch {
|
||||
return fresh;
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -147,10 +157,10 @@ export function AppProvider({ children }: { children: ReactNode }) {
|
|||
// getState is a cheap local vault read; errors are ignored here since every
|
||||
// screen surfaces its own request failures.
|
||||
useEffect(() => {
|
||||
const timer = window.setInterval(() => {
|
||||
const timer = setInterval(() => {
|
||||
void refresh().catch(() => {});
|
||||
}, 5000);
|
||||
return () => window.clearInterval(timer);
|
||||
return () => clearInterval(timer);
|
||||
}, [refresh]);
|
||||
|
||||
const createProfile = useCallback(
|
||||
|
|
|
|||
|
|
@ -98,4 +98,31 @@ describe('HomeScreen', () => {
|
|||
const aliceRow = screen.getByText('Alice').closest('.home-profile-row') as HTMLElement;
|
||||
expect(within(aliceRow).getByRole('button', { name: 'Select' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not refetch publications on every background state poll', async () => {
|
||||
// Regression: the 5s AppProvider poll must not refire the publications
|
||||
// loader (it flickered Home between "Loading…" and done forever).
|
||||
// Fake timers from the start: the poll interval must be scheduled under
|
||||
// fake time, and RTL async queries stall under fake timers, so drive
|
||||
// everything with explicit timer advances instead.
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const backend = createFakeBackend();
|
||||
renderHome(backend);
|
||||
renderWithApp(
|
||||
<HomeScreen onNavigate={vi.fn()} onCreateProfile={vi.fn()} onImportProfile={vi.fn()} />,
|
||||
);
|
||||
|
||||
// Initial load completes.
|
||||
await vi.advanceTimersByTimeAsync(500);
|
||||
const initialFetches = backend.requests.filter((r) => r.method === 'feed_get').length;
|
||||
expect(initialFetches).toBeGreaterThan(0);
|
||||
|
||||
// Two full poll ticks with unchanged state must not refetch.
|
||||
await vi.advanceTimersByTimeAsync(12000);
|
||||
expect(backend.requests.filter((r) => r.method === 'feed_get').length).toBe(initialFetches);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -193,7 +193,9 @@ export function createFakeBackend(initial?: AppState): FakeBackend {
|
|||
switch (method) {
|
||||
case 'init':
|
||||
case 'get_state':
|
||||
return state;
|
||||
// Deep-copy like the real IPC boundary (fresh JSON per call), so
|
||||
// tests observe new object identities exactly as production does.
|
||||
return structuredClone(state);
|
||||
|
||||
// NIP-46 client handshake surface used by SignerModeScreen. The fake
|
||||
// keeps a Nip46SignerStatus-shaped object so handshake-state tests
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue