refactor(team): extract local state epoch registry

This commit is contained in:
777genius 2026-05-22 09:56:32 +03:00
parent 4a561f2cd2
commit 7d08a10d6f
3 changed files with 91 additions and 15 deletions

View file

@ -38,6 +38,13 @@ import {
normalizeTeamGetDataOptions,
} from '../team/teamDataRequestKeys';
import { selectTeamDataForName } from '../team/teamDataSelectors';
import {
captureTeamLocalStateEpoch,
clearAllTeamLocalStateEpochs,
hasTeamLocalStateEpoch,
invalidateTeamLocalStateEpoch,
isTeamLocalStateEpochCurrent,
} from '../team/teamLocalStateEpoch';
import {
areInboxMessageArraysEquivalent,
clearTeamMessageSelectorCaches,
@ -158,7 +165,6 @@ const inFlightTeamMemberActivityMetaRequests = new Map<string, Promise<void>>();
const pendingFreshTeamMemberActivityMetaRefreshes = new Set<string>();
const pendingTeamPendingReplyRefreshTimers = new Map<string, ReturnType<typeof setTimeout>>();
const lastResolvedTeamDataRefreshAtByTeam = new Map<string, number>();
const teamLocalStateEpochByTeam = new Map<string, number>();
let inFlightGlobalTasksRefresh: Promise<void> | null = null;
let pendingFreshGlobalTasksRefresh = false;
const memberSpawnStatusesIpcBackoffUntilByTeam = new Map<string, number>();
@ -232,7 +238,7 @@ export function __resetTeamSliceModuleStateForTests(): void {
pendingTeamPendingReplyRefreshTimers.clear();
clearAllPendingReplyRefreshWaits();
lastResolvedTeamDataRefreshAtByTeam.clear();
teamLocalStateEpochByTeam.clear();
clearAllTeamLocalStateEpochs();
memberSpawnStatusesIpcBackoffUntilByTeam.clear();
teamRefreshBurstDiagnostics.clear();
memberSpawnUiEqualLastWarnAtByTeam.clear();
@ -432,18 +438,6 @@ function buildTeamScopedProgressTombstones(
};
}
function captureTeamLocalStateEpoch(teamName: string): number {
return teamLocalStateEpochByTeam.get(teamName) ?? 0;
}
function isTeamLocalStateEpochCurrent(teamName: string, epoch: number): boolean {
return captureTeamLocalStateEpoch(teamName) === epoch;
}
function invalidateTeamLocalStateEpoch(teamName: string): void {
teamLocalStateEpochByTeam.set(teamName, captureTeamLocalStateEpoch(teamName) + 1);
}
function beginInFlightTeamDataRefresh(teamName: string): symbol {
const token = Symbol(teamName);
const existing = inFlightRefreshTeamDataCalls.get(teamName);
@ -663,7 +657,7 @@ export function __getTeamScopedTransientStateForTests(teamName: string): {
hasPendingFreshMemberActivityMetaRefresh:
pendingFreshTeamMemberActivityMetaRefreshes.has(teamName),
hasLastResolvedTeamDataRefresh: lastResolvedTeamDataRefreshAtByTeam.has(teamName),
hasCurrentLocalStateEpoch: teamLocalStateEpochByTeam.has(teamName),
hasCurrentLocalStateEpoch: hasTeamLocalStateEpoch(teamName),
hasMemberSpawnStatusesIpcBackoff: memberSpawnStatusesIpcBackoffUntilByTeam.has(teamName),
hasTeamRefreshBurstDiagnostics: teamRefreshBurstDiagnostics.has(teamName),
hasMemberSpawnUiEqualLastWarn: memberSpawnUiEqualLastWarnAtByTeam.has(teamName),

View file

@ -0,0 +1,25 @@
const teamLocalStateEpochByTeam = new Map<string, number>();
export function captureTeamLocalStateEpoch(teamName: string): number {
return teamLocalStateEpochByTeam.get(teamName) ?? 0;
}
export function isTeamLocalStateEpochCurrent(teamName: string, epoch: number): boolean {
return captureTeamLocalStateEpoch(teamName) === epoch;
}
export function invalidateTeamLocalStateEpoch(teamName: string): void {
teamLocalStateEpochByTeam.set(teamName, captureTeamLocalStateEpoch(teamName) + 1);
}
export function hasTeamLocalStateEpoch(teamName: string): boolean {
return teamLocalStateEpochByTeam.has(teamName);
}
export function clearTeamLocalStateEpoch(teamName: string): void {
teamLocalStateEpochByTeam.delete(teamName);
}
export function clearAllTeamLocalStateEpochs(): void {
teamLocalStateEpochByTeam.clear();
}

View file

@ -0,0 +1,57 @@
import { afterEach, describe, expect, it } from 'vitest';
import {
captureTeamLocalStateEpoch,
clearAllTeamLocalStateEpochs,
clearTeamLocalStateEpoch,
hasTeamLocalStateEpoch,
invalidateTeamLocalStateEpoch,
isTeamLocalStateEpochCurrent,
} from '../../../src/renderer/store/team/teamLocalStateEpoch';
afterEach(() => {
clearAllTeamLocalStateEpochs();
});
describe('teamLocalStateEpoch', () => {
it('starts missing teams at epoch zero without materializing an entry', () => {
expect(captureTeamLocalStateEpoch('my-team')).toBe(0);
expect(isTeamLocalStateEpochCurrent('my-team', 0)).toBe(true);
expect(hasTeamLocalStateEpoch('my-team')).toBe(false);
});
it('increments epochs independently per team', () => {
invalidateTeamLocalStateEpoch('my-team');
invalidateTeamLocalStateEpoch('my-team');
invalidateTeamLocalStateEpoch('other-team');
expect(captureTeamLocalStateEpoch('my-team')).toBe(2);
expect(captureTeamLocalStateEpoch('other-team')).toBe(1);
expect(isTeamLocalStateEpochCurrent('my-team', 1)).toBe(false);
expect(isTeamLocalStateEpochCurrent('my-team', 2)).toBe(true);
});
it('clears one team epoch without touching other teams', () => {
invalidateTeamLocalStateEpoch('my-team');
invalidateTeamLocalStateEpoch('other-team');
clearTeamLocalStateEpoch('my-team');
expect(captureTeamLocalStateEpoch('my-team')).toBe(0);
expect(hasTeamLocalStateEpoch('my-team')).toBe(false);
expect(captureTeamLocalStateEpoch('other-team')).toBe(1);
expect(hasTeamLocalStateEpoch('other-team')).toBe(true);
});
it('clears all materialized epochs', () => {
invalidateTeamLocalStateEpoch('my-team');
invalidateTeamLocalStateEpoch('other-team');
clearAllTeamLocalStateEpochs();
expect(hasTeamLocalStateEpoch('my-team')).toBe(false);
expect(hasTeamLocalStateEpoch('other-team')).toBe(false);
expect(captureTeamLocalStateEpoch('my-team')).toBe(0);
expect(captureTeamLocalStateEpoch('other-team')).toBe(0);
});
});