refactor(team): extract spawn status backoff

This commit is contained in:
777genius 2026-05-22 10:08:13 +03:00
parent e718ccf39a
commit 38b0a87d5d
3 changed files with 123 additions and 9 deletions

View file

@ -51,6 +51,13 @@ import {
invalidateTeamLocalStateEpoch,
isTeamLocalStateEpochCurrent,
} from '../team/teamLocalStateEpoch';
import {
clearAllMemberSpawnStatusesIpcBackoffs,
clearMemberSpawnStatusesIpcBackoff,
hasMemberSpawnStatusesIpcBackoff,
isMemberSpawnStatusesIpcBackoffActive,
recordMemberSpawnStatusesIpcRetryBackoff,
} from '../team/teamMemberSpawnStatusBackoff';
import {
areInboxMessageArraysEquivalent,
clearTeamMessageSelectorCaches,
@ -173,7 +180,6 @@ const pendingFreshTeamMemberActivityMetaRefreshes = new Set<string>();
const pendingTeamPendingReplyRefreshTimers = new Map<string, ReturnType<typeof setTimeout>>();
let inFlightGlobalTasksRefresh: Promise<void> | null = null;
let pendingFreshGlobalTasksRefresh = false;
const memberSpawnStatusesIpcBackoffUntilByTeam = new Map<string, number>();
const teamRefreshBurstDiagnostics = new Map<
string,
{ windowStartedAt: number; count: number; lastWarnAt: number }
@ -241,7 +247,7 @@ export function __resetTeamSliceModuleStateForTests(): void {
clearAllPendingReplyRefreshWaits();
clearAllLastResolvedTeamDataRefreshes();
clearAllTeamLocalStateEpochs();
memberSpawnStatusesIpcBackoffUntilByTeam.clear();
clearAllMemberSpawnStatusesIpcBackoffs();
teamRefreshBurstDiagnostics.clear();
memberSpawnUiEqualLastWarnAtByTeam.clear();
resolvedMembersSelectorCache.clear();
@ -274,7 +280,7 @@ function clearTeamScopedTransientState(teamName: string): void {
inFlightTeamMemberActivityMetaRequests.delete(teamName);
pendingFreshTeamMemberActivityMetaRefreshes.delete(teamName);
clearLastResolvedTeamDataRefreshAt(teamName);
memberSpawnStatusesIpcBackoffUntilByTeam.delete(teamName);
clearMemberSpawnStatusesIpcBackoff(teamName);
teamRefreshBurstDiagnostics.delete(teamName);
memberSpawnUiEqualLastWarnAtByTeam.delete(teamName);
clearTeamScopedSelectorCaches(teamName);
@ -660,7 +666,7 @@ export function __getTeamScopedTransientStateForTests(teamName: string): {
pendingFreshTeamMemberActivityMetaRefreshes.has(teamName),
hasLastResolvedTeamDataRefresh: hasLastResolvedTeamDataRefreshAt(teamName),
hasCurrentLocalStateEpoch: hasTeamLocalStateEpoch(teamName),
hasMemberSpawnStatusesIpcBackoff: memberSpawnStatusesIpcBackoffUntilByTeam.has(teamName),
hasMemberSpawnStatusesIpcBackoff: hasMemberSpawnStatusesIpcBackoff(teamName),
hasTeamRefreshBurstDiagnostics: teamRefreshBurstDiagnostics.has(teamName),
hasMemberSpawnUiEqualLastWarn: memberSpawnUiEqualLastWarnAtByTeam.has(teamName),
};
@ -2987,13 +2993,12 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
launchParamsByTeam: loadAllLaunchParams(),
fetchMemberSpawnStatuses: async (teamName: string) => {
if (!api.teams?.getMemberSpawnStatuses) return;
const backoffUntil = memberSpawnStatusesIpcBackoffUntilByTeam.get(teamName) ?? 0;
if (backoffUntil > Date.now()) {
if (isMemberSpawnStatusesIpcBackoffActive(teamName)) {
return;
}
try {
const snapshot = await api.teams.getMemberSpawnStatuses(teamName);
memberSpawnStatusesIpcBackoffUntilByTeam.delete(teamName);
clearMemberSpawnStatusesIpcBackoff(teamName);
set((prev) => {
if (snapshot.runId != null && prev.ignoredRuntimeRunIds[snapshot.runId] === teamName) {
return {};
@ -3057,9 +3062,9 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (message.includes("No handler registered for 'team:memberSpawnStatuses'")) {
memberSpawnStatusesIpcBackoffUntilByTeam.set(
recordMemberSpawnStatusesIpcRetryBackoff(
teamName,
Date.now() + MEMBER_SPAWN_STATUSES_IPC_RETRY_BACKOFF_MS
MEMBER_SPAWN_STATUSES_IPC_RETRY_BACKOFF_MS
);
}
// ignore — spawn statuses are best-effort

View file

@ -0,0 +1,39 @@
const memberSpawnStatusesIpcBackoffUntilByTeam = new Map<string, number>();
export function getMemberSpawnStatusesIpcBackoffUntil(teamName: string): number {
return memberSpawnStatusesIpcBackoffUntilByTeam.get(teamName) ?? 0;
}
export function hasMemberSpawnStatusesIpcBackoff(teamName: string): boolean {
return memberSpawnStatusesIpcBackoffUntilByTeam.has(teamName);
}
export function isMemberSpawnStatusesIpcBackoffActive(
teamName: string,
now = Date.now()
): boolean {
return getMemberSpawnStatusesIpcBackoffUntil(teamName) > now;
}
export function recordMemberSpawnStatusesIpcBackoffUntil(
teamName: string,
backoffUntil: number
): void {
memberSpawnStatusesIpcBackoffUntilByTeam.set(teamName, backoffUntil);
}
export function recordMemberSpawnStatusesIpcRetryBackoff(
teamName: string,
retryBackoffMs: number,
now = Date.now()
): void {
recordMemberSpawnStatusesIpcBackoffUntil(teamName, now + retryBackoffMs);
}
export function clearMemberSpawnStatusesIpcBackoff(teamName: string): void {
memberSpawnStatusesIpcBackoffUntilByTeam.delete(teamName);
}
export function clearAllMemberSpawnStatusesIpcBackoffs(): void {
memberSpawnStatusesIpcBackoffUntilByTeam.clear();
}

View file

@ -0,0 +1,70 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import {
clearAllMemberSpawnStatusesIpcBackoffs,
clearMemberSpawnStatusesIpcBackoff,
getMemberSpawnStatusesIpcBackoffUntil,
hasMemberSpawnStatusesIpcBackoff,
isMemberSpawnStatusesIpcBackoffActive,
recordMemberSpawnStatusesIpcBackoffUntil,
recordMemberSpawnStatusesIpcRetryBackoff,
} from '../../../src/renderer/store/team/teamMemberSpawnStatusBackoff';
afterEach(() => {
vi.useRealTimers();
clearAllMemberSpawnStatusesIpcBackoffs();
});
describe('teamMemberSpawnStatusBackoff', () => {
it('defaults to no backoff for unknown teams', () => {
expect(getMemberSpawnStatusesIpcBackoffUntil('my-team')).toBe(0);
expect(hasMemberSpawnStatusesIpcBackoff('my-team')).toBe(false);
expect(isMemberSpawnStatusesIpcBackoffActive('my-team', 100)).toBe(false);
});
it('tracks active backoff deadlines by team', () => {
recordMemberSpawnStatusesIpcBackoffUntil('my-team', 150);
recordMemberSpawnStatusesIpcBackoffUntil('other-team', 250);
expect(getMemberSpawnStatusesIpcBackoffUntil('my-team')).toBe(150);
expect(isMemberSpawnStatusesIpcBackoffActive('my-team', 149)).toBe(true);
expect(isMemberSpawnStatusesIpcBackoffActive('my-team', 150)).toBe(false);
expect(isMemberSpawnStatusesIpcBackoffActive('other-team', 249)).toBe(true);
});
it('records retry backoff from Date.now by default', () => {
vi.setSystemTime(new Date('2026-05-22T07:00:00.000Z'));
recordMemberSpawnStatusesIpcRetryBackoff('my-team', 5_000);
expect(getMemberSpawnStatusesIpcBackoffUntil('my-team')).toBe(
new Date('2026-05-22T07:00:05.000Z').getTime()
);
});
it('records retry backoff from an explicit clock for deterministic callers', () => {
recordMemberSpawnStatusesIpcRetryBackoff('my-team', 5_000, 100);
expect(getMemberSpawnStatusesIpcBackoffUntil('my-team')).toBe(5_100);
});
it('clears one team backoff without touching others', () => {
recordMemberSpawnStatusesIpcBackoffUntil('my-team', 150);
recordMemberSpawnStatusesIpcBackoffUntil('other-team', 250);
clearMemberSpawnStatusesIpcBackoff('my-team');
expect(hasMemberSpawnStatusesIpcBackoff('my-team')).toBe(false);
expect(getMemberSpawnStatusesIpcBackoffUntil('other-team')).toBe(250);
});
it('clears all recorded backoffs', () => {
recordMemberSpawnStatusesIpcBackoffUntil('my-team', 150);
recordMemberSpawnStatusesIpcBackoffUntil('other-team', 250);
clearAllMemberSpawnStatusesIpcBackoffs();
expect(hasMemberSpawnStatusesIpcBackoff('my-team')).toBe(false);
expect(hasMemberSpawnStatusesIpcBackoff('other-team')).toBe(false);
});
});