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: {
|
function schedulePostPaintTeamEnrichments(params: {
|
||||||
teamName: string;
|
teamName: string;
|
||||||
requestNonce: number;
|
requestNonce: number;
|
||||||
|
|
@ -1485,32 +1524,36 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
||||||
) {
|
) {
|
||||||
return;
|
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
|
// Atomic update: set teams AND clean up provisioning snapshots in one call
|
||||||
// to prevent any render cycle with duplicate cards.
|
// to prevent any render cycle with duplicate cards.
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const nextSnapshots = { ...state.provisioningSnapshotByTeam };
|
const nextTeams = structurallySharePlainValue(state.teams, teams);
|
||||||
for (const team of teams) {
|
const indexes = buildTeamSummaryIndexes(nextTeams);
|
||||||
delete nextSnapshots[team.teamName];
|
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 {
|
return {
|
||||||
teams,
|
teams: nextTeams,
|
||||||
teamByName,
|
teamByName: nextTeamByName,
|
||||||
teamBySessionId,
|
teamBySessionId: nextTeamBySessionId,
|
||||||
teamsLoading: false,
|
teamsLoading: false,
|
||||||
teamsError: null,
|
teamsError: null,
|
||||||
provisioningSnapshotByTeam: nextSnapshots,
|
provisioningSnapshotByTeam: nextSnapshots,
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,8 @@ interface TeamSummaryLike {
|
||||||
teamName: string;
|
teamName: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
projectPath: string;
|
projectPath: string;
|
||||||
|
leadSessionId?: string;
|
||||||
|
sessionHistory?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GlobalTaskLike {
|
interface GlobalTaskLike {
|
||||||
|
|
@ -251,6 +253,31 @@ describe('team slice context races', () => {
|
||||||
expect(store.getState().teamsLoading).toBe(false);
|
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 () => {
|
it('reruns a pending global task refresh for the current context instead of applying stale tasks', async () => {
|
||||||
const store = createSliceStore();
|
const store = createSliceStore();
|
||||||
const localTasks = deferred<GlobalTaskLike[]>();
|
const localTasks = deferred<GlobalTaskLike[]>();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue