fix(core-state): guard refresh commits after unmount (#1992)

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
Helck
2026-05-17 15:18:10 -07:00
committed by GitHub
co-authored by Codex
parent f65ef6a431
commit e6f3e1f983
2 changed files with 44 additions and 1 deletions
+17
View File
@@ -190,7 +190,12 @@ export default function CoreStateProvider({ children }: { children: ReactNode })
const logoutGuardUntilRef = useRef(0);
const bootstrapFailCountRef = useRef(0);
const refreshInFlightRef = useRef<Promise<void> | 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;
}
@@ -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<Snapshot>(resolve => {
resolveSnapshot = resolve;
});
fetchSnapshot.mockReturnValue(pendingSnapshot);
listTeams.mockResolvedValue([]);
const { unmount } = render(
<CoreStateProvider>
<Consumer />
</CoreStateProvider>
);
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(() => {});