From 9bd85856177c9740c8ad94a469297a4c1832d989 Mon Sep 17 00:00:00 2001 From: 777genius Date: Fri, 22 May 2026 10:27:22 +0300 Subject: [PATCH] refactor(team): extract refresh burst diagnostics --- src/renderer/store/slices/teamSlice.ts | 37 +++------ .../store/team/teamRefreshBurstDiagnostics.ts | 48 +++++++++++ .../teamRefreshBurstDiagnosticsStore.test.ts | 83 +++++++++++++++++++ 3 files changed, 141 insertions(+), 27 deletions(-) create mode 100644 src/renderer/store/team/teamRefreshBurstDiagnostics.ts create mode 100644 test/renderer/store/teamRefreshBurstDiagnosticsStore.test.ts diff --git a/src/renderer/store/slices/teamSlice.ts b/src/renderer/store/slices/teamSlice.ts index bc24198f..b2d8f654 100644 --- a/src/renderer/store/slices/teamSlice.ts +++ b/src/renderer/store/slices/teamSlice.ts @@ -80,6 +80,12 @@ import { clearPendingReplyRefreshWaits, setPendingReplyRefreshEnabled, } from '../team/teamPendingReplyWaits'; +import { + clearAllTeamRefreshBurstDiagnostics, + clearTeamRefreshBurstDiagnostics, + hasTeamRefreshBurstDiagnostics, + noteTeamRefreshBurst, +} from '../team/teamRefreshBurstDiagnostics'; import { noteTeamRefreshFanout } from '../teamRefreshFanoutDiagnostics'; import { getWorktreeNavigationState } from '../utils/stateResetHelpers'; @@ -186,10 +192,6 @@ const pendingFreshTeamMemberActivityMetaRefreshes = new Set(); const pendingTeamPendingReplyRefreshTimers = new Map>(); let inFlightGlobalTasksRefresh: Promise | null = null; let pendingFreshGlobalTasksRefresh = false; -const teamRefreshBurstDiagnostics = new Map< - string, - { windowStartedAt: number; count: number; lastWarnAt: number } ->(); interface RefreshTeamDataOptions { withDedup?: boolean; } @@ -253,7 +255,7 @@ export function __resetTeamSliceModuleStateForTests(): void { clearAllLastResolvedTeamDataRefreshes(); clearAllTeamLocalStateEpochs(); clearAllMemberSpawnStatusesIpcBackoffs(); - teamRefreshBurstDiagnostics.clear(); + clearAllTeamRefreshBurstDiagnostics(); clearAllMemberSpawnUiEqualLastWarns(); resolvedMembersSelectorCache.clear(); resolvedMemberSelectorCache.clear(); @@ -286,7 +288,7 @@ function clearTeamScopedTransientState(teamName: string): void { pendingFreshTeamMemberActivityMetaRefreshes.delete(teamName); clearLastResolvedTeamDataRefreshAt(teamName); clearMemberSpawnStatusesIpcBackoff(teamName); - teamRefreshBurstDiagnostics.delete(teamName); + clearTeamRefreshBurstDiagnostics(teamName); clearMemberSpawnUiEqualLastWarn(teamName); clearTeamScopedSelectorCaches(teamName); } @@ -672,7 +674,7 @@ export function __getTeamScopedTransientStateForTests(teamName: string): { hasLastResolvedTeamDataRefresh: hasLastResolvedTeamDataRefreshAt(teamName), hasCurrentLocalStateEpoch: hasTeamLocalStateEpoch(teamName), hasMemberSpawnStatusesIpcBackoff: hasMemberSpawnStatusesIpcBackoff(teamName), - hasTeamRefreshBurstDiagnostics: teamRefreshBurstDiagnostics.has(teamName), + hasTeamRefreshBurstDiagnostics: hasTeamRefreshBurstDiagnostics(teamName), hasMemberSpawnUiEqualLastWarn: hasMemberSpawnUiEqualLastWarn(teamName), }; } @@ -838,25 +840,6 @@ function fetchTeamDataFresh( ); } -function noteTeamRefreshBurst(teamName: string): number { - const now = Date.now(); - const diagnostic = teamRefreshBurstDiagnostics.get(teamName) ?? { - windowStartedAt: now, - count: 0, - lastWarnAt: 0, - }; - - if (now - diagnostic.windowStartedAt > TEAM_REFRESH_BURST_WINDOW_MS) { - diagnostic.windowStartedAt = now; - diagnostic.count = 0; - } - - diagnostic.count += 1; - - teamRefreshBurstDiagnostics.set(teamName, diagnostic); - return diagnostic.count; -} - function areLaunchSummaryCountsEqual( left: PersistedTeamLaunchSummary | undefined, right: PersistedTeamLaunchSummary | undefined @@ -4203,7 +4186,7 @@ export const createTeamSlice: StateCreator = (set, const refreshToken = beginInFlightTeamDataRefresh(teamName); // Silent refresh — update data without showing loading skeleton. // Only selectTeam() sets loading: true (for initial load). - noteTeamRefreshBurst(teamName); + noteTeamRefreshBurst(teamName, TEAM_REFRESH_BURST_WINDOW_MS); if (reusedInFlightRequest) { pendingFreshTeamDataRefreshes.add(teamName); } diff --git a/src/renderer/store/team/teamRefreshBurstDiagnostics.ts b/src/renderer/store/team/teamRefreshBurstDiagnostics.ts new file mode 100644 index 00000000..27205052 --- /dev/null +++ b/src/renderer/store/team/teamRefreshBurstDiagnostics.ts @@ -0,0 +1,48 @@ +interface TeamRefreshBurstDiagnostic { + windowStartedAt: number; + count: number; + lastWarnAt: number; +} + +const teamRefreshBurstDiagnostics = new Map(); + +export function hasTeamRefreshBurstDiagnostics(teamName: string): boolean { + return teamRefreshBurstDiagnostics.has(teamName); +} + +export function getTeamRefreshBurstDiagnosticForTests( + teamName: string +): TeamRefreshBurstDiagnostic | undefined { + const diagnostic = teamRefreshBurstDiagnostics.get(teamName); + return diagnostic ? { ...diagnostic } : undefined; +} + +export function noteTeamRefreshBurst( + teamName: string, + windowMs: number, + now = Date.now() +): number { + const diagnostic = teamRefreshBurstDiagnostics.get(teamName) ?? { + windowStartedAt: now, + count: 0, + lastWarnAt: 0, + }; + + if (now - diagnostic.windowStartedAt > windowMs) { + diagnostic.windowStartedAt = now; + diagnostic.count = 0; + } + + diagnostic.count += 1; + + teamRefreshBurstDiagnostics.set(teamName, diagnostic); + return diagnostic.count; +} + +export function clearTeamRefreshBurstDiagnostics(teamName: string): void { + teamRefreshBurstDiagnostics.delete(teamName); +} + +export function clearAllTeamRefreshBurstDiagnostics(): void { + teamRefreshBurstDiagnostics.clear(); +} diff --git a/test/renderer/store/teamRefreshBurstDiagnosticsStore.test.ts b/test/renderer/store/teamRefreshBurstDiagnosticsStore.test.ts new file mode 100644 index 00000000..5cc3f529 --- /dev/null +++ b/test/renderer/store/teamRefreshBurstDiagnosticsStore.test.ts @@ -0,0 +1,83 @@ +import { afterEach, describe, expect, it } from 'vitest'; + +import { + clearAllTeamRefreshBurstDiagnostics, + clearTeamRefreshBurstDiagnostics, + getTeamRefreshBurstDiagnosticForTests, + hasTeamRefreshBurstDiagnostics, + noteTeamRefreshBurst, +} from '../../../src/renderer/store/team/teamRefreshBurstDiagnostics'; + +afterEach(() => { + clearAllTeamRefreshBurstDiagnostics(); +}); + +describe('teamRefreshBurstDiagnostics store', () => { + it('creates a window on the first refresh note', () => { + expect(noteTeamRefreshBurst('my-team', 4_000, 10_000)).toBe(1); + + expect(getTeamRefreshBurstDiagnosticForTests('my-team')).toEqual({ + windowStartedAt: 10_000, + count: 1, + lastWarnAt: 0, + }); + expect(hasTeamRefreshBurstDiagnostics('my-team')).toBe(true); + }); + + it('increments inside the active burst window', () => { + expect(noteTeamRefreshBurst('my-team', 4_000, 10_000)).toBe(1); + expect(noteTeamRefreshBurst('my-team', 4_000, 13_999)).toBe(2); + expect(noteTeamRefreshBurst('my-team', 4_000, 14_000)).toBe(3); + + expect(getTeamRefreshBurstDiagnosticForTests('my-team')).toEqual({ + windowStartedAt: 10_000, + count: 3, + lastWarnAt: 0, + }); + }); + + it('resets only after now is strictly beyond the burst window', () => { + expect(noteTeamRefreshBurst('my-team', 4_000, 10_000)).toBe(1); + expect(noteTeamRefreshBurst('my-team', 4_000, 14_001)).toBe(1); + + expect(getTeamRefreshBurstDiagnosticForTests('my-team')).toEqual({ + windowStartedAt: 14_001, + count: 1, + lastWarnAt: 0, + }); + }); + + it('tracks each team independently', () => { + noteTeamRefreshBurst('my-team', 4_000, 10_000); + noteTeamRefreshBurst('my-team', 4_000, 10_500); + noteTeamRefreshBurst('other-team', 4_000, 11_000); + + expect(getTeamRefreshBurstDiagnosticForTests('my-team')?.count).toBe(2); + expect(getTeamRefreshBurstDiagnosticForTests('other-team')?.count).toBe(1); + }); + + it('clears one team or all diagnostics', () => { + noteTeamRefreshBurst('my-team', 4_000, 10_000); + noteTeamRefreshBurst('other-team', 4_000, 11_000); + + clearTeamRefreshBurstDiagnostics('my-team'); + + expect(hasTeamRefreshBurstDiagnostics('my-team')).toBe(false); + expect(hasTeamRefreshBurstDiagnostics('other-team')).toBe(true); + + clearAllTeamRefreshBurstDiagnostics(); + + expect(hasTeamRefreshBurstDiagnostics('other-team')).toBe(false); + }); + + it('returns defensive diagnostic snapshots for tests', () => { + noteTeamRefreshBurst('my-team', 4_000, 10_000); + + const snapshot = getTeamRefreshBurstDiagnosticForTests('my-team'); + if (snapshot) { + snapshot.count = 99; + } + + expect(getTeamRefreshBurstDiagnosticForTests('my-team')?.count).toBe(1); + }); +});