diff --git a/src/renderer/src/app-shell/startup-actions-selector.test.ts b/src/renderer/src/app-shell/startup-actions-selector.test.ts new file mode 100644 index 00000000000..1d559eb0dcb --- /dev/null +++ b/src/renderer/src/app-shell/startup-actions-selector.test.ts @@ -0,0 +1,91 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { createStore } from 'zustand/vanilla' +import { + resetStartupActionsSelectorCacheForTest, + selectStartupActions, + type StartupActions +} from './startup-actions-selector' + +type StartupTestState = StartupActions & { publication: number } + +function makeActions(): StartupActions { + return { + fetchReposForAllHosts: vi.fn(), + awaitLocalRepoCatalogSettlement: vi.fn(), + fetchProjectGroupsForAllHosts: vi.fn(), + fetchFolderWorkspacesForAllHosts: vi.fn(), + fetchAllWorktrees: vi.fn(), + fetchWorktrees: vi.fn(), + fetchWorktreeLineage: vi.fn(), + fetchOrcaProfiles: vi.fn(), + fetchSettings: vi.fn(), + awaitOwnerWorktreeVisibilityDefaultsHydration: vi.fn(), + fetchKeybindings: vi.fn(), + initGitHubCache: vi.fn(), + hydrateWorkspaceSession: vi.fn(), + hydrateTabsSession: vi.fn(), + hydrateEditorSession: vi.fn(), + hydrateBrowserSession: vi.fn(), + fetchBrowserSessionProfiles: vi.fn(), + reconnectPersistedTerminals: vi.fn(), + setTerminalStartupRestorationReady: vi.fn(), + setDeferredSshReconnectTargets: vi.fn(), + setSshConnectionState: vi.fn(), + hydratePersistedUI: vi.fn(), + setHydrationSucceeded: vi.fn(), + pruneLastVisitedTimestamps: vi.fn(), + seedActiveWorktreeLastVisitedIfMissing: vi.fn() + } as StartupActions +} + +describe('startup action selector', () => { + beforeEach(() => { + resetStartupActionsSelectorCacheForTest() + }) + + it('reuses one action bundle across 1,000 repeated reads', () => { + const actions = makeActions() + const first = selectStartupActions(actions) + + for (let read = 0; read < 1_000; read += 1) { + expect(selectStartupActions(actions)).toBe(first) + } + }) + + it('rebuilds the bundle when one action reference changes', () => { + const actions = makeActions() + const first = selectStartupActions(actions) + const replacement = { ...actions, fetchSettings: vi.fn() } as StartupActions + + const next = selectStartupActions(replacement) + expect(next).not.toBe(first) + + for (let read = 0; read < 1_000; read += 1) { + expect(selectStartupActions(replacement)).toBe(next) + } + }) + + it('keeps the selected projection stable across 1,000 unrelated publications', () => { + const actions = makeActions() + const store = createStore(() => ({ ...actions, publication: 0 })) + let publicationCount = 0 + let projectionChangeCount = 0 + let previousProjection = selectStartupActions(store.getState()) + const unsubscribe = store.subscribe((state) => { + publicationCount += 1 + const projection = selectStartupActions(state) + if (projection !== previousProjection) { + projectionChangeCount += 1 + } + previousProjection = projection + }) + + for (let publication = 1; publication <= 1_000; publication += 1) { + store.setState({ publication }) + } + + expect(publicationCount).toBe(1_000) + expect(projectionChangeCount).toBe(0) + unsubscribe() + }) +}) diff --git a/src/renderer/src/app-shell/startup-actions-selector.ts b/src/renderer/src/app-shell/startup-actions-selector.ts new file mode 100644 index 00000000000..c7dca311a03 --- /dev/null +++ b/src/renderer/src/app-shell/startup-actions-selector.ts @@ -0,0 +1,105 @@ +import type { AppState } from '../store/types' + +export type StartupActions = Pick< + AppState, + | 'fetchReposForAllHosts' + | 'awaitLocalRepoCatalogSettlement' + | 'fetchProjectGroupsForAllHosts' + | 'fetchFolderWorkspacesForAllHosts' + | 'fetchAllWorktrees' + | 'fetchWorktrees' + | 'fetchWorktreeLineage' + | 'fetchOrcaProfiles' + | 'fetchSettings' + | 'awaitOwnerWorktreeVisibilityDefaultsHydration' + | 'fetchKeybindings' + | 'initGitHubCache' + | 'hydrateWorkspaceSession' + | 'hydrateTabsSession' + | 'hydrateEditorSession' + | 'hydrateBrowserSession' + | 'fetchBrowserSessionProfiles' + | 'reconnectPersistedTerminals' + | 'setTerminalStartupRestorationReady' + | 'setDeferredSshReconnectTargets' + | 'setSshConnectionState' + | 'hydratePersistedUI' + | 'setHydrationSucceeded' + | 'pruneLastVisitedTimestamps' + | 'seedActiveWorktreeLastVisitedIfMissing' +> + +let cachedStartupActions: StartupActions | null = null + +/** Keeps the always-mounted startup subscription stable across unrelated store writes. */ +export function selectStartupActions(state: StartupActions): StartupActions { + if ( + cachedStartupActions && + cachedStartupActions.fetchReposForAllHosts === state.fetchReposForAllHosts && + cachedStartupActions.awaitLocalRepoCatalogSettlement === + state.awaitLocalRepoCatalogSettlement && + cachedStartupActions.fetchProjectGroupsForAllHosts === state.fetchProjectGroupsForAllHosts && + cachedStartupActions.fetchFolderWorkspacesForAllHosts === + state.fetchFolderWorkspacesForAllHosts && + cachedStartupActions.fetchAllWorktrees === state.fetchAllWorktrees && + cachedStartupActions.fetchWorktrees === state.fetchWorktrees && + cachedStartupActions.fetchWorktreeLineage === state.fetchWorktreeLineage && + cachedStartupActions.fetchOrcaProfiles === state.fetchOrcaProfiles && + cachedStartupActions.fetchSettings === state.fetchSettings && + cachedStartupActions.awaitOwnerWorktreeVisibilityDefaultsHydration === + state.awaitOwnerWorktreeVisibilityDefaultsHydration && + cachedStartupActions.fetchKeybindings === state.fetchKeybindings && + cachedStartupActions.initGitHubCache === state.initGitHubCache && + cachedStartupActions.hydrateWorkspaceSession === state.hydrateWorkspaceSession && + cachedStartupActions.hydrateTabsSession === state.hydrateTabsSession && + cachedStartupActions.hydrateEditorSession === state.hydrateEditorSession && + cachedStartupActions.hydrateBrowserSession === state.hydrateBrowserSession && + cachedStartupActions.fetchBrowserSessionProfiles === state.fetchBrowserSessionProfiles && + cachedStartupActions.reconnectPersistedTerminals === state.reconnectPersistedTerminals && + cachedStartupActions.setTerminalStartupRestorationReady === + state.setTerminalStartupRestorationReady && + cachedStartupActions.setDeferredSshReconnectTargets === state.setDeferredSshReconnectTargets && + cachedStartupActions.setSshConnectionState === state.setSshConnectionState && + cachedStartupActions.hydratePersistedUI === state.hydratePersistedUI && + cachedStartupActions.setHydrationSucceeded === state.setHydrationSucceeded && + cachedStartupActions.pruneLastVisitedTimestamps === state.pruneLastVisitedTimestamps && + cachedStartupActions.seedActiveWorktreeLastVisitedIfMissing === + state.seedActiveWorktreeLastVisitedIfMissing + ) { + return cachedStartupActions + } + + cachedStartupActions = { + fetchReposForAllHosts: state.fetchReposForAllHosts, + awaitLocalRepoCatalogSettlement: state.awaitLocalRepoCatalogSettlement, + fetchProjectGroupsForAllHosts: state.fetchProjectGroupsForAllHosts, + fetchFolderWorkspacesForAllHosts: state.fetchFolderWorkspacesForAllHosts, + fetchAllWorktrees: state.fetchAllWorktrees, + fetchWorktrees: state.fetchWorktrees, + fetchWorktreeLineage: state.fetchWorktreeLineage, + fetchOrcaProfiles: state.fetchOrcaProfiles, + fetchSettings: state.fetchSettings, + awaitOwnerWorktreeVisibilityDefaultsHydration: + state.awaitOwnerWorktreeVisibilityDefaultsHydration, + fetchKeybindings: state.fetchKeybindings, + initGitHubCache: state.initGitHubCache, + hydrateWorkspaceSession: state.hydrateWorkspaceSession, + hydrateTabsSession: state.hydrateTabsSession, + hydrateEditorSession: state.hydrateEditorSession, + hydrateBrowserSession: state.hydrateBrowserSession, + fetchBrowserSessionProfiles: state.fetchBrowserSessionProfiles, + reconnectPersistedTerminals: state.reconnectPersistedTerminals, + setTerminalStartupRestorationReady: state.setTerminalStartupRestorationReady, + setDeferredSshReconnectTargets: state.setDeferredSshReconnectTargets, + setSshConnectionState: state.setSshConnectionState, + hydratePersistedUI: state.hydratePersistedUI, + setHydrationSucceeded: state.setHydrationSucceeded, + pruneLastVisitedTimestamps: state.pruneLastVisitedTimestamps, + seedActiveWorktreeLastVisitedIfMissing: state.seedActiveWorktreeLastVisitedIfMissing + } + return cachedStartupActions +} + +export function resetStartupActionsSelectorCacheForTest(): void { + cachedStartupActions = null +} diff --git a/src/renderer/src/app-shell/use-app-startup-actions.ts b/src/renderer/src/app-shell/use-app-startup-actions.ts index df7cec71ab1..112a1b64df9 100644 --- a/src/renderer/src/app-shell/use-app-startup-actions.ts +++ b/src/renderer/src/app-shell/use-app-startup-actions.ts @@ -1,39 +1,9 @@ // The renderer boot chain's store subscription, kept apart from the chain itself -// so one useShallow equality check covers every startup action. +// so unrelated store publications reuse one action projection. -import { useShallow } from 'zustand/react/shallow' import { useAppStore } from '../store' +import { selectStartupActions } from './startup-actions-selector' export function useStartupActions() { - // Why: consolidate action refs into one useShallow subscription so React runs one equality check per store mutation instead of one per action. - return useAppStore( - useShallow((s) => ({ - fetchReposForAllHosts: s.fetchReposForAllHosts, - awaitLocalRepoCatalogSettlement: s.awaitLocalRepoCatalogSettlement, - fetchProjectGroupsForAllHosts: s.fetchProjectGroupsForAllHosts, - fetchFolderWorkspacesForAllHosts: s.fetchFolderWorkspacesForAllHosts, - fetchAllWorktrees: s.fetchAllWorktrees, - fetchWorktrees: s.fetchWorktrees, - fetchWorktreeLineage: s.fetchWorktreeLineage, - fetchOrcaProfiles: s.fetchOrcaProfiles, - fetchSettings: s.fetchSettings, - awaitOwnerWorktreeVisibilityDefaultsHydration: - s.awaitOwnerWorktreeVisibilityDefaultsHydration, - fetchKeybindings: s.fetchKeybindings, - initGitHubCache: s.initGitHubCache, - hydrateWorkspaceSession: s.hydrateWorkspaceSession, - hydrateTabsSession: s.hydrateTabsSession, - hydrateEditorSession: s.hydrateEditorSession, - hydrateBrowserSession: s.hydrateBrowserSession, - fetchBrowserSessionProfiles: s.fetchBrowserSessionProfiles, - reconnectPersistedTerminals: s.reconnectPersistedTerminals, - setTerminalStartupRestorationReady: s.setTerminalStartupRestorationReady, - setDeferredSshReconnectTargets: s.setDeferredSshReconnectTargets, - setSshConnectionState: s.setSshConnectionState, - hydratePersistedUI: s.hydratePersistedUI, - setHydrationSucceeded: s.setHydrationSucceeded, - pruneLastVisitedTimestamps: s.pruneLastVisitedTimestamps, - seedActiveWorktreeLastVisitedIfMissing: s.seedActiveWorktreeLastVisitedIfMissing - })) - ) + return useAppStore(selectStartupActions) }