perf(renderer): preserve unchanged team list state

This commit is contained in:
777genius 2026-05-31 08:08:11 +03:00
parent 290f01e559
commit d10eeea269
2 changed files with 91 additions and 21 deletions

View file

@ -492,6 +492,45 @@ function isSelectedTeamLoadStillCurrent(
);
}
function buildTeamSummaryIndexes(teams: readonly TeamSummary[]): {
teamByName: Record<string, TeamSummary>;
teamBySessionId: Record<string, TeamSummary>;
} {
const teamByName: Record<string, TeamSummary> = {};
const teamBySessionId: Record<string, TeamSummary> = {};
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<string, TeamSummary>,
teams: readonly TeamSummary[]
): Record<string, TeamSummary> {
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<AppState, [], [], TeamSlice> = (set,
) {
return;
}
const teamByName: Record<string, TeamSummary> = {};
const teamBySessionId: Record<string, TeamSummary> = {};
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,

View file

@ -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<GlobalTaskLike[]>();