Files
openchamber/packages/ui/src/stores/globalSessionStructure.ts
T
c_w_xiaohei 701173e399 perf(ui): index and batch session updates
Batch global session, status, ordering, and activity timing mutations at the existing directory event boundary so large subagent bursts publish each owner once.

Maintain active session roots, children, and directory buckets in the global store, reuse them in Sidebar projections, and avoid rebuilding live aggregates and structural data for unrelated renders while preserving authoritative ordering reconciliation.
2026-08-26 00:42:37 +08:00

225 lines
8.0 KiB
TypeScript

import type { Session } from '@opencode-ai/sdk/v2';
import { normalizePath } from '@/lib/pathNormalization';
export type GlobalSessionStructure = {
activeSessionIds: readonly string[];
activeRootIds: readonly string[];
activeChildrenByParentId: ReadonlyMap<string, readonly string[]>;
activeIdsByDirectory: ReadonlyMap<string, readonly string[]>;
};
export type GlobalSessionStructureMutation = {
sessionId: string;
previous: Session | null;
next: Session | null;
};
type SessionLocation = {
directory: string | null;
parentId: string | null;
};
type BucketChange = {
additions: Set<string>;
removals: Set<string>;
};
type SessionIndexFields = {
directory?: string | null;
parentID?: string | null;
project?: { worktree?: string | null } | null;
};
const indexFields = (session: Session): Session & SessionIndexFields => {
// SAFETY: OpenCode session payloads expose these stable fields even though the SDK base Session omits them.
return session as Session & SessionIndexFields;
};
const parentIdOf = (session: Session): string | null => (
indexFields(session).parentID ?? null
);
export const resolveGlobalSessionDirectory = (session: Session): string | null => {
const record = indexFields(session);
return normalizePath(record.directory ?? null)
?? normalizePath(record.project?.worktree ?? null);
};
export const mergeSessionDirectoryMetadata = (incoming: Session, existing?: Session | null): Session => {
if (!existing) return incoming;
const incomingRecord = indexFields(incoming);
const existingRecord = indexFields(existing);
const incomingDirectory = normalizePath(incomingRecord.directory ?? null);
const incomingWorktree = normalizePath(incomingRecord.project?.worktree ?? null);
const existingDirectory = normalizePath(existingRecord.directory ?? null);
const existingWorktree = normalizePath(existingRecord.project?.worktree ?? null);
let changed = false;
const next: typeof incomingRecord = { ...incomingRecord };
if (!incomingDirectory && existingDirectory) {
next.directory = existingRecord.directory;
changed = true;
}
if (!incomingWorktree && existingWorktree) {
next.project = {
...(existingRecord.project ?? {}),
...(incomingRecord.project ?? {}),
worktree: existingRecord.project?.worktree,
};
changed = true;
} else if (!incomingRecord.project && existingRecord.project) {
next.project = existingRecord.project;
changed = true;
}
return changed ? next : incoming;
};
const locationOf = (session: Session): SessionLocation | null => session.time?.archived ? null : ({
directory: resolveGlobalSessionDirectory(session),
parentId: parentIdOf(session),
});
const sameLocation = (left: SessionLocation, right: SessionLocation): boolean => (
left.directory === right.directory
&& left.parentId === right.parentId
);
const appendToBucket = (buckets: Map<string, string[]>, key: string, sessionId: string): void => {
const bucket = buckets.get(key);
if (bucket) bucket.push(sessionId);
else buckets.set(key, [sessionId]);
};
export const buildGlobalSessionStructure = (
activeSessions: readonly Session[],
): GlobalSessionStructure => {
const activeRootIds: string[] = [];
const activeChildrenByParentId = new Map<string, string[]>();
const activeIdsByDirectory = new Map<string, string[]>();
const index = (
session: Session,
roots: string[],
children: Map<string, string[]>,
directories: Map<string, string[]>,
): void => {
const location = locationOf(session);
if (!location) return;
if (location.parentId) appendToBucket(children, location.parentId, session.id);
else roots.push(session.id);
if (location.directory) appendToBucket(directories, location.directory, session.id);
};
for (const session of activeSessions) index(session, activeRootIds, activeChildrenByParentId, activeIdsByDirectory);
return {
activeSessionIds: activeSessions.map((session) => session.id),
activeRootIds,
activeChildrenByParentId,
activeIdsByDirectory,
};
};
const recordBucketChange = (
changes: Map<string, BucketChange>,
key: string,
sessionId: string,
operation: 'add' | 'remove',
): void => {
const change = changes.get(key) ?? { additions: new Set<string>(), removals: new Set<string>() };
if (operation === 'add') {
change.removals.delete(sessionId);
change.additions.delete(sessionId);
change.additions.add(sessionId);
} else {
change.additions.delete(sessionId);
change.removals.add(sessionId);
}
changes.set(key, change);
};
const applyListChange = (
source: readonly string[],
change: BucketChange,
): readonly string[] => {
if (change.additions.size === 0 && change.removals.size === 0) return source;
const additions = [...change.additions].reverse();
const added = new Set(additions);
const retained = source.filter((id) => !change.removals.has(id) && !added.has(id));
const next = [...additions, ...retained];
if (next.length === source.length && next.every((id, index) => id === source[index])) return source;
return next;
};
const applyBucketChanges = (
source: ReadonlyMap<string, readonly string[]>,
changes: Map<string, BucketChange>,
): ReadonlyMap<string, readonly string[]> => {
if (changes.size === 0) return source;
let next: Map<string, readonly string[]> | null = null;
for (const [key, change] of changes) {
const previous = source.get(key) ?? [];
const bucket = applyListChange(previous, change);
if (bucket === previous) continue;
next ??= new Map(source);
if (bucket.length === 0) next.delete(key);
else next.set(key, bucket);
}
return next ?? source;
};
export const applyGlobalSessionStructureMutations = (
structure: GlobalSessionStructure,
mutations: readonly GlobalSessionStructureMutation[],
): GlobalSessionStructure => {
const activeRoots: BucketChange = { additions: new Set(), removals: new Set() };
const activeSessions: BucketChange = { additions: new Set(), removals: new Set() };
const activeChildren = new Map<string, BucketChange>();
const activeDirectories = new Map<string, BucketChange>();
const record = (location: SessionLocation, sessionId: string, operation: 'add' | 'remove'): void => {
if (operation === 'add') {
activeSessions.removals.delete(sessionId);
activeSessions.additions.delete(sessionId);
activeSessions.additions.add(sessionId);
} else {
activeSessions.additions.delete(sessionId);
activeSessions.removals.add(sessionId);
}
if (location.parentId) recordBucketChange(activeChildren, location.parentId, sessionId, operation);
else if (operation === 'add') {
activeRoots.removals.delete(sessionId);
activeRoots.additions.delete(sessionId);
activeRoots.additions.add(sessionId);
} else {
activeRoots.additions.delete(sessionId);
activeRoots.removals.add(sessionId);
}
if (location.directory) recordBucketChange(activeDirectories, location.directory, sessionId, operation);
};
for (const mutation of mutations) {
const previousLocation = mutation.previous ? locationOf(mutation.previous) : null;
const nextLocation = mutation.next ? locationOf(mutation.next) : null;
if (previousLocation && nextLocation && sameLocation(previousLocation, nextLocation)) continue;
if (previousLocation) record(previousLocation, mutation.sessionId, 'remove');
if (nextLocation) record(nextLocation, mutation.sessionId, 'add');
}
const next: GlobalSessionStructure = {
activeSessionIds: applyListChange(structure.activeSessionIds, activeSessions),
activeRootIds: applyListChange(structure.activeRootIds, activeRoots),
activeChildrenByParentId: applyBucketChanges(structure.activeChildrenByParentId, activeChildren),
activeIdsByDirectory: applyBucketChanges(structure.activeIdsByDirectory, activeDirectories),
};
return next.activeSessionIds === structure.activeSessionIds
&& next.activeRootIds === structure.activeRootIds
&& next.activeChildrenByParentId === structure.activeChildrenByParentId
&& next.activeIdsByDirectory === structure.activeIdsByDirectory
? structure
: next;
};