refactor(team): extract refresh burst diagnostics
This commit is contained in:
parent
34902059b2
commit
9bd8585617
3 changed files with 141 additions and 27 deletions
|
|
@ -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<string>();
|
|||
const pendingTeamPendingReplyRefreshTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
let inFlightGlobalTasksRefresh: Promise<void> | 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<AppState, [], [], TeamSlice> = (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);
|
||||
}
|
||||
|
|
|
|||
48
src/renderer/store/team/teamRefreshBurstDiagnostics.ts
Normal file
48
src/renderer/store/team/teamRefreshBurstDiagnostics.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
interface TeamRefreshBurstDiagnostic {
|
||||
windowStartedAt: number;
|
||||
count: number;
|
||||
lastWarnAt: number;
|
||||
}
|
||||
|
||||
const teamRefreshBurstDiagnostics = new Map<string, TeamRefreshBurstDiagnostic>();
|
||||
|
||||
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();
|
||||
}
|
||||
83
test/renderer/store/teamRefreshBurstDiagnosticsStore.test.ts
Normal file
83
test/renderer/store/teamRefreshBurstDiagnosticsStore.test.ts
Normal file
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue