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
This commit is contained in:
committed by
GitHub
parent
485efc7117
commit
85400459e9
@@ -3,6 +3,7 @@ import { persist } from 'zustand/middleware';
|
||||
import type { GitHubPullRequestStatus, RuntimeAPIs } from '@/lib/api/types';
|
||||
import { mapWithConcurrency } from '@/lib/concurrency';
|
||||
import { createDeferredSafeJSONStorage } from './utils/safeStorage';
|
||||
import { getRuntimeKey } from '@/lib/runtime-switch';
|
||||
|
||||
const PR_REVALIDATE_TTL_MS = 90_000;
|
||||
const PR_REVALIDATE_INTERVAL_MS = 15_000;
|
||||
@@ -14,6 +15,7 @@ const PR_OPEN_STABLE_INTERVAL_MS = 5 * 60_000;
|
||||
const PR_STATUS_REFRESH_CONCURRENCY = 4;
|
||||
const PR_PERSIST_TTL_MS = 12 * 60 * 60_000;
|
||||
const PR_STATUS_STORAGE_KEY = 'openchamber.github-pr-status';
|
||||
const PR_MAX_ENTRIES = 200;
|
||||
|
||||
const isTerminalPrState = (state: string | null | undefined): boolean => state === 'closed' || state === 'merged';
|
||||
const isPendingChecks = (status: GitHubPullRequestStatus | null): boolean => {
|
||||
@@ -24,9 +26,14 @@ const isPendingChecks = (status: GitHubPullRequestStatus | null): boolean => {
|
||||
return checks.state === 'pending' || checks.pending > 0;
|
||||
};
|
||||
|
||||
const getOpenPrRefreshInterval = (status: GitHubPullRequestStatus | null): number => {
|
||||
if (isPendingChecks(status)) return PR_OPEN_BUSY_INTERVAL_MS;
|
||||
if (status?.checks && status.checks.state !== 'pending') return PR_OPEN_STABLE_INTERVAL_MS;
|
||||
return PR_OPEN_DEFAULT_INTERVAL_MS;
|
||||
};
|
||||
|
||||
export const getGitHubPrStatusKey = (directory: string, branch: string, remoteName?: string | null): string => {
|
||||
void remoteName;
|
||||
return `${directory}::${branch}`;
|
||||
return JSON.stringify([getRuntimeKey(), directory, branch, remoteName ?? 'auto']);
|
||||
};
|
||||
|
||||
type RefreshOptions = {
|
||||
@@ -43,6 +50,7 @@ type PrTrackingTarget = {
|
||||
};
|
||||
|
||||
type PrRuntimeParams = {
|
||||
runtimeKey?: string;
|
||||
directory: string;
|
||||
branch: string;
|
||||
remoteName: string | null;
|
||||
@@ -53,6 +61,7 @@ type PrRuntimeParams = {
|
||||
};
|
||||
|
||||
type PrEntryIdentity = {
|
||||
runtimeKey: string;
|
||||
directory: string;
|
||||
branch: string;
|
||||
remoteName: string | null;
|
||||
@@ -69,6 +78,7 @@ type PrStatusEntry = {
|
||||
params: PrRuntimeParams | null;
|
||||
identity: PrEntryIdentity | null;
|
||||
resolvedRemoteName: string | null;
|
||||
paramsRevision: number;
|
||||
};
|
||||
|
||||
type PersistedPrStatusEntry = Pick<
|
||||
@@ -87,12 +97,14 @@ type GitHubPrStatusStore = {
|
||||
refresh: (key: string, options?: RefreshOptions) => Promise<void>;
|
||||
refreshTargets: (targets: PrTrackingTarget[], options?: RefreshOptions) => Promise<void>;
|
||||
updateStatus: (key: string, updater: (prev: GitHubPullRequestStatus | null) => GitHubPullRequestStatus | null) => void;
|
||||
resetForRuntimeSwitch: () => void;
|
||||
};
|
||||
|
||||
const timers = new Map<string, number>();
|
||||
const bootstrapTimers = new Map<string, number[]>();
|
||||
const inFlightBySignature = new Set<string>();
|
||||
const inFlightBySignature = new Map<string, symbol>();
|
||||
const lastRefreshBySignature = new Map<string, number>();
|
||||
let prRuntimeGeneration = 0;
|
||||
|
||||
// Global concurrency gate for PR-status network requests.
|
||||
//
|
||||
@@ -141,6 +153,7 @@ const createEntry = (): PrStatusEntry => ({
|
||||
params: null,
|
||||
identity: null,
|
||||
resolvedRemoteName: null,
|
||||
paramsRevision: 0,
|
||||
});
|
||||
|
||||
const getIdentityFromEntry = (entry: PrStatusEntry | null | undefined): PrEntryIdentity | null => {
|
||||
@@ -148,6 +161,7 @@ const getIdentityFromEntry = (entry: PrStatusEntry | null | undefined): PrEntryI
|
||||
return {
|
||||
directory: entry.params.directory,
|
||||
branch: entry.params.branch,
|
||||
runtimeKey: entry.params.runtimeKey ?? entry.identity?.runtimeKey ?? getRuntimeKey(),
|
||||
remoteName: entry.params.remoteName ?? entry.resolvedRemoteName ?? entry.identity?.remoteName ?? null,
|
||||
};
|
||||
}
|
||||
@@ -162,7 +176,7 @@ const getSignatureFromEntry = (entry: PrStatusEntry | null | undefined): string
|
||||
if (!identity?.directory || !identity.branch) {
|
||||
return null;
|
||||
}
|
||||
return `${identity.directory}::${identity.branch}`;
|
||||
return JSON.stringify([identity.runtimeKey, identity.directory, identity.branch, identity.remoteName ?? 'auto']);
|
||||
};
|
||||
|
||||
const getKeysBySignature = (entries: Record<string, PrStatusEntry>, signature: string): string[] => {
|
||||
@@ -172,20 +186,35 @@ const getKeysBySignature = (entries: Record<string, PrStatusEntry>, signature: s
|
||||
};
|
||||
|
||||
const mergeParams = (entry: PrStatusEntry, next: PrRuntimeParams): PrStatusEntry => {
|
||||
const runtimeKey = next.runtimeKey ?? getRuntimeKey();
|
||||
const remoteName = next.remoteName ?? entry.params?.remoteName ?? entry.resolvedRemoteName ?? entry.identity?.remoteName ?? null;
|
||||
const paramsChanged = !entry.params
|
||||
|| entry.params.runtimeKey !== runtimeKey
|
||||
|| entry.params.directory !== next.directory
|
||||
|| entry.params.branch !== next.branch
|
||||
|| entry.params.remoteName !== remoteName
|
||||
|| entry.params.canShow !== next.canShow
|
||||
|| entry.params.github !== next.github
|
||||
|| entry.params.githubAuthChecked !== next.githubAuthChecked
|
||||
|| entry.params.githubConnected !== next.githubConnected;
|
||||
return {
|
||||
...entry,
|
||||
paramsRevision: paramsChanged ? entry.paramsRevision + 1 : entry.paramsRevision,
|
||||
...(paramsChanged ? { isLoading: false, error: null } : {}),
|
||||
params: entry.params
|
||||
? {
|
||||
...entry.params,
|
||||
...next,
|
||||
runtimeKey,
|
||||
remoteName,
|
||||
}
|
||||
: {
|
||||
...next,
|
||||
runtimeKey,
|
||||
remoteName,
|
||||
},
|
||||
identity: {
|
||||
runtimeKey,
|
||||
directory: next.directory,
|
||||
branch: next.branch,
|
||||
remoteName,
|
||||
@@ -256,6 +285,20 @@ const hydrateEntry = (entry: PersistedPrStatusEntry | undefined): PrStatusEntry
|
||||
resolvedRemoteName: entry?.resolvedRemoteName ?? entry?.status?.resolvedRemoteName ?? null,
|
||||
});
|
||||
|
||||
const boundEntries = (entries: Record<string, PrStatusEntry>): Record<string, PrStatusEntry> => {
|
||||
const all = Object.entries(entries);
|
||||
if (all.length <= PR_MAX_ENTRIES) return entries;
|
||||
return Object.fromEntries(all
|
||||
.sort(([, left], [, right]) => {
|
||||
const leftProtected = left.watchers > 0 || left.isLoading;
|
||||
const rightProtected = right.watchers > 0 || right.isLoading;
|
||||
if (leftProtected !== rightProtected) return leftProtected ? -1 : 1;
|
||||
return Math.max(right.lastRefreshAt, right.lastDiscoveryPollAt)
|
||||
- Math.max(left.lastRefreshAt, left.lastDiscoveryPollAt);
|
||||
})
|
||||
.slice(0, PR_MAX_ENTRIES));
|
||||
};
|
||||
|
||||
export const useGitHubPrStatusStore = create<GitHubPrStatusStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
@@ -263,16 +306,36 @@ export const useGitHubPrStatusStore = create<GitHubPrStatusStore>()(
|
||||
activeRequestCount: 0,
|
||||
totalRequestCount: 0,
|
||||
|
||||
resetForRuntimeSwitch: () => {
|
||||
prRuntimeGeneration += 1;
|
||||
for (const timerId of timers.values()) window.clearInterval(timerId);
|
||||
for (const timerIds of bootstrapTimers.values()) timerIds.forEach((timerId) => window.clearTimeout(timerId));
|
||||
timers.clear();
|
||||
bootstrapTimers.clear();
|
||||
inFlightBySignature.clear();
|
||||
lastRefreshBySignature.clear();
|
||||
set((state) => ({
|
||||
activeRequestCount: 0,
|
||||
entries: Object.fromEntries(Object.entries(state.entries).map(([key, entry]) => [key, {
|
||||
...entry,
|
||||
watchers: 0,
|
||||
isLoading: false,
|
||||
params: null,
|
||||
paramsRevision: entry.paramsRevision + 1,
|
||||
}])),
|
||||
}));
|
||||
},
|
||||
|
||||
ensureEntry: (key) => {
|
||||
set((state) => {
|
||||
if (state.entries[key]) {
|
||||
return state;
|
||||
}
|
||||
return {
|
||||
entries: {
|
||||
entries: boundEntries({
|
||||
...state.entries,
|
||||
[key]: createEntry(),
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
},
|
||||
@@ -369,11 +432,7 @@ export const useGitHubPrStatusStore = create<GitHubPrStatusStore>()(
|
||||
}
|
||||
|
||||
const elapsed = Date.now() - entry.lastRefreshAt;
|
||||
const nextInterval = isPendingChecks(entry.status)
|
||||
? PR_OPEN_BUSY_INTERVAL_MS
|
||||
: (entry.status?.checks && entry.status.checks.state !== 'pending'
|
||||
? PR_OPEN_STABLE_INTERVAL_MS
|
||||
: PR_OPEN_DEFAULT_INTERVAL_MS);
|
||||
const nextInterval = getOpenPrRefreshInterval(entry.status);
|
||||
if (elapsed < nextInterval) {
|
||||
return;
|
||||
}
|
||||
@@ -449,8 +508,17 @@ export const useGitHubPrStatusStore = create<GitHubPrStatusStore>()(
|
||||
return;
|
||||
}
|
||||
|
||||
inFlightBySignature.add(signature);
|
||||
lastRefreshBySignature.set(signature, Date.now());
|
||||
const requestToken = Symbol(signature);
|
||||
const runtimeGeneration = prRuntimeGeneration;
|
||||
const paramsRevision = entry.paramsRevision;
|
||||
const runtimeKey = entry.params?.runtimeKey ?? entry.identity?.runtimeKey ?? getRuntimeKey();
|
||||
const isCurrent = () => (
|
||||
runtimeGeneration === prRuntimeGeneration
|
||||
&& runtimeKey === getRuntimeKey()
|
||||
&& inFlightBySignature.get(signature) === requestToken
|
||||
&& get().entries[key]?.paramsRevision === paramsRevision
|
||||
);
|
||||
inFlightBySignature.set(signature, requestToken);
|
||||
|
||||
set((prev) => {
|
||||
const nextEntries = { ...prev.entries };
|
||||
@@ -461,7 +529,6 @@ export const useGitHubPrStatusStore = create<GitHubPrStatusStore>()(
|
||||
}
|
||||
nextEntries[signatureKey] = {
|
||||
...current,
|
||||
lastRefreshAt: Date.now(),
|
||||
isLoading: options?.silent ? current.isLoading : true,
|
||||
error: null,
|
||||
};
|
||||
@@ -472,6 +539,7 @@ export const useGitHubPrStatusStore = create<GitHubPrStatusStore>()(
|
||||
});
|
||||
|
||||
if (params.githubAuthChecked && params.githubConnected === false) {
|
||||
if (!isCurrent()) return;
|
||||
set((prev) => {
|
||||
const nextEntries = { ...prev.entries };
|
||||
signatureKeys.forEach((signatureKey) => {
|
||||
@@ -491,11 +559,12 @@ export const useGitHubPrStatusStore = create<GitHubPrStatusStore>()(
|
||||
entries: nextEntries,
|
||||
};
|
||||
});
|
||||
inFlightBySignature.delete(signature);
|
||||
if (inFlightBySignature.get(signature) === requestToken) inFlightBySignature.delete(signature);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!params.github?.prStatus) {
|
||||
if (!isCurrent()) return;
|
||||
set((prev) => {
|
||||
const nextEntries = { ...prev.entries };
|
||||
signatureKeys.forEach((signatureKey) => {
|
||||
@@ -515,7 +584,7 @@ export const useGitHubPrStatusStore = create<GitHubPrStatusStore>()(
|
||||
entries: nextEntries,
|
||||
};
|
||||
});
|
||||
inFlightBySignature.delete(signature);
|
||||
if (inFlightBySignature.get(signature) === requestToken) inFlightBySignature.delete(signature);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -528,10 +597,16 @@ export const useGitHubPrStatusStore = create<GitHubPrStatusStore>()(
|
||||
await acquirePrStatusNetworkSlot();
|
||||
let next: GitHubPullRequestStatus;
|
||||
try {
|
||||
if (!isCurrent()) return;
|
||||
// Failed requests need the same non-forced cooldown as successful
|
||||
// refreshes. Record only work that reaches the network slot so a
|
||||
// stale queued request cannot suppress its replacement.
|
||||
lastRefreshBySignature.set(signature, Date.now());
|
||||
next = await params.github.prStatus(params.directory, params.branch, params.remoteName ?? undefined, { force: options?.force });
|
||||
} finally {
|
||||
releasePrStatusNetworkSlot();
|
||||
}
|
||||
if (!isCurrent()) return;
|
||||
set((prev) => {
|
||||
const nextEntries = { ...prev.entries };
|
||||
signatureKeys.forEach((signatureKey) => {
|
||||
@@ -563,6 +638,7 @@ export const useGitHubPrStatusStore = create<GitHubPrStatusStore>()(
|
||||
|
||||
const resolvedRemoteName = status.resolvedRemoteName ?? current.resolvedRemoteName ?? params.remoteName ?? null;
|
||||
const identity = getIdentityFromEntry(current) ?? {
|
||||
runtimeKey,
|
||||
directory: params.directory,
|
||||
branch: params.branch,
|
||||
remoteName: params.remoteName ?? null,
|
||||
@@ -574,6 +650,7 @@ export const useGitHubPrStatusStore = create<GitHubPrStatusStore>()(
|
||||
error: null,
|
||||
isLoading: options?.silent ? current.isLoading : false,
|
||||
isInitialStatusResolved: options?.markInitialResolved === false ? current.isInitialStatusResolved : true,
|
||||
lastRefreshAt: Date.now(),
|
||||
resolvedRemoteName,
|
||||
identity: {
|
||||
...identity,
|
||||
@@ -587,6 +664,7 @@ export const useGitHubPrStatusStore = create<GitHubPrStatusStore>()(
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
if (!isCurrent()) return;
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
set((prev) => {
|
||||
const nextEntries = { ...prev.entries };
|
||||
@@ -607,8 +685,10 @@ export const useGitHubPrStatusStore = create<GitHubPrStatusStore>()(
|
||||
};
|
||||
});
|
||||
} finally {
|
||||
inFlightBySignature.delete(signature);
|
||||
set((prev) => ({ ...prev, activeRequestCount: Math.max(0, prev.activeRequestCount - 1) }));
|
||||
if (inFlightBySignature.get(signature) === requestToken) inFlightBySignature.delete(signature);
|
||||
if (runtimeGeneration === prRuntimeGeneration) {
|
||||
set((prev) => ({ ...prev, activeRequestCount: Math.max(0, prev.activeRequestCount - 1) }));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -648,6 +728,8 @@ export const useGitHubPrStatusStore = create<GitHubPrStatusStore>()(
|
||||
{
|
||||
name: PR_STATUS_STORAGE_KEY,
|
||||
storage: createDeferredSafeJSONStorage(),
|
||||
version: 2,
|
||||
migrate: (persistedState, version) => version < 2 ? { entries: {} } : persistedState,
|
||||
partialize: (state) => ({
|
||||
entries: Object.fromEntries(
|
||||
Object.entries(state.entries)
|
||||
@@ -657,8 +739,11 @@ export const useGitHubPrStatusStore = create<GitHubPrStatusStore>()(
|
||||
return false;
|
||||
}
|
||||
const freshness = Math.max(entry.lastRefreshAt, entry.lastDiscoveryPollAt);
|
||||
return freshness === 0 || Date.now() - freshness < PR_PERSIST_TTL_MS;
|
||||
return freshness > 0 && Date.now() - freshness < PR_PERSIST_TTL_MS;
|
||||
})
|
||||
.sort(([, left], [, right]) => Math.max(right.lastRefreshAt, right.lastDiscoveryPollAt)
|
||||
- Math.max(left.lastRefreshAt, left.lastDiscoveryPollAt))
|
||||
.slice(0, PR_MAX_ENTRIES)
|
||||
.map(([key, entry]) => [key, toPersistedEntry(entry)]),
|
||||
),
|
||||
}),
|
||||
@@ -668,7 +753,14 @@ export const useGitHubPrStatusStore = create<GitHubPrStatusStore>()(
|
||||
return {
|
||||
...current,
|
||||
entries: Object.fromEntries(
|
||||
Object.entries(persistedEntries).map(([key, entry]) => [key, hydrateEntry(entry)]),
|
||||
Object.entries(persistedEntries)
|
||||
.filter(([, entry]) => Boolean(
|
||||
entry.identity?.runtimeKey
|
||||
&& Math.max(entry.lastRefreshAt, entry.lastDiscoveryPollAt) > 0
|
||||
&& Date.now() - Math.max(entry.lastRefreshAt, entry.lastDiscoveryPollAt) < PR_PERSIST_TTL_MS,
|
||||
))
|
||||
.slice(0, PR_MAX_ENTRIES)
|
||||
.map(([key, entry]) => [key, hydrateEntry(entry)]),
|
||||
),
|
||||
};
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user