Files
openchamber/packages/ui/src/stores/useSessionPinnedStore.ts
T
Bohdan Triapitsyn 85400459e9 perf: overhaul session loading, caching, and runtime isolation (#2360)
Improve OpenChamber responsiveness under large session workloads while fixing
cache, synchronization, and persistence correctness across runtimes, projects,
directories, and worktrees.

- prioritize selected and visible sessions during bootstrap and defer
  non-critical enrichment work
- reduce redundant message loading, event processing, store publication, and
  hidden sidebar work
- prevent stale session and message requests from overwriting newer
  authoritative state
- preserve existing data when authoritative fetches fail instead of treating
  failures as successful empty responses
- scope session materialization, messages, drafts, queues, todos, pins,
  permissions, folders, tabs, Git state, and pull request data by runtime and
  directory identity
- harden runtime switching, reconnect, cleanup, mutation reconciliation, and
  persisted-state ordering
- preserve live subagent Task linkage when metadata arrives after an older
  message request or while streaming parts are suspended
- coalesce overlapping tail refreshes without losing newer refresh demand
- improve cold-session loading by moving deferrable work out of the critical
  bootstrap path
- isolate URL authentication, mobile credentials, native secrets, and other
  runtime-owned state across endpoint changes
- bound long-lived caches and remove avoidable allocations from event and
  rendering hot paths
- limit virtualization to archive collections where it improves rendering
  without disrupting active sidebar layout
- stabilize session folders, pin ordering, expanded state, and persisted
  sidebar behavior
- open skill files through the same secure editor and outside-workspace grant
  flow used by file navigation, including worktree sessions
- expand regression coverage for stale completions, runtime collisions,
  reconnect behavior, persistence races, authoritative empty results, and
  subagent refresh ordering
- document the updated synchronization, cache ownership, performance, and
  runtime-isolation invariants
2026-07-21 20:52:20 +03:00

122 lines
4.7 KiB
TypeScript

import { create } from 'zustand';
import { getRuntimeKey } from '@/lib/runtime-switch';
import { normalizePath } from '@/lib/pathNormalization';
import { getDeferredSafeStorage } from './utils/safeStorage';
const STORAGE_KEY = 'oc.sessions.pinned.v2';
const LEGACY_STORAGE_KEY = 'oc.sessions.pinned';
export type SessionPinnedTarget = { directory: string; sessionId: string };
type PersistedPins = { version: 2; sessions: Record<string, number> };
type PinnedSessionState = {
ids: Set<string>;
touchedAt: Record<string, number>;
};
type SessionPinnedStore = PinnedSessionState & {
setIds: (next: Set<string> | ((prev: Set<string>) => Set<string>)) => void;
toggle: (target: SessionPinnedTarget) => void;
clearPinnedSession: (runtimeKey: string, directory: string, sessionId: string) => void;
};
const storage = getDeferredSafeStorage();
export const getPinnedSessionKey = (runtimeKey: string, directory: string, sessionId: string): string | null => {
const normalizedDirectory = normalizePath(directory);
if (!runtimeKey || !normalizedDirectory || !sessionId) return null;
return JSON.stringify([runtimeKey, normalizedDirectory, sessionId]);
};
const parsePinnedSessionKey = (key: string): [string, string, string] | null => {
try {
const parsed = JSON.parse(key) as unknown;
if (!Array.isArray(parsed) || parsed.length !== 3) return null;
const [runtimeKey, directory, sessionId] = parsed;
if (typeof runtimeKey !== 'string' || typeof directory !== 'string' || typeof sessionId !== 'string') return null;
const normalizedDirectory = normalizePath(directory);
if (!runtimeKey || !normalizedDirectory || normalizedDirectory !== directory || !sessionId) return null;
return [runtimeKey, normalizedDirectory, sessionId];
} catch {
return null;
}
};
export const isSessionPinned = (ids: Set<string>, directory: string | null | undefined, sessionId: string): boolean => {
if (!directory) return false;
const key = getPinnedSessionKey(getRuntimeKey(), directory, sessionId);
return key ? ids.has(key) : false;
};
const readPinned = (): PinnedSessionState => {
storage.removeItem(LEGACY_STORAGE_KEY);
const raw = storage.getItem(STORAGE_KEY);
if (raw === null) return { ids: new Set(), touchedAt: {} };
try {
const parsed = JSON.parse(raw) as Partial<PersistedPins>;
if (parsed.version !== 2 || !parsed.sessions || typeof parsed.sessions !== 'object') return { ids: new Set(), touchedAt: {} };
const entries = Object.entries(parsed.sessions)
.filter(([key, touchedAt]) => parsePinnedSessionKey(key) && typeof touchedAt === 'number' && Number.isFinite(touchedAt))
.sort((left, right) => right[1] - left[1]);
return { ids: new Set(entries.map(([key]) => key)), touchedAt: Object.fromEntries(entries) };
} catch {
storage.removeItem(STORAGE_KEY);
return { ids: new Set(), touchedAt: {} };
}
};
const boundPinnedState = (ids: Set<string>, touchedAt: Record<string, number>): PinnedSessionState => {
const entries = [...ids]
.filter((key) => parsePinnedSessionKey(key) !== null)
.map((key) => [key, touchedAt[key] ?? Date.now()] as const)
.sort((left, right) => right[1] - left[1]);
return {
ids: new Set(entries.map(([key]) => key)),
touchedAt: Object.fromEntries(entries),
};
};
const persistPinned = ({ ids, touchedAt }: PinnedSessionState): void => {
const sessions = Object.fromEntries([...ids].map((key) => [key, touchedAt[key] ?? Date.now()]));
storage.setItem(STORAGE_KEY, JSON.stringify({ version: 2, sessions }));
};
const initial = readPinned();
export const useSessionPinnedStore = create<SessionPinnedStore>((set, get) => ({
ids: initial.ids,
touchedAt: initial.touchedAt,
setIds: (next) => {
const current = get().ids;
const resolved = typeof next === 'function' ? next(current) : next;
if (resolved === current) return;
const pinnedState = boundPinnedState(resolved, get().touchedAt);
set(pinnedState);
persistPinned(pinnedState);
},
toggle: (target) => {
const key = getPinnedSessionKey(getRuntimeKey(), target.directory, target.sessionId);
if (!key) return;
const ids = new Set(get().ids);
const touchedAt = { ...get().touchedAt };
if (ids.has(key)) {
ids.delete(key);
delete touchedAt[key];
} else {
ids.add(key);
touchedAt[key] = Date.now();
}
const pinnedState = boundPinnedState(ids, touchedAt);
set(pinnedState);
persistPinned(pinnedState);
},
clearPinnedSession: (runtimeKey, directory, sessionId) => {
const key = getPinnedSessionKey(runtimeKey, directory, sessionId);
if (!key || !get().ids.has(key)) return;
const ids = new Set(get().ids);
ids.delete(key);
get().setIds(ids);
},
}));