From 4a561f2cd2005cffd9069d9d0b4dba3461bb917b Mon Sep 17 00:00:00 2001 From: 777genius Date: Fri, 22 May 2026 09:50:06 +0300 Subject: [PATCH] refactor(team): extract pending reply waits --- src/renderer/store/slices/teamSlice.ts | 52 +++------------ .../store/team/teamPendingReplyWaits.ts | 45 +++++++++++++ .../store/teamPendingReplyWaits.test.ts | 65 +++++++++++++++++++ 3 files changed, 120 insertions(+), 42 deletions(-) create mode 100644 src/renderer/store/team/teamPendingReplyWaits.ts create mode 100644 test/renderer/store/teamPendingReplyWaits.test.ts diff --git a/src/renderer/store/slices/teamSlice.ts b/src/renderer/store/slices/teamSlice.ts index 86e2c864..c1f17995 100644 --- a/src/renderer/store/slices/teamSlice.ts +++ b/src/renderer/store/slices/teamSlice.ts @@ -49,6 +49,11 @@ import { pruneOptimisticMessages, upsertOptimisticTeamMessage, } from '../team/teamMessagesCache'; +import { + clearAllPendingReplyRefreshWaits, + clearPendingReplyRefreshWaits, + setPendingReplyRefreshEnabled, +} from '../team/teamPendingReplyWaits'; import { noteTeamRefreshFanout } from '../teamRefreshFanoutDiagnostics'; import { getWorktreeNavigationState } from '../utils/stateResetHelpers'; @@ -115,6 +120,10 @@ export type { TeamMessagesCacheEntry, } from '../team/teamMessagesCache'; export { selectMemberMessagesForTeamMember, selectTeamMessages } from '../team/teamMessagesCache'; +export { + getActiveTeamPendingReplyWaits, + hasActiveTeamPendingReplyWait, +} from '../team/teamPendingReplyWaits'; const GRAPH_STABLE_SLOT_LAYOUT_VERSION = 'stable-slots-v1' as const; const DISABLE_PERSISTED_TEAM_GRAPH_SLOT_ASSIGNMENTS = true; @@ -148,7 +157,6 @@ const pendingFreshTeamMessagesHeadRefreshes = new Set(); const inFlightTeamMemberActivityMetaRequests = new Map>(); const pendingFreshTeamMemberActivityMetaRefreshes = new Set(); const pendingTeamPendingReplyRefreshTimers = new Map>(); -const activeTeamPendingReplyWaitSourceIdsByTeam = new Map>(); const lastResolvedTeamDataRefreshAtByTeam = new Map(); const teamLocalStateEpochByTeam = new Map(); let inFlightGlobalTasksRefresh: Promise | null = null; @@ -203,18 +211,6 @@ export function getLastResolvedTeamDataRefreshAt(teamName: string): number | und return lastResolvedTeamDataRefreshAtByTeam.get(teamName); } -export function hasActiveTeamPendingReplyWait(teamName: string): boolean { - return (activeTeamPendingReplyWaitSourceIdsByTeam.get(teamName)?.size ?? 0) > 0; -} - -export function getActiveTeamPendingReplyWaits(): Set { - return new Set( - Array.from(activeTeamPendingReplyWaitSourceIdsByTeam.entries()) - .filter(([, sourceIds]) => sourceIds.size > 0) - .map(([teamName]) => teamName) - ); -} - export function __resetTeamSliceModuleStateForTests(): void { inFlightTeamDataRequests.clear(); inFlightRefreshTeamDataCalls.clear(); @@ -234,7 +230,7 @@ export function __resetTeamSliceModuleStateForTests(): void { clearTimeout(timer); } pendingTeamPendingReplyRefreshTimers.clear(); - activeTeamPendingReplyWaitSourceIdsByTeam.clear(); + clearAllPendingReplyRefreshWaits(); lastResolvedTeamDataRefreshAtByTeam.clear(); teamLocalStateEpochByTeam.clear(); memberSpawnStatusesIpcBackoffUntilByTeam.clear(); @@ -1082,34 +1078,6 @@ function clearPendingReplyRefreshTimer(teamName: string): void { pendingTeamPendingReplyRefreshTimers.delete(teamName); } -function clearPendingReplyRefreshWaits(teamName: string): void { - activeTeamPendingReplyWaitSourceIdsByTeam.delete(teamName); -} - -function setPendingReplyRefreshEnabled( - teamName: string, - sourceId: string, - enabled: boolean -): boolean { - if (enabled) { - const existing = activeTeamPendingReplyWaitSourceIdsByTeam.get(teamName) ?? new Set(); - existing.add(sourceId); - activeTeamPendingReplyWaitSourceIdsByTeam.set(teamName, existing); - return true; - } - - const existing = activeTeamPendingReplyWaitSourceIdsByTeam.get(teamName); - if (!existing) { - return false; - } - existing.delete(sourceId); - if (existing.size === 0) { - activeTeamPendingReplyWaitSourceIdsByTeam.delete(teamName); - return false; - } - return true; -} - async function refreshTaskChangePresenceForUpdatedTask( getState: () => AppState, teamName: string, diff --git a/src/renderer/store/team/teamPendingReplyWaits.ts b/src/renderer/store/team/teamPendingReplyWaits.ts new file mode 100644 index 00000000..e4c11b65 --- /dev/null +++ b/src/renderer/store/team/teamPendingReplyWaits.ts @@ -0,0 +1,45 @@ +const activeTeamPendingReplyWaitSourceIdsByTeam = new Map>(); + +export function hasActiveTeamPendingReplyWait(teamName: string): boolean { + return (activeTeamPendingReplyWaitSourceIdsByTeam.get(teamName)?.size ?? 0) > 0; +} + +export function getActiveTeamPendingReplyWaits(): Set { + return new Set( + Array.from(activeTeamPendingReplyWaitSourceIdsByTeam.entries()) + .filter(([, sourceIds]) => sourceIds.size > 0) + .map(([teamName]) => teamName) + ); +} + +export function clearAllPendingReplyRefreshWaits(): void { + activeTeamPendingReplyWaitSourceIdsByTeam.clear(); +} + +export function clearPendingReplyRefreshWaits(teamName: string): void { + activeTeamPendingReplyWaitSourceIdsByTeam.delete(teamName); +} + +export function setPendingReplyRefreshEnabled( + teamName: string, + sourceId: string, + enabled: boolean +): boolean { + if (enabled) { + const existing = activeTeamPendingReplyWaitSourceIdsByTeam.get(teamName) ?? new Set(); + existing.add(sourceId); + activeTeamPendingReplyWaitSourceIdsByTeam.set(teamName, existing); + return true; + } + + const existing = activeTeamPendingReplyWaitSourceIdsByTeam.get(teamName); + if (!existing) { + return false; + } + existing.delete(sourceId); + if (existing.size === 0) { + activeTeamPendingReplyWaitSourceIdsByTeam.delete(teamName); + return false; + } + return true; +} diff --git a/test/renderer/store/teamPendingReplyWaits.test.ts b/test/renderer/store/teamPendingReplyWaits.test.ts new file mode 100644 index 00000000..45a7b1aa --- /dev/null +++ b/test/renderer/store/teamPendingReplyWaits.test.ts @@ -0,0 +1,65 @@ +import { afterEach, describe, expect, it } from 'vitest'; + +import { + clearAllPendingReplyRefreshWaits, + clearPendingReplyRefreshWaits, + getActiveTeamPendingReplyWaits, + hasActiveTeamPendingReplyWait, + setPendingReplyRefreshEnabled, +} from '../../../src/renderer/store/team/teamPendingReplyWaits'; + +afterEach(() => { + clearAllPendingReplyRefreshWaits(); +}); + +describe('teamPendingReplyWaits', () => { + it('tracks active teams with at least one enabled source', () => { + expect(setPendingReplyRefreshEnabled('my-team', 'tab-a', true)).toBe(true); + expect(setPendingReplyRefreshEnabled('other-team', 'tab-b', true)).toBe(true); + + expect(hasActiveTeamPendingReplyWait('my-team')).toBe(true); + expect(hasActiveTeamPendingReplyWait('other-team')).toBe(true); + expect(getActiveTeamPendingReplyWaits()).toEqual(new Set(['my-team', 'other-team'])); + }); + + it('keeps a team active until the last source is disabled', () => { + setPendingReplyRefreshEnabled('my-team', 'tab-a', true); + setPendingReplyRefreshEnabled('my-team', 'tab-b', true); + + expect(setPendingReplyRefreshEnabled('my-team', 'tab-b', false)).toBe(true); + expect(hasActiveTeamPendingReplyWait('my-team')).toBe(true); + expect(getActiveTeamPendingReplyWaits()).toEqual(new Set(['my-team'])); + + expect(setPendingReplyRefreshEnabled('my-team', 'tab-a', false)).toBe(false); + expect(hasActiveTeamPendingReplyWait('my-team')).toBe(false); + expect(getActiveTeamPendingReplyWaits().size).toBe(0); + }); + + it('is idempotent for repeated enables from the same source', () => { + setPendingReplyRefreshEnabled('my-team', 'tab-a', true); + setPendingReplyRefreshEnabled('my-team', 'tab-a', true); + + expect(setPendingReplyRefreshEnabled('my-team', 'tab-a', false)).toBe(false); + expect(hasActiveTeamPendingReplyWait('my-team')).toBe(false); + }); + + it('returns false when disabling a source that has no active wait', () => { + expect(setPendingReplyRefreshEnabled('missing-team', 'tab-a', false)).toBe(false); + expect(getActiveTeamPendingReplyWaits().size).toBe(0); + }); + + it('clears waits by team or globally', () => { + setPendingReplyRefreshEnabled('my-team', 'tab-a', true); + setPendingReplyRefreshEnabled('other-team', 'tab-b', true); + + clearPendingReplyRefreshWaits('my-team'); + + expect(hasActiveTeamPendingReplyWait('my-team')).toBe(false); + expect(getActiveTeamPendingReplyWaits()).toEqual(new Set(['other-team'])); + + clearAllPendingReplyRefreshWaits(); + + expect(hasActiveTeamPendingReplyWait('other-team')).toBe(false); + expect(getActiveTeamPendingReplyWaits().size).toBe(0); + }); +});