refactor(team): extract scoped state cleanup

This commit is contained in:
777genius 2026-05-22 10:42:12 +03:00
parent 9bd8585617
commit c091bd8d96
3 changed files with 398 additions and 160 deletions

View file

@ -86,6 +86,11 @@ import {
hasTeamRefreshBurstDiagnostics,
noteTeamRefreshBurst,
} from '../team/teamRefreshBurstDiagnostics';
import {
buildTeamScopedProgressTombstones,
collectTeamScopedStateRemovals,
collectTeamScopedVisibleLoadingResets,
} from '../team/teamScopedStateCleanup';
import { noteTeamRefreshFanout } from '../teamRefreshFanoutDiagnostics';
import { getWorktreeNavigationState } from '../utils/stateResetHelpers';
@ -293,166 +298,6 @@ function clearTeamScopedTransientState(teamName: string): void {
clearTeamScopedSelectorCaches(teamName);
}
function collectTeamScopedVisibleLoadingResets(
state: Pick<
TeamSlice,
'teamMessagesByName' | 'selectedTeamName' | 'selectedTeamLoading' | 'selectedTeamError'
>,
teamName: string
): Partial<TeamSlice> {
const nextTeamMessagesEntry = state.teamMessagesByName[teamName];
const nextTeamMessagesByName =
nextTeamMessagesEntry &&
(nextTeamMessagesEntry.loadingHead || nextTeamMessagesEntry.loadingOlder)
? {
...state.teamMessagesByName,
[teamName]: {
...nextTeamMessagesEntry,
loadingHead: false,
loadingOlder: false,
},
}
: null;
const shouldResetSelectedSurface =
state.selectedTeamName === teamName &&
(state.selectedTeamLoading || state.selectedTeamError != null);
return {
...(nextTeamMessagesByName ? { teamMessagesByName: nextTeamMessagesByName } : {}),
...(shouldResetSelectedSurface
? {
selectedTeamLoading: false,
selectedTeamError: null,
}
: {}),
};
}
function omitTeamKey<T>(record: Record<string, T>, teamName: string): Record<string, T> | null {
if (!(teamName in record)) {
return null;
}
const next = { ...record };
delete next[teamName];
return next;
}
function collectTeamScopedStateRemovals(
state: Pick<
TeamSlice,
| 'provisioningRuns'
| 'teamDataCacheByName'
| 'teamAgentRuntimeByTeam'
| 'teamMessagesByName'
| 'memberActivityMetaByTeam'
| 'provisioningSnapshotByTeam'
| 'currentProvisioningRunIdByTeam'
| 'currentRuntimeRunIdByTeam'
| 'provisioningStartedAtFloorByTeam'
| 'leadActivityByTeam'
| 'leadContextByTeam'
| 'activeTaskLogActivityByTeam'
| 'activeToolsByTeam'
| 'finishedVisibleByTeam'
| 'toolHistoryByTeam'
| 'memberSpawnStatusesByTeam'
| 'memberSpawnSnapshotsByTeam'
| 'provisioningErrorByTeam'
>,
teamName: string
): Partial<TeamSlice> {
const nextProvisioningRuns = Object.fromEntries(
Object.entries(state.provisioningRuns).filter(([, run]) => run.teamName !== teamName)
) as Record<string, TeamProvisioningProgress>;
const nextTeamDataCache = omitTeamKey(state.teamDataCacheByName, teamName);
const nextTeamAgentRuntime = omitTeamKey(state.teamAgentRuntimeByTeam, teamName);
const nextTeamMessages = omitTeamKey(state.teamMessagesByName, teamName);
const nextMemberActivityMeta = omitTeamKey(state.memberActivityMetaByTeam, teamName);
const nextProvisioningSnapshot = omitTeamKey(state.provisioningSnapshotByTeam, teamName);
const nextCurrentProvisioningRunId = omitTeamKey(state.currentProvisioningRunIdByTeam, teamName);
const nextCurrentRuntimeRunId = omitTeamKey(state.currentRuntimeRunIdByTeam, teamName);
const nextProvisioningStartedAtFloor = omitTeamKey(
state.provisioningStartedAtFloorByTeam,
teamName
);
const nextLeadActivity = omitTeamKey(state.leadActivityByTeam, teamName);
const nextLeadContext = omitTeamKey(state.leadContextByTeam, teamName);
const nextActiveTaskLogActivity = omitTeamKey(state.activeTaskLogActivityByTeam, teamName);
const nextActiveTools = omitTeamKey(state.activeToolsByTeam, teamName);
const nextFinishedVisible = omitTeamKey(state.finishedVisibleByTeam, teamName);
const nextToolHistory = omitTeamKey(state.toolHistoryByTeam, teamName);
const nextMemberSpawnStatuses = omitTeamKey(state.memberSpawnStatusesByTeam, teamName);
const nextMemberSpawnSnapshots = omitTeamKey(state.memberSpawnSnapshotsByTeam, teamName);
const nextProvisioningErrors = omitTeamKey(state.provisioningErrorByTeam, teamName);
return {
...(Object.keys(nextProvisioningRuns).length !== Object.keys(state.provisioningRuns).length
? { provisioningRuns: nextProvisioningRuns }
: {}),
...(nextTeamDataCache ? { teamDataCacheByName: nextTeamDataCache } : {}),
...(nextTeamAgentRuntime ? { teamAgentRuntimeByTeam: nextTeamAgentRuntime } : {}),
...(nextTeamMessages ? { teamMessagesByName: nextTeamMessages } : {}),
...(nextMemberActivityMeta ? { memberActivityMetaByTeam: nextMemberActivityMeta } : {}),
...(nextProvisioningSnapshot ? { provisioningSnapshotByTeam: nextProvisioningSnapshot } : {}),
...(nextCurrentProvisioningRunId
? { currentProvisioningRunIdByTeam: nextCurrentProvisioningRunId }
: {}),
...(nextCurrentRuntimeRunId ? { currentRuntimeRunIdByTeam: nextCurrentRuntimeRunId } : {}),
...(nextProvisioningStartedAtFloor
? { provisioningStartedAtFloorByTeam: nextProvisioningStartedAtFloor }
: {}),
...(nextLeadActivity ? { leadActivityByTeam: nextLeadActivity } : {}),
...(nextLeadContext ? { leadContextByTeam: nextLeadContext } : {}),
...(nextActiveTaskLogActivity
? { activeTaskLogActivityByTeam: nextActiveTaskLogActivity }
: {}),
...(nextActiveTools ? { activeToolsByTeam: nextActiveTools } : {}),
...(nextFinishedVisible ? { finishedVisibleByTeam: nextFinishedVisible } : {}),
...(nextToolHistory ? { toolHistoryByTeam: nextToolHistory } : {}),
...(nextMemberSpawnStatuses ? { memberSpawnStatusesByTeam: nextMemberSpawnStatuses } : {}),
...(nextMemberSpawnSnapshots ? { memberSpawnSnapshotsByTeam: nextMemberSpawnSnapshots } : {}),
...(nextProvisioningErrors ? { provisioningErrorByTeam: nextProvisioningErrors } : {}),
};
}
function buildTeamScopedProgressTombstones(
state: Pick<
TeamSlice,
| 'currentProvisioningRunIdByTeam'
| 'currentRuntimeRunIdByTeam'
| 'ignoredProvisioningRunIds'
| 'ignoredRuntimeRunIds'
| 'provisioningStartedAtFloorByTeam'
>,
teamName: string,
floor: string
): Pick<
TeamSlice,
'ignoredProvisioningRunIds' | 'ignoredRuntimeRunIds' | 'provisioningStartedAtFloorByTeam'
> {
const nextIgnoredProvisioningRunIds = { ...state.ignoredProvisioningRunIds };
const nextIgnoredRuntimeRunIds = { ...state.ignoredRuntimeRunIds };
const currentProvisioningRunId = state.currentProvisioningRunIdByTeam[teamName];
const currentRuntimeRunId = state.currentRuntimeRunIdByTeam[teamName];
if (currentProvisioningRunId) {
nextIgnoredProvisioningRunIds[currentProvisioningRunId] = teamName;
}
if (currentRuntimeRunId) {
nextIgnoredRuntimeRunIds[currentRuntimeRunId] = teamName;
}
return {
ignoredProvisioningRunIds: nextIgnoredProvisioningRunIds,
ignoredRuntimeRunIds: nextIgnoredRuntimeRunIds,
provisioningStartedAtFloorByTeam: {
...state.provisioningStartedAtFloorByTeam,
[teamName]: floor,
},
};
}
function beginInFlightTeamDataRefresh(teamName: string): symbol {
const token = Symbol(teamName);
const existing = inFlightRefreshTeamDataCalls.get(teamName);

View file

@ -0,0 +1,190 @@
interface TeamMessagesLoadingEntry {
loadingHead: boolean;
loadingOlder: boolean;
}
interface TeamScopedVisibleLoadingResetState<
TTeamMessagesEntry extends TeamMessagesLoadingEntry,
> {
teamMessagesByName: Record<string, TTeamMessagesEntry>;
selectedTeamName: string | null;
selectedTeamLoading: boolean;
selectedTeamError: string | null;
}
interface TeamScopedProvisioningRun {
teamName: string;
}
type TeamScopedRecord = Record<string, unknown>;
interface TeamScopedStateRemovalState<
TProvisioningRun extends TeamScopedProvisioningRun = TeamScopedProvisioningRun,
> {
provisioningRuns: Record<string, TProvisioningRun>;
teamDataCacheByName: TeamScopedRecord;
teamAgentRuntimeByTeam: TeamScopedRecord;
teamMessagesByName: TeamScopedRecord;
memberActivityMetaByTeam: TeamScopedRecord;
provisioningSnapshotByTeam: TeamScopedRecord;
currentProvisioningRunIdByTeam: TeamScopedRecord;
currentRuntimeRunIdByTeam: TeamScopedRecord;
provisioningStartedAtFloorByTeam: TeamScopedRecord;
leadActivityByTeam: TeamScopedRecord;
leadContextByTeam: TeamScopedRecord;
activeTaskLogActivityByTeam: TeamScopedRecord;
activeToolsByTeam: TeamScopedRecord;
finishedVisibleByTeam: TeamScopedRecord;
toolHistoryByTeam: TeamScopedRecord;
memberSpawnStatusesByTeam: TeamScopedRecord;
memberSpawnSnapshotsByTeam: TeamScopedRecord;
provisioningErrorByTeam: TeamScopedRecord;
}
type TeamScopedStateRemovalKey = keyof TeamScopedStateRemovalState;
interface TeamScopedProgressTombstoneState {
currentProvisioningRunIdByTeam: Record<string, string | null | undefined>;
currentRuntimeRunIdByTeam: Record<string, string | null | undefined>;
ignoredProvisioningRunIds: Record<string, string>;
ignoredRuntimeRunIds: Record<string, string>;
provisioningStartedAtFloorByTeam: Record<string, string>;
}
export function collectTeamScopedVisibleLoadingResets<
TTeamMessagesEntry extends TeamMessagesLoadingEntry,
>(
state: TeamScopedVisibleLoadingResetState<TTeamMessagesEntry>,
teamName: string
): Partial<TeamScopedVisibleLoadingResetState<TTeamMessagesEntry>> {
const nextTeamMessagesEntry = state.teamMessagesByName[teamName];
const nextTeamMessagesByName =
nextTeamMessagesEntry &&
(nextTeamMessagesEntry.loadingHead || nextTeamMessagesEntry.loadingOlder)
? {
...state.teamMessagesByName,
[teamName]: {
...nextTeamMessagesEntry,
loadingHead: false,
loadingOlder: false,
} as TTeamMessagesEntry,
}
: null;
const shouldResetSelectedSurface =
state.selectedTeamName === teamName &&
(state.selectedTeamLoading || state.selectedTeamError != null);
return {
...(nextTeamMessagesByName ? { teamMessagesByName: nextTeamMessagesByName } : {}),
...(shouldResetSelectedSurface
? {
selectedTeamLoading: false,
selectedTeamError: null,
}
: {}),
};
}
function omitTeamKey<TRecord extends Record<string, unknown>>(
record: TRecord,
teamName: string
): TRecord | null {
if (!(teamName in record)) {
return null;
}
const next = { ...record };
delete next[teamName];
return next;
}
export function collectTeamScopedStateRemovals<TState extends TeamScopedStateRemovalState>(
state: TState,
teamName: string
): Partial<Pick<TState, TeamScopedStateRemovalKey>> {
const nextProvisioningRuns = Object.fromEntries(
Object.entries(state.provisioningRuns).filter(([, run]) => run.teamName !== teamName)
) as TState['provisioningRuns'];
const nextTeamDataCache = omitTeamKey(state.teamDataCacheByName, teamName);
const nextTeamAgentRuntime = omitTeamKey(state.teamAgentRuntimeByTeam, teamName);
const nextTeamMessages = omitTeamKey(state.teamMessagesByName, teamName);
const nextMemberActivityMeta = omitTeamKey(state.memberActivityMetaByTeam, teamName);
const nextProvisioningSnapshot = omitTeamKey(state.provisioningSnapshotByTeam, teamName);
const nextCurrentProvisioningRunId = omitTeamKey(state.currentProvisioningRunIdByTeam, teamName);
const nextCurrentRuntimeRunId = omitTeamKey(state.currentRuntimeRunIdByTeam, teamName);
const nextProvisioningStartedAtFloor = omitTeamKey(
state.provisioningStartedAtFloorByTeam,
teamName
);
const nextLeadActivity = omitTeamKey(state.leadActivityByTeam, teamName);
const nextLeadContext = omitTeamKey(state.leadContextByTeam, teamName);
const nextActiveTaskLogActivity = omitTeamKey(state.activeTaskLogActivityByTeam, teamName);
const nextActiveTools = omitTeamKey(state.activeToolsByTeam, teamName);
const nextFinishedVisible = omitTeamKey(state.finishedVisibleByTeam, teamName);
const nextToolHistory = omitTeamKey(state.toolHistoryByTeam, teamName);
const nextMemberSpawnStatuses = omitTeamKey(state.memberSpawnStatusesByTeam, teamName);
const nextMemberSpawnSnapshots = omitTeamKey(state.memberSpawnSnapshotsByTeam, teamName);
const nextProvisioningErrors = omitTeamKey(state.provisioningErrorByTeam, teamName);
return {
...(Object.keys(nextProvisioningRuns).length !== Object.keys(state.provisioningRuns).length
? { provisioningRuns: nextProvisioningRuns }
: {}),
...(nextTeamDataCache ? { teamDataCacheByName: nextTeamDataCache } : {}),
...(nextTeamAgentRuntime ? { teamAgentRuntimeByTeam: nextTeamAgentRuntime } : {}),
...(nextTeamMessages ? { teamMessagesByName: nextTeamMessages } : {}),
...(nextMemberActivityMeta ? { memberActivityMetaByTeam: nextMemberActivityMeta } : {}),
...(nextProvisioningSnapshot ? { provisioningSnapshotByTeam: nextProvisioningSnapshot } : {}),
...(nextCurrentProvisioningRunId
? { currentProvisioningRunIdByTeam: nextCurrentProvisioningRunId }
: {}),
...(nextCurrentRuntimeRunId ? { currentRuntimeRunIdByTeam: nextCurrentRuntimeRunId } : {}),
...(nextProvisioningStartedAtFloor
? { provisioningStartedAtFloorByTeam: nextProvisioningStartedAtFloor }
: {}),
...(nextLeadActivity ? { leadActivityByTeam: nextLeadActivity } : {}),
...(nextLeadContext ? { leadContextByTeam: nextLeadContext } : {}),
...(nextActiveTaskLogActivity
? { activeTaskLogActivityByTeam: nextActiveTaskLogActivity }
: {}),
...(nextActiveTools ? { activeToolsByTeam: nextActiveTools } : {}),
...(nextFinishedVisible ? { finishedVisibleByTeam: nextFinishedVisible } : {}),
...(nextToolHistory ? { toolHistoryByTeam: nextToolHistory } : {}),
...(nextMemberSpawnStatuses ? { memberSpawnStatusesByTeam: nextMemberSpawnStatuses } : {}),
...(nextMemberSpawnSnapshots ? { memberSpawnSnapshotsByTeam: nextMemberSpawnSnapshots } : {}),
...(nextProvisioningErrors ? { provisioningErrorByTeam: nextProvisioningErrors } : {}),
};
}
export function buildTeamScopedProgressTombstones<TState extends TeamScopedProgressTombstoneState>(
state: TState,
teamName: string,
floor: string
): Pick<
TState,
'ignoredProvisioningRunIds' | 'ignoredRuntimeRunIds' | 'provisioningStartedAtFloorByTeam'
> {
const nextIgnoredProvisioningRunIds = { ...state.ignoredProvisioningRunIds };
const nextIgnoredRuntimeRunIds = { ...state.ignoredRuntimeRunIds };
const currentProvisioningRunId = state.currentProvisioningRunIdByTeam[teamName];
const currentRuntimeRunId = state.currentRuntimeRunIdByTeam[teamName];
if (currentProvisioningRunId) {
nextIgnoredProvisioningRunIds[currentProvisioningRunId] = teamName;
}
if (currentRuntimeRunId) {
nextIgnoredRuntimeRunIds[currentRuntimeRunId] = teamName;
}
return {
ignoredProvisioningRunIds: nextIgnoredProvisioningRunIds,
ignoredRuntimeRunIds: nextIgnoredRuntimeRunIds,
provisioningStartedAtFloorByTeam: {
...state.provisioningStartedAtFloorByTeam,
[teamName]: floor,
},
} as Pick<
TState,
'ignoredProvisioningRunIds' | 'ignoredRuntimeRunIds' | 'provisioningStartedAtFloorByTeam'
>;
}

View file

@ -0,0 +1,203 @@
import { describe, expect, it } from 'vitest';
import {
buildTeamScopedProgressTombstones,
collectTeamScopedStateRemovals,
collectTeamScopedVisibleLoadingResets,
} from '../../../src/renderer/store/team/teamScopedStateCleanup';
const teamScopedRecordKeys = [
'teamDataCacheByName',
'teamAgentRuntimeByTeam',
'teamMessagesByName',
'memberActivityMetaByTeam',
'provisioningSnapshotByTeam',
'currentProvisioningRunIdByTeam',
'currentRuntimeRunIdByTeam',
'provisioningStartedAtFloorByTeam',
'leadActivityByTeam',
'leadContextByTeam',
'activeTaskLogActivityByTeam',
'activeToolsByTeam',
'finishedVisibleByTeam',
'toolHistoryByTeam',
'memberSpawnStatusesByTeam',
'memberSpawnSnapshotsByTeam',
'provisioningErrorByTeam',
] as const;
function buildRecord(label: string): Record<string, unknown> {
return {
'my-team': `${label}:mine`,
'other-team': `${label}:other`,
};
}
function buildRemovalState(): Parameters<typeof collectTeamScopedStateRemovals>[0] {
return {
provisioningRuns: {
'run-mine-1': { teamName: 'my-team' },
'run-other': { teamName: 'other-team' },
'run-mine-2': { teamName: 'my-team' },
},
teamDataCacheByName: buildRecord('teamDataCacheByName'),
teamAgentRuntimeByTeam: buildRecord('teamAgentRuntimeByTeam'),
teamMessagesByName: buildRecord('teamMessagesByName'),
memberActivityMetaByTeam: buildRecord('memberActivityMetaByTeam'),
provisioningSnapshotByTeam: buildRecord('provisioningSnapshotByTeam'),
currentProvisioningRunIdByTeam: buildRecord('currentProvisioningRunIdByTeam'),
currentRuntimeRunIdByTeam: buildRecord('currentRuntimeRunIdByTeam'),
provisioningStartedAtFloorByTeam: buildRecord('provisioningStartedAtFloorByTeam'),
leadActivityByTeam: buildRecord('leadActivityByTeam'),
leadContextByTeam: buildRecord('leadContextByTeam'),
activeTaskLogActivityByTeam: buildRecord('activeTaskLogActivityByTeam'),
activeToolsByTeam: buildRecord('activeToolsByTeam'),
finishedVisibleByTeam: buildRecord('finishedVisibleByTeam'),
toolHistoryByTeam: buildRecord('toolHistoryByTeam'),
memberSpawnStatusesByTeam: buildRecord('memberSpawnStatusesByTeam'),
memberSpawnSnapshotsByTeam: buildRecord('memberSpawnSnapshotsByTeam'),
provisioningErrorByTeam: buildRecord('provisioningErrorByTeam'),
};
}
describe('teamScopedStateCleanup', () => {
it('resets visible team loading and message loading flags for the scoped team', () => {
const otherEntry = {
loadingHead: true,
loadingOlder: false,
marker: 'other',
};
const patch = collectTeamScopedVisibleLoadingResets(
{
teamMessagesByName: {
'my-team': {
loadingHead: true,
loadingOlder: true,
marker: 'mine',
},
'other-team': otherEntry,
},
selectedTeamName: 'my-team',
selectedTeamLoading: true,
selectedTeamError: 'Boom',
},
'my-team'
);
expect(patch).toEqual({
teamMessagesByName: {
'my-team': {
loadingHead: false,
loadingOlder: false,
marker: 'mine',
},
'other-team': otherEntry,
},
selectedTeamLoading: false,
selectedTeamError: null,
});
});
it('does not emit visible loading changes when the scoped team is already idle', () => {
const patch = collectTeamScopedVisibleLoadingResets(
{
teamMessagesByName: {
'my-team': {
loadingHead: false,
loadingOlder: false,
},
},
selectedTeamName: 'other-team',
selectedTeamLoading: false,
selectedTeamError: null,
},
'my-team'
);
expect(patch).toEqual({});
});
it('removes scoped team records and provisioning runs while preserving other teams', () => {
const patch = collectTeamScopedStateRemovals(buildRemovalState(), 'my-team');
expect(patch.provisioningRuns).toEqual({
'run-other': { teamName: 'other-team' },
});
for (const key of teamScopedRecordKeys) {
expect(patch[key]).toEqual({
'other-team': `${key}:other`,
});
}
});
it('does not emit removal changes when the team is absent', () => {
const state = buildRemovalState();
const patch = collectTeamScopedStateRemovals(state, 'missing-team');
expect(patch).toEqual({});
});
it('tombstones current provisioning and runtime run ids for the scoped team', () => {
const tombstones = buildTeamScopedProgressTombstones(
{
currentProvisioningRunIdByTeam: {
'my-team': 'provisioning-run-1',
'other-team': 'provisioning-run-2',
},
currentRuntimeRunIdByTeam: {
'my-team': 'runtime-run-1',
},
ignoredProvisioningRunIds: {
old: 'old-team',
},
ignoredRuntimeRunIds: {
'old-runtime': 'old-team',
},
provisioningStartedAtFloorByTeam: {
'other-team': '2026-01-01T00:00:00.000Z',
},
},
'my-team',
'2026-05-22T10:00:00.000Z'
);
expect(tombstones).toEqual({
ignoredProvisioningRunIds: {
old: 'old-team',
'provisioning-run-1': 'my-team',
},
ignoredRuntimeRunIds: {
'old-runtime': 'old-team',
'runtime-run-1': 'my-team',
},
provisioningStartedAtFloorByTeam: {
'other-team': '2026-01-01T00:00:00.000Z',
'my-team': '2026-05-22T10:00:00.000Z',
},
});
});
it('still records a floor when there are no current run ids to tombstone', () => {
const tombstones = buildTeamScopedProgressTombstones(
{
currentProvisioningRunIdByTeam: {},
currentRuntimeRunIdByTeam: {
'my-team': null,
},
ignoredProvisioningRunIds: {},
ignoredRuntimeRunIds: {},
provisioningStartedAtFloorByTeam: {},
},
'my-team',
'2026-05-22T10:00:00.000Z'
);
expect(tombstones).toEqual({
ignoredProvisioningRunIds: {},
ignoredRuntimeRunIds: {},
provisioningStartedAtFloorByTeam: {
'my-team': '2026-05-22T10:00:00.000Z',
},
});
});
});