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:
@@ -80,6 +80,10 @@ Current consumers:
|
||||
- `Header.tsx`
|
||||
- agent/session activity surfaces using `useGlobalSessionStatus()` / `useAllSessionStatuses()`
|
||||
|
||||
Cross-directory selectors subscribe to the narrow child-store field they aggregate. Session aggregation listens to `state.session`; per-session status listens only to that session's `state.session_status` entry. Unrelated streaming events such as `message.part.delta` must not trigger global session/status scans.
|
||||
|
||||
Imperative cross-directory session lookups use the cached ID index from `getAllSyncSessionMap()`. The index is rebuilt only when a child store's `state.session` reference changes; permission lineage checks must reuse it instead of rebuilding a full session map per call.
|
||||
|
||||
### Mutation responsibility
|
||||
|
||||
`useGlobalSessionsStore` is not maintained by SSE directly. It is kept correct by:
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
|
||||
import { ChildStoreManager } from './child-store';
|
||||
|
||||
describe('ChildStoreManager.subscribeAllSelected', () => {
|
||||
test('ignores unrelated child-store updates', () => {
|
||||
const manager = new ChildStoreManager();
|
||||
const child = manager.ensureChild('/workspace', { bootstrap: false });
|
||||
let notifications = 0;
|
||||
const unsubscribe = manager.subscribeAllSelected((state) => state.session, () => {
|
||||
notifications += 1;
|
||||
});
|
||||
|
||||
child.setState({ session_status: { session: { type: 'busy' } } });
|
||||
expect(notifications).toBe(0);
|
||||
|
||||
child.setState({ session: [...child.getState().session] });
|
||||
expect(notifications).toBe(1);
|
||||
|
||||
unsubscribe();
|
||||
manager.disposeAll();
|
||||
});
|
||||
|
||||
test('notifies when the child-store registry changes', () => {
|
||||
const manager = new ChildStoreManager();
|
||||
let notifications = 0;
|
||||
const unsubscribe = manager.subscribeAllSelected((state) => state.session, () => {
|
||||
notifications += 1;
|
||||
});
|
||||
|
||||
manager.ensureChild('/workspace', { bootstrap: false });
|
||||
expect(notifications).toBe(1);
|
||||
|
||||
unsubscribe();
|
||||
manager.disposeAll();
|
||||
});
|
||||
});
|
||||
@@ -238,4 +238,41 @@ export class ChildStoreManager {
|
||||
storeUnsubscribers.clear()
|
||||
}
|
||||
}
|
||||
|
||||
subscribeAllSelected<T>(selector: (state: DirectoryStore) => T, listener: () => void): () => void {
|
||||
const storeUnsubscribers = new Map<string, () => void>()
|
||||
|
||||
const syncStoreSubscriptions = () => {
|
||||
const activeDirectories = new Set(this.children.keys())
|
||||
|
||||
for (const [directory, unsubscribe] of storeUnsubscribers.entries()) {
|
||||
if (activeDirectories.has(directory)) continue
|
||||
unsubscribe()
|
||||
storeUnsubscribers.delete(directory)
|
||||
}
|
||||
|
||||
for (const [directory, store] of this.children.entries()) {
|
||||
if (storeUnsubscribers.has(directory)) continue
|
||||
storeUnsubscribers.set(directory, store.subscribe((state, previous) => {
|
||||
if (!Object.is(selector(state), selector(previous))) {
|
||||
listener()
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
syncStoreSubscriptions()
|
||||
const unsubscribeRegistry = this.subscribeRegistry(() => {
|
||||
syncStoreSubscriptions()
|
||||
listener()
|
||||
})
|
||||
|
||||
return () => {
|
||||
unsubscribeRegistry()
|
||||
for (const unsubscribe of storeUnsubscribers.values()) {
|
||||
unsubscribe()
|
||||
}
|
||||
storeUnsubscribers.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +109,11 @@ function getLiveStates(childStores: ChildStoreManager): State[] {
|
||||
return Array.from(childStores.children.values(), (store) => store.getState())
|
||||
}
|
||||
|
||||
function useLiveSyncSelector<T>(selector: (states: State[]) => T, isEqual: (left: T, right: T) => boolean = Object.is): T {
|
||||
function useLiveSyncSelector<T>(
|
||||
selector: (states: State[]) => T,
|
||||
isEqual: (left: T, right: T) => boolean = Object.is,
|
||||
subscribe?: (childStores: ChildStoreManager, notify: () => void) => () => void,
|
||||
): T {
|
||||
const { childStores } = useSyncSystem()
|
||||
const cacheRef = useRef<T | undefined>(undefined)
|
||||
const initializedRef = useRef(false)
|
||||
@@ -126,7 +130,10 @@ function useLiveSyncSelector<T>(selector: (states: State[]) => T, isEqual: (left
|
||||
}, [childStores, isEqual, selector])
|
||||
|
||||
return React.useSyncExternalStore(
|
||||
useCallback((notify) => childStores.subscribeAll(notify), [childStores]),
|
||||
useCallback(
|
||||
(notify) => subscribe ? subscribe(childStores, notify) : childStores.subscribeAll(notify),
|
||||
[childStores, subscribe],
|
||||
),
|
||||
getSnapshot,
|
||||
getSnapshot,
|
||||
)
|
||||
@@ -142,6 +149,14 @@ function useLiveSyncSelector<T>(selector: (states: State[]) => T, isEqual: (left
|
||||
export function useGlobalSessionStatus(sessionId: string): SessionStatus | undefined {
|
||||
return useLiveSyncSelector(
|
||||
useCallback((states) => findLiveSessionStatus(states, sessionId), [sessionId]),
|
||||
Object.is,
|
||||
useCallback(
|
||||
(childStores: ChildStoreManager, notify: () => void) => childStores.subscribeAllSelected(
|
||||
(state: State) => state.session_status?.[sessionId],
|
||||
notify,
|
||||
),
|
||||
[sessionId],
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -150,6 +165,13 @@ export function useAllSessionStatuses(): Record<string, SessionStatus> {
|
||||
return useLiveSyncSelector(
|
||||
useCallback((states) => aggregateLiveSessionStatuses(states), []),
|
||||
areStatusMapsEquivalent,
|
||||
useCallback(
|
||||
(childStores: ChildStoreManager, notify: () => void) => childStores.subscribeAllSelected(
|
||||
(state: State) => state.session_status,
|
||||
notify,
|
||||
),
|
||||
[],
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -157,6 +179,13 @@ export function useAllLiveSessions(): Session[] {
|
||||
return useLiveSyncSelector(
|
||||
useCallback((states) => aggregateLiveSessions(states), []),
|
||||
areSessionListsEquivalent,
|
||||
useCallback(
|
||||
(childStores: ChildStoreManager, notify: () => void) => childStores.subscribeAllSelected(
|
||||
(state: State) => state.session,
|
||||
notify,
|
||||
),
|
||||
[],
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user