perf(renderer): preserve unchanged team list state
This commit is contained in:
parent
290f01e559
commit
d10eeea269
2 changed files with 91 additions and 21 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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[]>();
|
||||
|
|
|
|||
Loading…
Reference in a new issue