perf(sidebar): index session ownership and narrow live subscriptions

Replace repeated project-by-session directory matching across sidebar hooks with a shared ownership index that resolves each unique directory once and exposes direct project and folder-scope buckets.

Gate destructive folder reconciliation on authoritative session data and topology readiness, preserve last-known worktrees after discovery failures, and retain nested-project, VS Code, active/archive dedupe, and Windows drive-root semantics.

Narrow cross-directory subscriptions to session and status slices so streaming deltas no longer trigger global aggregation. Reuse a cached session ID index for permission lineage checks instead of rebuilding it on every session switch.

On the reported 15-project, 67-worktree, 14,561-session shape, ownership indexing averages 3.81 ms versus roughly 450 ms for the cache-only hotfix.

Validation: 28 targeted tests, UI type-check, UI lint, and dead-code analysis.
This commit is contained in:
Bohdan Triapitsyn
2026-07-13 23:18:12 +03:00
parent 799904f0f4
commit b36afbf5ee
19 changed files with 619 additions and 511 deletions
+36 -8
View File
@@ -14,6 +14,9 @@ let _childStores: ChildStoreManager | null = null
let _directory: string = ""
let _registerSessionDirectory: ((sessionID: string, directory: string) => void) | null = null
const configListeners = new Set<(directory: string, config: Config) => void>()
let cachedSessionManager: ChildStoreManager | null = null
let cachedSessionSlices = new Map<string, State["session"]>()
let cachedSessionsById = new Map<string, State["session"][number]>()
export function setSyncRefs(
_sdk: OpencodeClient,
@@ -22,6 +25,11 @@ export function setSyncRefs(
registerSessionDirectory?: (sessionID: string, directory: string) => void,
) {
_childStores = childStores
if (cachedSessionManager !== childStores) {
cachedSessionManager = null
cachedSessionSlices = new Map()
cachedSessionsById = new Map()
}
_directory = directory
if (registerSessionDirectory) {
_registerSessionDirectory = registerSessionDirectory
@@ -76,17 +84,37 @@ export function getSyncSessions(directory?: string) {
/** Read sessions across all initialized child stores */
export function getAllSyncSessions() {
const stores = _childStores
if (!stores) return []
return Array.from(getAllSyncSessionMap().values())
}
const deduped = new Map<string, State["session"][number]>()
for (const store of stores.children.values()) {
for (const session of store.getState().session) {
if (!session?.id) continue
deduped.set(session.id, session)
/** Read the cached cross-directory session index, rebuilding only when a session slice changes. */
export function getAllSyncSessionMap(): ReadonlyMap<string, State["session"][number]> {
const stores = _childStores
if (!stores) return cachedSessionsById
let changed = cachedSessionManager !== stores || cachedSessionSlices.size !== stores.children.size
for (const [directory, store] of stores.children) {
if (cachedSessionSlices.get(directory) !== store.getState().session) {
changed = true
break
}
}
return Array.from(deduped.values())
if (!changed) return cachedSessionsById
const nextSlices = new Map<string, State["session"]>()
const nextSessionsById = new Map<string, State["session"][number]>()
for (const [directory, store] of stores.children) {
const sessions = store.getState().session
nextSlices.set(directory, sessions)
for (const session of sessions) {
if (!session?.id) continue
nextSessionsById.set(session.id, session)
}
}
cachedSessionManager = stores
cachedSessionSlices = nextSlices
cachedSessionsById = nextSessionsById
return cachedSessionsById
}
/** Read messages for a session from current directory's child store */