Files
openchamber/packages/ui/src/stores/useTodosPersistStore.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

88 lines
3.6 KiB
TypeScript

import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import type { Todo } from '@opencode-ai/sdk/v2/client';
import { getRuntimeKey } from '@/lib/runtime-switch';
import { normalizePath } from '@/lib/pathNormalization';
import { createDeferredSafeJSONStorage } from './utils/safeStorage';
const MAX_SESSIONS = 50;
interface SessionTodosRecord {
todos: Todo[];
touchedAt: number;
}
interface TodosPersistState {
sessions: Record<string, SessionTodosRecord>;
setSessionTodos: (directory: string, sessionId: string, todos: Todo[] | undefined) => void;
getSessionTodos: (directory: string, sessionId: string) => Todo[] | undefined;
clearSessionTodos: (runtimeKey: string, directory: string, sessionId: string) => void;
}
export const getTodosPersistenceKey = (runtimeKey: string, directory: string, sessionId: string): string =>
JSON.stringify([runtimeKey, normalizePath(directory), sessionId]);
const getCurrentSessionKey = (directory: string, sessionId: string): string | null => {
if (!directory || !sessionId) return null;
return getTodosPersistenceKey(getRuntimeKey(), directory, sessionId);
};
const evictOldest = (sessions: Record<string, SessionTodosRecord>): Record<string, SessionTodosRecord> => {
const ids = Object.keys(sessions);
if (ids.length <= MAX_SESSIONS) return sessions;
const sorted = ids
.map((id) => [id, sessions[id].touchedAt] as const)
.sort((a, b) => a[1] - b[1]);
const drop = sorted.slice(0, ids.length - MAX_SESSIONS).map(([id]) => id);
const next = { ...sessions };
for (const id of drop) delete next[id];
return next;
};
export const useTodosPersistStore = create<TodosPersistState>()(
devtools(
persist(
(set, get) => ({
sessions: {},
setSessionTodos: (directory, sessionId, todos) => {
const key = getCurrentSessionKey(directory, sessionId);
if (!key) return;
set((state) => {
const next = { ...state.sessions };
if (!todos || todos.length === 0) {
if (!(key in next)) return state;
delete next[key];
return { sessions: next };
}
next[key] = { todos, touchedAt: Date.now() };
return { sessions: evictOldest(next) };
});
},
getSessionTodos: (directory, sessionId) => {
const key = getCurrentSessionKey(directory, sessionId);
return key ? get().sessions[key]?.todos : undefined;
},
clearSessionTodos: (runtimeKey, directory, sessionId) => {
if (!runtimeKey || !directory || !sessionId) return;
const key = getTodosPersistenceKey(runtimeKey, directory, sessionId);
set((state) => {
if (!(key in state.sessions)) return state;
const sessions = { ...state.sessions };
delete sessions[key];
return { sessions };
});
},
}),
{
name: 'openchamber-session-todos',
version: 2,
storage: createDeferredSafeJSONStorage(),
partialize: (state) => ({ sessions: state.sessions }),
migrate: () => ({ sessions: {} }),
},
),
{ name: 'TodosPersistStore' },
),
);