From d10eeea2693349e4cdc4a46ebec24e70f8e22b15 Mon Sep 17 00:00:00 2001 From: 777genius Date: Sun, 31 May 2026 08:08:11 +0300 Subject: [PATCH] perf(renderer): preserve unchanged team list state --- src/renderer/store/slices/teamSlice.ts | 85 ++++++++++++++----- .../store/teamSliceContextRace.test.ts | 27 ++++++ 2 files changed, 91 insertions(+), 21 deletions(-) diff --git a/src/renderer/store/slices/teamSlice.ts b/src/renderer/store/slices/teamSlice.ts index 5f9e7b18..c1769627 100644 --- a/src/renderer/store/slices/teamSlice.ts +++ b/src/renderer/store/slices/teamSlice.ts @@ -492,6 +492,45 @@ function isSelectedTeamLoadStillCurrent( ); } +function buildTeamSummaryIndexes(teams: readonly TeamSummary[]): { + teamByName: Record; + teamBySessionId: Record; +} { + const teamByName: Record = {}; + const teamBySessionId: Record = {}; + for (const team of teams) { + teamByName[team.teamName] = team; + if (team.leadSessionId) { + teamBySessionId[team.leadSessionId] = team; + } + if (Array.isArray(team.sessionHistory)) { + for (const sid of team.sessionHistory) { + if (typeof sid === 'string' && sid) { + teamBySessionId[sid] = team; + } + } + } + } + return { teamByName, teamBySessionId }; +} + +function removeProvisioningSnapshotsForTeams( + snapshots: Record, + teams: readonly TeamSummary[] +): Record { + let nextSnapshots = snapshots; + for (const team of teams) { + if (!Object.prototype.hasOwnProperty.call(nextSnapshots, team.teamName)) { + continue; + } + if (nextSnapshots === snapshots) { + nextSnapshots = { ...snapshots }; + } + delete nextSnapshots[team.teamName]; + } + return nextSnapshots; +} + function schedulePostPaintTeamEnrichments(params: { teamName: string; requestNonce: number; @@ -1485,32 +1524,36 @@ export const createTeamSlice: StateCreator = (set, ) { return; } - const teamByName: Record = {}; - const teamBySessionId: Record = {}; - for (const team of teams) { - teamByName[team.teamName] = team; - if (team.leadSessionId) { - teamBySessionId[team.leadSessionId] = team; - } - if (Array.isArray(team.sessionHistory)) { - for (const sid of team.sessionHistory) { - if (typeof sid === 'string' && sid) { - teamBySessionId[sid] = team; - } - } - } - } // Atomic update: set teams AND clean up provisioning snapshots in one call // to prevent any render cycle with duplicate cards. set((state) => { - const nextSnapshots = { ...state.provisioningSnapshotByTeam }; - for (const team of teams) { - delete nextSnapshots[team.teamName]; + const nextTeams = structurallySharePlainValue(state.teams, teams); + const indexes = buildTeamSummaryIndexes(nextTeams); + const nextTeamByName = structurallySharePlainValue(state.teamByName, indexes.teamByName); + const nextTeamBySessionId = structurallySharePlainValue( + state.teamBySessionId, + indexes.teamBySessionId + ); + const nextSnapshots = removeProvisioningSnapshotsForTeams( + state.provisioningSnapshotByTeam, + nextTeams + ); + + if ( + nextTeams === state.teams && + nextTeamByName === state.teamByName && + nextTeamBySessionId === state.teamBySessionId && + nextSnapshots === state.provisioningSnapshotByTeam && + state.teamsLoading === false && + state.teamsError === null + ) { + return {}; } + return { - teams, - teamByName, - teamBySessionId, + teams: nextTeams, + teamByName: nextTeamByName, + teamBySessionId: nextTeamBySessionId, teamsLoading: false, teamsError: null, provisioningSnapshotByTeam: nextSnapshots, diff --git a/test/renderer/store/teamSliceContextRace.test.ts b/test/renderer/store/teamSliceContextRace.test.ts index bfd431b9..a4f62aac 100644 --- a/test/renderer/store/teamSliceContextRace.test.ts +++ b/test/renderer/store/teamSliceContextRace.test.ts @@ -35,6 +35,8 @@ interface TeamSummaryLike { teamName: string; displayName: string; projectPath: string; + leadSessionId?: string; + sessionHistory?: string[]; } interface GlobalTaskLike { @@ -251,6 +253,31 @@ describe('team slice context races', () => { expect(store.getState().teamsLoading).toBe(false); }); + it('preserves team list references when a refresh returns unchanged teams', async () => { + const store = createSliceStore(); + const team = { + teamName: 'atlas-hq-15', + displayName: 'Atlas HQ', + projectPath: '/repo', + leadSessionId: 'lead-session', + sessionHistory: ['previous-session'], + }; + apiMock.teams.list.mockResolvedValueOnce([team]).mockResolvedValueOnce([{ ...team }]); + + await store.getState().fetchTeams(); + const firstTeams = store.getState().teams; + const firstTeamByName = store.getState().teamByName; + const firstTeamBySessionId = store.getState().teamBySessionId; + + await store.getState().fetchTeams(); + + expect(store.getState().teams).toBe(firstTeams); + expect(store.getState().teamByName).toBe(firstTeamByName); + expect(store.getState().teamBySessionId).toBe(firstTeamBySessionId); + expect(store.getState().teamBySessionId['lead-session']).toBe(firstTeams[0]); + expect(store.getState().teamBySessionId['previous-session']).toBe(firstTeams[0]); + }); + it('reruns a pending global task refresh for the current context instead of applying stale tasks', async () => { const store = createSliceStore(); const localTasks = deferred();