perf(store): keep the repo list's identity through workspace hydration

buildRuntimeSessionPlaceholders opened with `repos.slice()`, so every workspace
session hydration handed the store a brand-new `repos` array — including the
common case where the session referenced no unknown runtime workspace and the
contents were identical. `repos` is selected whole at 46 sites, so each
hydration rerendered all of them for no data change.

The appends below already build a new array rather than mutating, and the
sibling `nextWorktreesByRepo` in the same function was already copy-on-write;
this just gives `repos` the same treatment. No consumer of the returned array
mutates it in place.
This commit is contained in:
Neil
2026-09-06 02:25:02 -07:00
parent 3d48d3a481
commit fa6cf08df1
2 changed files with 90 additions and 2 deletions
@@ -0,0 +1,83 @@
import { describe, expect, it } from 'vitest'
import type { Repo } from '../../../../shared/repo-types'
import type { Worktree } from '../../../../shared/worktree/types'
import { DEFAULT_REPO_BADGE_COLOR } from '../../../../shared/constants'
import { buildRuntimeSessionPlaceholders } from './workspace-terminal-placeholders'
const repo: Repo = {
id: 'repo-1',
path: '/repos/one',
displayName: 'one',
badgeColor: DEFAULT_REPO_BADGE_COLOR,
addedAt: 0,
connectionId: null,
executionHostId: 'local'
}
const worktree: Worktree = {
id: 'repo-1::/repos/one',
repoId: 'repo-1',
hostId: 'local',
displayName: 'main',
comment: '',
linkedIssue: null,
linkedPR: null,
linkedLinearIssue: null,
linkedGitLabMR: null,
linkedGitLabIssue: null,
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 0,
path: '/repos/one',
head: '',
branch: '',
isBare: false,
isMainWorktree: true
}
describe('buildRuntimeSessionPlaceholders', () => {
it('returns the original repos array when no placeholder repo is needed', () => {
const repos = [repo]
const worktreesByRepo = { 'repo-1': [worktree] }
const result = buildRuntimeSessionPlaceholders({
repos,
runtimeHostIdByWorkspaceSessionKey: {},
worktreesByRepo
})
// Hydration writes these straight to the store; a fresh array would rerender
// every component selecting the whole repo list for no data change.
expect(result.repos).toBe(repos)
expect(result.worktreesByRepo).toBe(worktreesByRepo)
})
it('keeps the original repos array when the session only references known repos', () => {
const repos = [repo]
const result = buildRuntimeSessionPlaceholders({
repos,
runtimeHostIdByWorkspaceSessionKey: { 'repo-1::/repos/one': 'runtime:host-1' },
worktreesByRepo: { 'repo-1': [worktree] }
})
expect(result.repos).toBe(repos)
})
it('still appends a placeholder repo for an unknown runtime workspace', () => {
const repos = [repo]
const result = buildRuntimeSessionPlaceholders({
repos,
runtimeHostIdByWorkspaceSessionKey: { 'repo-2::/repos/two': 'runtime:host-1' },
worktreesByRepo: { 'repo-1': [worktree] }
})
expect(result.repos).not.toBe(repos)
expect(result.repos.map((entry) => entry.id)).toEqual(['repo-1', 'repo-2'])
// The caller's array must not be mutated in place.
expect(repos).toHaveLength(1)
})
})
@@ -20,10 +20,15 @@ export function buildRuntimeSessionPlaceholders({
runtimeHostIdByWorkspaceSessionKey: Record<string, ExecutionHostId>
worktreesByRepo: Record<string, Worktree[]>
}): {
repos: Repo[]
repos: readonly Repo[]
worktreesByRepo: Record<string, Worktree[]>
} {
let nextRepos = repos.slice()
// Why not `repos.slice()`: hydration writes this array straight back to the store, and
// sessions that add no placeholder repo are the common case. Copying unconditionally
// gave `repos` a new identity on every workspace hydration, rerendering every component
// that selects the whole array. Appends below already build a new array, as
// nextWorktreesByRepo does for its own copy-on-write.
let nextRepos: readonly Repo[] = repos
let nextWorktreesByRepo = worktreesByRepo
for (const workspaceSessionKey of Object.keys(runtimeHostIdByWorkspaceSessionKey)) {
const hostId = runtimeHostIdByWorkspaceSessionKey[workspaceSessionKey]