diff --git a/app/src/providers/CoreStateProvider.tsx b/app/src/providers/CoreStateProvider.tsx index b386b72a7..09e678ddf 100644 --- a/app/src/providers/CoreStateProvider.tsx +++ b/app/src/providers/CoreStateProvider.tsx @@ -190,7 +190,12 @@ export default function CoreStateProvider({ children }: { children: ReactNode }) const logoutGuardUntilRef = useRef(0); const bootstrapFailCountRef = useRef(0); const refreshInFlightRef = useRef | null>(null); + const isMountedRef = useRef(true); const commitState = useCallback((updater: (previous: CoreState) => CoreState) => { + if (!isMountedRef.current) { + return; + } + setState(previous => { const next = updater(previous); setCoreStateSnapshot(next); @@ -198,9 +203,21 @@ export default function CoreStateProvider({ children }: { children: ReactNode }) }); }, []); + useEffect(() => { + isMountedRef.current = true; + return () => { + isMountedRef.current = false; + snapshotRequestIdRef.current += 1; + teamsRequestIdRef.current += 1; + }; + }, []); + const refreshCore = useCallback(async () => { const requestId = ++snapshotRequestIdRef.current; const snapshot = normalizeSnapshot(await fetchCoreAppSnapshot()); + if (!isMountedRef.current) { + return; + } if (!snapshot.sessionToken) { logoutGuardUntilRef.current = 0; } diff --git a/app/src/providers/__tests__/CoreStateProvider.test.tsx b/app/src/providers/__tests__/CoreStateProvider.test.tsx index 334de67f5..24e22f036 100644 --- a/app/src/providers/__tests__/CoreStateProvider.test.tsx +++ b/app/src/providers/__tests__/CoreStateProvider.test.tsx @@ -4,7 +4,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import * as coreStateApi from '../../services/coreStateApi'; import * as tauriCommands from '../../utils/tauriCommands'; -import { setCoreStateSnapshot } from '../../lib/coreState/store'; +import { getCoreStateSnapshot, setCoreStateSnapshot } from '../../lib/coreState/store'; import { setActiveUserId } from '../../store/userScopedStorage'; import CoreStateProvider, { coreStatePollFailureWarningMessage, @@ -221,6 +221,32 @@ describe('CoreStateProvider — identity-change cache clearing', () => { await waitFor(() => expect(screen.getByTestId('ready').textContent).toBe('ready')); }); + it('does not commit a pending bootstrap refresh after unmount', async () => { + let resolveSnapshot!: (snapshot: Snapshot) => void; + const pendingSnapshot = new Promise(resolve => { + resolveSnapshot = resolve; + }); + fetchSnapshot.mockReturnValue(pendingSnapshot); + listTeams.mockResolvedValue([]); + + const { unmount } = render( + + + + ); + + unmount(); + + await act(async () => { + resolveSnapshot(makeSnapshot({ userId: null, sessionToken: null })); + await pendingSnapshot; + await Promise.resolve(); + }); + + expect(getCoreStateSnapshot().isReady).toBe(false); + expect(getCoreStateSnapshot().snapshot.auth.userId).toBeNull(); + }); + it('warns when the initial core state poll fails', async () => { const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});