diff --git a/src/renderer/store/slices/teamSlice.ts b/src/renderer/store/slices/teamSlice.ts index 9b6932b8..fa389985 100644 --- a/src/renderer/store/slices/teamSlice.ts +++ b/src/renderer/store/slices/teamSlice.ts @@ -247,6 +247,7 @@ const pendingFreshTeamMemberActivityMetaRefreshes = new Set(); const pendingTeamPendingReplyRefreshTimers = new Map>(); let latestTeamsFetchRequestId = 0; let inFlightGlobalTasksRefresh: Promise | null = null; +let inFlightGlobalTasksRefreshScope: ContextRequestScope | null = null; let pendingFreshGlobalTasksRefresh = false; interface RefreshTeamDataOptions { withDedup?: boolean; @@ -1642,7 +1643,13 @@ export const createTeamSlice: StateCreator = (set, fetchAllTasks: async () => { if (inFlightGlobalTasksRefresh) { - pendingFreshGlobalTasksRefresh = true; + const inFlightScope = inFlightGlobalTasksRefreshScope; + if ( + get().globalTasksInitialized || + (inFlightScope && !isContextRequestScopeCurrent(get, inFlightScope)) + ) { + pendingFreshGlobalTasksRefresh = true; + } await inFlightGlobalTasksRefresh; return; } @@ -1658,6 +1665,7 @@ export const createTeamSlice: StateCreator = (set, set({ globalTasksLoading: true, globalTasksError: null }); } const requestScope = captureContextRequestScope(get); + inFlightGlobalTasksRefreshScope = requestScope; const oldTasks = get().globalTasks; try { const tasks = await withTimeout( @@ -1706,6 +1714,7 @@ export const createTeamSlice: StateCreator = (set, const request = runRefresh().finally(() => { if (inFlightGlobalTasksRefresh === request) { inFlightGlobalTasksRefresh = null; + inFlightGlobalTasksRefreshScope = null; } }); inFlightGlobalTasksRefresh = request; diff --git a/test/renderer/store/teamSliceContextRace.test.ts b/test/renderer/store/teamSliceContextRace.test.ts index a4f62aac..b37d9f31 100644 --- a/test/renderer/store/teamSliceContextRace.test.ts +++ b/test/renderer/store/teamSliceContextRace.test.ts @@ -326,6 +326,36 @@ describe('team slice context races', () => { expect(store.getState().globalTasksLoading).toBe(false); }); + it('coalesces concurrent initial global task refreshes for the same context', async () => { + const store = createSliceStore(); + const initialTasks = deferred(); + apiMock.teams.getAllTasks.mockReturnValueOnce(initialTasks.promise); + + const firstFetch = store.getState().fetchAllTasks(); + const secondFetch = store.getState().fetchAllTasks(); + + initialTasks.resolve([ + { + id: 'initial-task', + subject: 'Initial task', + status: 'todo', + teamName: 'initial-team', + teamDisplayName: 'Initial Team', + projectPath: '/initial/project', + comments: [], + }, + ]); + + await Promise.all([firstFetch, secondFetch]); + + expect(apiMock.teams.getAllTasks).toHaveBeenCalledTimes(1); + expect(store.getState().globalTasks).toEqual([ + expect.objectContaining({ id: 'initial-task', teamName: 'initial-team' }), + ]); + expect(store.getState().globalTasksInitialized).toBe(true); + expect(store.getState().globalTasksLoading).toBe(false); + }); + it('ignores global tasks loaded before a context epoch reset with the same context id', async () => { const store = createSliceStore(); const localTasks = deferred();