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,9 @@ import type { DirState, State } from "./types"
|
||||
import { INITIAL_STATE, MAX_DIR_STORES, DIR_IDLE_TTL_MS } from "./types"
|
||||
import { pickDirectoriesToEvict, canDisposeDirectory, hasPendingBlockingRequests } from "./eviction"
|
||||
import { readDirCache, persistVcs, persistProjectMeta, persistIcon, persistSessions } from "./persist-cache"
|
||||
import { normalizePath } from "@/lib/pathNormalization"
|
||||
import { startSessionLoadPerformanceEvent } from "./session-load-performance"
|
||||
import { countSyncPerformance } from "./performance-diagnostics"
|
||||
|
||||
export type DirectoryStore = State & {
|
||||
/** Apply a partial state update */
|
||||
@@ -11,13 +14,210 @@ export type DirectoryStore = State & {
|
||||
replace: (next: State) => void
|
||||
}
|
||||
|
||||
type PermissionSubscriber = () => void
|
||||
const permissionSubscribersByStore = new WeakMap<StoreApi<DirectoryStore>, Map<string, Set<PermissionSubscriber>>>()
|
||||
|
||||
type SessionMessageChange = {
|
||||
messagesChanged: boolean
|
||||
reset: boolean
|
||||
partMessageIDs: readonly string[]
|
||||
}
|
||||
|
||||
type SessionMessageSubscriber = (change: SessionMessageChange) => void
|
||||
const messageSubscribersByStore = new WeakMap<StoreApi<DirectoryStore>, Map<string, Set<SessionMessageSubscriber>>>()
|
||||
const pendingPartChangesByStore = new WeakMap<StoreApi<DirectoryStore>, Map<string, Set<string>>>()
|
||||
|
||||
export function subscribeDirectorySessionMessages(
|
||||
store: StoreApi<DirectoryStore>,
|
||||
sessionID: string,
|
||||
listener: SessionMessageSubscriber,
|
||||
): () => void {
|
||||
let bySession = messageSubscribersByStore.get(store)
|
||||
if (!bySession) {
|
||||
bySession = new Map()
|
||||
messageSubscribersByStore.set(store, bySession)
|
||||
}
|
||||
let listeners = bySession.get(sessionID)
|
||||
if (!listeners) {
|
||||
listeners = new Set()
|
||||
bySession.set(sessionID, listeners)
|
||||
}
|
||||
listeners.add(listener)
|
||||
return () => {
|
||||
listeners?.delete(listener)
|
||||
if (listeners?.size === 0) bySession?.delete(sessionID)
|
||||
if (bySession?.size === 0) messageSubscribersByStore.delete(store)
|
||||
}
|
||||
}
|
||||
|
||||
export function markDirectorySessionPartChanged(
|
||||
store: StoreApi<DirectoryStore>,
|
||||
sessionID: string,
|
||||
messageID: string,
|
||||
): void {
|
||||
if (!sessionID || !messageID) return
|
||||
let bySession = pendingPartChangesByStore.get(store)
|
||||
if (!bySession) {
|
||||
bySession = new Map()
|
||||
pendingPartChangesByStore.set(store, bySession)
|
||||
}
|
||||
let messageIDs = bySession.get(sessionID)
|
||||
if (!messageIDs) {
|
||||
messageIDs = new Set()
|
||||
bySession.set(sessionID, messageIDs)
|
||||
}
|
||||
messageIDs.add(messageID)
|
||||
}
|
||||
|
||||
export function subscribeDirectoryPermission(
|
||||
store: StoreApi<DirectoryStore>,
|
||||
sessionID: string,
|
||||
listener: PermissionSubscriber,
|
||||
): () => void {
|
||||
let bySession = permissionSubscribersByStore.get(store)
|
||||
if (!bySession) {
|
||||
bySession = new Map()
|
||||
permissionSubscribersByStore.set(store, bySession)
|
||||
}
|
||||
let listeners = bySession.get(sessionID)
|
||||
if (!listeners) {
|
||||
listeners = new Set()
|
||||
bySession.set(sessionID, listeners)
|
||||
}
|
||||
listeners.add(listener)
|
||||
return () => {
|
||||
listeners?.delete(listener)
|
||||
if (listeners?.size === 0) bySession?.delete(sessionID)
|
||||
if (bySession?.size === 0) permissionSubscribersByStore.delete(store)
|
||||
}
|
||||
}
|
||||
|
||||
const notifyChangedPermissions = (
|
||||
store: StoreApi<DirectoryStore>,
|
||||
current: State["permission"],
|
||||
previous: State["permission"],
|
||||
): void => {
|
||||
if (current === previous) return
|
||||
const subscribers = permissionSubscribersByStore.get(store)
|
||||
if (!subscribers || subscribers.size === 0) return
|
||||
for (const [sessionID, listeners] of subscribers) {
|
||||
if (current[sessionID] === previous[sessionID]) continue
|
||||
for (const listener of listeners) {
|
||||
countSyncPerformance("permissionChangeCallbacks")
|
||||
listener()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const notifyChangedSessionMessages = (
|
||||
store: StoreApi<DirectoryStore>,
|
||||
current: State,
|
||||
previous: State,
|
||||
): void => {
|
||||
const subscribers = messageSubscribersByStore.get(store)
|
||||
const pendingParts = pendingPartChangesByStore.get(store)
|
||||
pendingPartChangesByStore.delete(store)
|
||||
if (!subscribers || subscribers.size === 0) return
|
||||
|
||||
const notifications = new Map<string, SessionMessageChange>()
|
||||
if (current.message !== previous.message) {
|
||||
for (const sessionID of subscribers.keys()) {
|
||||
if (current.message[sessionID] === previous.message[sessionID]) continue
|
||||
notifications.set(sessionID, { messagesChanged: true, reset: false, partMessageIDs: [] })
|
||||
}
|
||||
}
|
||||
|
||||
if (current.part !== previous.part) {
|
||||
if (pendingParts && pendingParts.size > 0) {
|
||||
for (const [sessionID, messageIDs] of pendingParts) {
|
||||
if (!subscribers.has(sessionID)) continue
|
||||
const existing = notifications.get(sessionID)
|
||||
notifications.set(sessionID, {
|
||||
messagesChanged: existing?.messagesChanged ?? false,
|
||||
reset: existing?.reset ?? false,
|
||||
partMessageIDs: [...messageIDs],
|
||||
})
|
||||
}
|
||||
} else {
|
||||
for (const sessionID of subscribers.keys()) {
|
||||
const existing = notifications.get(sessionID)
|
||||
notifications.set(sessionID, {
|
||||
messagesChanged: existing?.messagesChanged ?? false,
|
||||
reset: true,
|
||||
partMessageIDs: existing?.partMessageIDs ?? [],
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [sessionID, change] of notifications) {
|
||||
const listeners = subscribers.get(sessionID)
|
||||
if (!listeners) continue
|
||||
for (const listener of listeners) {
|
||||
countSyncPerformance("sessionMessageChangeCallbacks")
|
||||
listener(change)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type DirectoryBootstrapPriority = "selected" | "active-project" | "expanded" | "visible" | "background"
|
||||
|
||||
export type DirectoryBootstrapReason =
|
||||
| "current-directory"
|
||||
| "selected-session"
|
||||
| "known-project"
|
||||
| "known-worktree"
|
||||
| "project-expanded"
|
||||
| "worktree-expanded"
|
||||
| "server-connected"
|
||||
| "action-demand"
|
||||
|
||||
export type DirectoryBootstrapDemand = {
|
||||
directory: string
|
||||
priority: DirectoryBootstrapPriority
|
||||
reason: DirectoryBootstrapReason
|
||||
force?: boolean
|
||||
}
|
||||
|
||||
export type DirectoryBootstrapState = "queued" | "running" | "complete" | "failed"
|
||||
|
||||
export type DirectoryBootstrapContext = DirectoryBootstrapDemand & {
|
||||
generation: number
|
||||
isCurrent: () => boolean
|
||||
}
|
||||
|
||||
const BOOTSTRAP_PRIORITY: Record<DirectoryBootstrapPriority, number> = {
|
||||
selected: 0,
|
||||
"active-project": 1,
|
||||
expanded: 2,
|
||||
visible: 3,
|
||||
background: 4,
|
||||
}
|
||||
|
||||
type QueuedBootstrap = DirectoryBootstrapDemand & {
|
||||
sequence: number
|
||||
enqueuedAt: number
|
||||
}
|
||||
|
||||
type RunningBootstrap = QueuedBootstrap & {
|
||||
generation: number
|
||||
token: object
|
||||
manualDemandRevision?: number
|
||||
rerunRequested?: boolean
|
||||
}
|
||||
|
||||
type ManualBootstrapDemand = {
|
||||
demand: DirectoryBootstrapDemand
|
||||
revision: number
|
||||
}
|
||||
|
||||
function createDirectoryStore(directory: string): StoreApi<DirectoryStore> {
|
||||
// Restore cached metadata from localStorage
|
||||
const cached = readDirCache(directory)
|
||||
|
||||
// Stale-while-revalidate: seed the session list from cache so the sidebar
|
||||
// paints chats instantly. Bootstrap phase-3 loadSessions overwrites with the
|
||||
// fresh list (its empty-list race guard preserves these until then).
|
||||
// paints chats instantly. Bootstrap replaces it with the authoritative list
|
||||
// while preserving session mutations that happened after the request began.
|
||||
const cachedSessions = cached.sessions ?? INITIAL_STATE.session
|
||||
|
||||
const store = create<DirectoryStore>()((set) => ({
|
||||
@@ -27,6 +227,7 @@ function createDirectoryStore(directory: string): StoreApi<DirectoryStore> {
|
||||
icon: cached.icon ?? INITIAL_STATE.icon,
|
||||
session: cachedSessions,
|
||||
sessionTotal: cachedSessions.length,
|
||||
sessionListSource: cachedSessions.length > 0 ? "persisted" : "empty",
|
||||
limit: Math.max(cachedSessions.length, INITIAL_STATE.limit),
|
||||
patch: (partial) => set(partial),
|
||||
replace: (next) => set(next),
|
||||
@@ -38,6 +239,8 @@ function createDirectoryStore(directory: string): StoreApi<DirectoryStore> {
|
||||
if (state.projectMeta !== prev.projectMeta) persistProjectMeta(directory, state.projectMeta)
|
||||
if (state.icon !== prev.icon) persistIcon(directory, state.icon)
|
||||
if (state.session !== prev.session) persistSessions(directory, state.session)
|
||||
notifyChangedPermissions(store, state.permission, prev.permission)
|
||||
notifyChangedSessionMessages(store, state, prev)
|
||||
})
|
||||
|
||||
return store
|
||||
@@ -49,11 +252,22 @@ export class ChildStoreManager {
|
||||
private readonly pins = new Map<string, number>()
|
||||
private readonly disposers = new Map<string, () => void>()
|
||||
private readonly registrySubscribers = new Set<() => void>()
|
||||
private readonly bootstrapSubscribers = new Set<() => void>()
|
||||
private readonly bootstrapDemandsByOwner = new Map<string, Map<string, DirectoryBootstrapDemand>>()
|
||||
private readonly manualBootstrapDemands = new Map<string, ManualBootstrapDemand>()
|
||||
private readonly bootstrapQueue = new Map<string, QueuedBootstrap>()
|
||||
private readonly runningBootstraps = new Map<string, RunningBootstrap>()
|
||||
private readonly bootstrapStates = new Map<string, DirectoryBootstrapState>()
|
||||
|
||||
private onBootstrap?: (directory: string) => void
|
||||
private onBootstrap?: (context: DirectoryBootstrapContext) => Promise<void> | void
|
||||
private onDispose?: (directory: string) => void
|
||||
private isBooting?: (directory: string) => boolean
|
||||
private isLoadingSessions?: (directory: string) => boolean
|
||||
private bootstrapConcurrency = 2
|
||||
private bootstrapGeneration = 0
|
||||
private bootstrapSequence = 0
|
||||
private manualBootstrapDemandRevision = 0
|
||||
private disposed = false
|
||||
|
||||
private notifyRegistrySubscribers() {
|
||||
for (const subscriber of this.registrySubscribers) {
|
||||
@@ -61,16 +275,34 @@ export class ChildStoreManager {
|
||||
}
|
||||
}
|
||||
|
||||
private notifyBootstrapSubscribers() {
|
||||
for (const subscriber of this.bootstrapSubscribers) subscriber()
|
||||
}
|
||||
|
||||
configure(callbacks: {
|
||||
onBootstrap?: (directory: string) => void
|
||||
onBootstrap?: (context: DirectoryBootstrapContext) => Promise<void> | void
|
||||
onDispose?: (directory: string) => void
|
||||
isBooting?: (directory: string) => boolean
|
||||
isLoadingSessions?: (directory: string) => boolean
|
||||
}) {
|
||||
bootstrapConcurrency?: number
|
||||
}): () => void {
|
||||
const generation = ++this.bootstrapGeneration
|
||||
this.disposed = false
|
||||
this.onBootstrap = callbacks.onBootstrap
|
||||
this.onDispose = callbacks.onDispose
|
||||
this.isBooting = callbacks.isBooting
|
||||
this.isLoadingSessions = callbacks.isLoadingSessions
|
||||
this.bootstrapConcurrency = Math.max(1, Math.floor(callbacks.bootstrapConcurrency ?? 2))
|
||||
this.pumpBootstrapQueue()
|
||||
|
||||
return () => {
|
||||
if (this.bootstrapGeneration !== generation) return
|
||||
this.bootstrapGeneration += 1
|
||||
this.onBootstrap = undefined
|
||||
this.onDispose = undefined
|
||||
this.isBooting = undefined
|
||||
this.isLoadingSessions = undefined
|
||||
}
|
||||
}
|
||||
|
||||
mark(directory: string) {
|
||||
@@ -80,48 +312,273 @@ export class ChildStoreManager {
|
||||
}
|
||||
|
||||
pin(directory: string) {
|
||||
if (!directory) return
|
||||
this.pins.set(directory, (this.pins.get(directory) ?? 0) + 1)
|
||||
this.mark(directory)
|
||||
const normalizedDirectory = normalizePath(directory)
|
||||
if (!normalizedDirectory) return
|
||||
this.pins.set(normalizedDirectory, (this.pins.get(normalizedDirectory) ?? 0) + 1)
|
||||
this.mark(normalizedDirectory)
|
||||
}
|
||||
|
||||
unpin(directory: string) {
|
||||
if (!directory) return
|
||||
const next = (this.pins.get(directory) ?? 0) - 1
|
||||
const normalizedDirectory = normalizePath(directory)
|
||||
if (!normalizedDirectory) return
|
||||
const next = (this.pins.get(normalizedDirectory) ?? 0) - 1
|
||||
if (next > 0) {
|
||||
this.pins.set(directory, next)
|
||||
this.pins.set(normalizedDirectory, next)
|
||||
return
|
||||
}
|
||||
this.pins.delete(directory)
|
||||
this.pins.delete(normalizedDirectory)
|
||||
this.runEviction()
|
||||
}
|
||||
|
||||
pinned(directory: string) {
|
||||
return (this.pins.get(directory) ?? 0) > 0
|
||||
const normalizedDirectory = normalizePath(directory)
|
||||
return normalizedDirectory ? (this.pins.get(normalizedDirectory) ?? 0) > 0 : false
|
||||
}
|
||||
|
||||
ensureChild(directory: string, options?: { bootstrap?: boolean }): StoreApi<DirectoryStore> {
|
||||
if (!directory) throw new Error("No directory provided to ensureChild")
|
||||
ensureChild(
|
||||
directory: string,
|
||||
options?: {
|
||||
bootstrap?: boolean
|
||||
priority?: DirectoryBootstrapPriority
|
||||
reason?: DirectoryBootstrapReason
|
||||
},
|
||||
): StoreApi<DirectoryStore> {
|
||||
const normalizedDirectory = normalizePath(directory)
|
||||
if (!normalizedDirectory) throw new Error("No directory provided to ensureChild")
|
||||
|
||||
let store = this.children.get(directory)
|
||||
let store = this.children.get(normalizedDirectory)
|
||||
if (!store) {
|
||||
store = createDirectoryStore(directory)
|
||||
this.children.set(directory, store)
|
||||
store = createDirectoryStore(normalizedDirectory)
|
||||
this.children.set(normalizedDirectory, store)
|
||||
this.notifyRegistrySubscribers()
|
||||
}
|
||||
|
||||
this.mark(directory)
|
||||
this.mark(normalizedDirectory)
|
||||
|
||||
const shouldBootstrap = options?.bootstrap ?? true
|
||||
if (shouldBootstrap && store.getState().status === "loading") {
|
||||
this.onBootstrap?.(directory)
|
||||
this.requestBootstrap({
|
||||
directory: normalizedDirectory,
|
||||
priority: options?.priority ?? "selected",
|
||||
reason: options?.reason ?? "action-demand",
|
||||
})
|
||||
}
|
||||
|
||||
return store
|
||||
}
|
||||
|
||||
getChild(directory: string): StoreApi<DirectoryStore> | undefined {
|
||||
return this.children.get(directory)
|
||||
const normalizedDirectory = normalizePath(directory)
|
||||
return normalizedDirectory ? this.children.get(normalizedDirectory) : undefined
|
||||
}
|
||||
|
||||
requestBootstrap(demand: DirectoryBootstrapDemand): void {
|
||||
const normalizedDirectory = normalizePath(demand.directory)
|
||||
if (!normalizedDirectory || this.disposed) return
|
||||
const normalizedDemand = { ...demand, directory: normalizedDirectory }
|
||||
const existingDemand = this.manualBootstrapDemands.get(normalizedDirectory)?.demand
|
||||
const nextDemand = existingDemand
|
||||
? {
|
||||
...existingDemand,
|
||||
...normalizedDemand,
|
||||
priority: BOOTSTRAP_PRIORITY[normalizedDemand.priority] < BOOTSTRAP_PRIORITY[existingDemand.priority]
|
||||
? normalizedDemand.priority
|
||||
: existingDemand.priority,
|
||||
force: Boolean(existingDemand.force || normalizedDemand.force),
|
||||
}
|
||||
: normalizedDemand
|
||||
this.manualBootstrapDemands.set(normalizedDirectory, {
|
||||
demand: nextDemand,
|
||||
revision: ++this.manualBootstrapDemandRevision,
|
||||
})
|
||||
this.ensureChild(normalizedDirectory, { bootstrap: false })
|
||||
this.queueBootstrap(nextDemand)
|
||||
}
|
||||
|
||||
setBootstrapDemand(owner: string, demands: DirectoryBootstrapDemand[]): void {
|
||||
if (!owner || this.disposed) return
|
||||
const next = new Map<string, DirectoryBootstrapDemand>()
|
||||
for (const demand of demands) {
|
||||
const directory = normalizePath(demand.directory)
|
||||
if (!directory) continue
|
||||
const normalized = { ...demand, directory }
|
||||
const existing = next.get(directory)
|
||||
if (!existing || BOOTSTRAP_PRIORITY[normalized.priority] < BOOTSTRAP_PRIORITY[existing.priority]) {
|
||||
next.set(directory, normalized)
|
||||
}
|
||||
}
|
||||
this.bootstrapDemandsByOwner.set(owner, next)
|
||||
this.reconcileBootstrapQueue()
|
||||
}
|
||||
|
||||
clearBootstrapDemand(owner: string): void {
|
||||
if (!this.bootstrapDemandsByOwner.delete(owner)) return
|
||||
this.reconcileBootstrapQueue()
|
||||
}
|
||||
|
||||
getBootstrapState(directory: string): DirectoryBootstrapState | undefined {
|
||||
const normalizedDirectory = normalizePath(directory)
|
||||
return normalizedDirectory ? this.bootstrapStates.get(normalizedDirectory) : undefined
|
||||
}
|
||||
|
||||
subscribeBootstrap(listener: () => void): () => void {
|
||||
this.bootstrapSubscribers.add(listener)
|
||||
return () => this.bootstrapSubscribers.delete(listener)
|
||||
}
|
||||
|
||||
private aggregateBootstrapDemand(directory: string): DirectoryBootstrapDemand | undefined {
|
||||
let result = this.manualBootstrapDemands.get(directory)?.demand
|
||||
for (const demands of this.bootstrapDemandsByOwner.values()) {
|
||||
const demand = demands.get(directory)
|
||||
if (!demand) continue
|
||||
if (!result || BOOTSTRAP_PRIORITY[demand.priority] < BOOTSTRAP_PRIORITY[result.priority]) result = demand
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private reconcileBootstrapQueue(): void {
|
||||
const directories = new Set<string>()
|
||||
for (const demands of this.bootstrapDemandsByOwner.values()) {
|
||||
for (const directory of demands.keys()) directories.add(directory)
|
||||
}
|
||||
for (const directory of this.manualBootstrapDemands.keys()) directories.add(directory)
|
||||
|
||||
let changed = false
|
||||
for (const [directory] of this.bootstrapQueue) {
|
||||
if (directories.has(directory)) continue
|
||||
this.bootstrapQueue.delete(directory)
|
||||
if (this.bootstrapStates.get(directory) === "queued") this.bootstrapStates.delete(directory)
|
||||
changed = true
|
||||
}
|
||||
for (const directory of directories) {
|
||||
const demand = this.aggregateBootstrapDemand(directory)
|
||||
if (!demand) continue
|
||||
this.ensureChild(directory, { bootstrap: false })
|
||||
changed = this.queueBootstrap(demand, false) || changed
|
||||
}
|
||||
if (changed) this.notifyBootstrapSubscribers()
|
||||
this.pumpBootstrapQueue()
|
||||
}
|
||||
|
||||
private queueBootstrap(demand: DirectoryBootstrapDemand, notify = true): boolean {
|
||||
const directory = demand.directory
|
||||
const store = this.children.get(directory)
|
||||
const state = this.bootstrapStates.get(directory)
|
||||
if (!demand.force && (state === "complete" || state === "failed" || store?.getState().status === "complete")) {
|
||||
return false
|
||||
}
|
||||
const running = this.runningBootstraps.get(directory)
|
||||
if (running) {
|
||||
if (demand.force) running.rerunRequested = true
|
||||
return false
|
||||
}
|
||||
const existing = this.bootstrapQueue.get(directory)
|
||||
const next: QueuedBootstrap = existing
|
||||
? {
|
||||
...existing,
|
||||
...demand,
|
||||
priority: BOOTSTRAP_PRIORITY[demand.priority] < BOOTSTRAP_PRIORITY[existing.priority]
|
||||
? demand.priority
|
||||
: existing.priority,
|
||||
force: Boolean(existing.force || demand.force),
|
||||
}
|
||||
: { ...demand, sequence: ++this.bootstrapSequence, enqueuedAt: Date.now() }
|
||||
const changed = !existing
|
||||
|| next.priority !== existing.priority
|
||||
|| next.reason !== existing.reason
|
||||
|| next.force !== existing.force
|
||||
this.bootstrapQueue.set(directory, next)
|
||||
this.bootstrapStates.set(directory, "queued")
|
||||
if (changed && notify) this.notifyBootstrapSubscribers()
|
||||
this.pumpBootstrapQueue()
|
||||
return changed
|
||||
}
|
||||
|
||||
private nextBootstrap(): QueuedBootstrap | undefined {
|
||||
const candidates = [...this.bootstrapQueue.values()].sort((left, right) => {
|
||||
const priority = BOOTSTRAP_PRIORITY[left.priority] - BOOTSTRAP_PRIORITY[right.priority]
|
||||
return priority !== 0 ? priority : left.sequence - right.sequence
|
||||
})
|
||||
const lowPriorityRunning = [...this.runningBootstraps.values()].filter(
|
||||
(entry) => BOOTSTRAP_PRIORITY[entry.priority] >= BOOTSTRAP_PRIORITY.visible,
|
||||
).length
|
||||
return candidates.find((entry) => (
|
||||
BOOTSTRAP_PRIORITY[entry.priority] < BOOTSTRAP_PRIORITY.visible || lowPriorityRunning === 0
|
||||
))
|
||||
}
|
||||
|
||||
private pumpBootstrapQueue(): void {
|
||||
if (!this.onBootstrap || this.disposed) return
|
||||
while (this.runningBootstraps.size < this.bootstrapConcurrency) {
|
||||
const next = this.nextBootstrap()
|
||||
if (!next) return
|
||||
this.bootstrapQueue.delete(next.directory)
|
||||
const token = {}
|
||||
const running: RunningBootstrap = {
|
||||
...next,
|
||||
generation: this.bootstrapGeneration,
|
||||
token,
|
||||
manualDemandRevision: this.manualBootstrapDemands.get(next.directory)?.revision,
|
||||
}
|
||||
this.runningBootstraps.set(next.directory, running)
|
||||
this.bootstrapStates.set(next.directory, "running")
|
||||
this.notifyBootstrapSubscribers()
|
||||
const finishPerformanceEvent = startSessionLoadPerformanceEvent({
|
||||
operation: "bootstrap.directory",
|
||||
directory: next.directory,
|
||||
caller: next.reason,
|
||||
queuedMs: Math.max(0, Date.now() - next.enqueuedAt),
|
||||
})
|
||||
|
||||
const isCurrent = () => (
|
||||
!this.disposed
|
||||
&& this.bootstrapGeneration === running.generation
|
||||
&& this.runningBootstraps.get(next.directory)?.token === token
|
||||
&& this.children.has(next.directory)
|
||||
)
|
||||
let bootstrapPromise: Promise<void>
|
||||
try {
|
||||
bootstrapPromise = Promise.resolve(this.onBootstrap({ ...next, generation: running.generation, isCurrent }))
|
||||
} catch (error) {
|
||||
bootstrapPromise = Promise.reject(error)
|
||||
}
|
||||
void bootstrapPromise
|
||||
.then(() => {
|
||||
if (isCurrent()) {
|
||||
this.bootstrapStates.set(next.directory, "complete")
|
||||
finishPerformanceEvent("complete")
|
||||
} else {
|
||||
finishPerformanceEvent("stale")
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (isCurrent()) {
|
||||
this.bootstrapStates.set(next.directory, "failed")
|
||||
finishPerformanceEvent("error")
|
||||
} else {
|
||||
finishPerformanceEvent("stale")
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
const executionBecameStale = this.bootstrapGeneration !== running.generation || this.disposed
|
||||
if (this.runningBootstraps.get(next.directory)?.token === token) {
|
||||
this.runningBootstraps.delete(next.directory)
|
||||
}
|
||||
const currentManualDemand = this.manualBootstrapDemands.get(next.directory)
|
||||
const hasNewForcedManualDemand = currentManualDemand !== undefined
|
||||
&& currentManualDemand.revision !== running.manualDemandRevision
|
||||
&& currentManualDemand.demand.force === true
|
||||
if (!executionBecameStale && !hasNewForcedManualDemand) {
|
||||
this.manualBootstrapDemands.delete(next.directory)
|
||||
}
|
||||
if ((executionBecameStale || hasNewForcedManualDemand || running.rerunRequested) && !this.disposed) {
|
||||
const demand = this.aggregateBootstrapDemand(next.directory)
|
||||
if (demand) this.queueBootstrap({ ...demand, force: true }, false)
|
||||
}
|
||||
this.notifyBootstrapSubscribers()
|
||||
this.pumpBootstrapQueue()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
disposeDirectory(directory: string): boolean {
|
||||
@@ -130,7 +587,9 @@ export class ChildStoreManager {
|
||||
directory,
|
||||
hasStore: this.children.has(directory),
|
||||
pinned: this.pinned(directory),
|
||||
booting: this.isBooting?.(directory) ?? false,
|
||||
booting: this.bootstrapStates.get(directory) === "queued"
|
||||
|| this.bootstrapStates.get(directory) === "running"
|
||||
|| (this.isBooting?.(directory) ?? false),
|
||||
loadingSessions: this.isLoadingSessions?.(directory) ?? false,
|
||||
hasPendingBlockingRequests: this.hasPendingBlockingRequestsForDirectory(directory),
|
||||
})
|
||||
@@ -139,6 +598,10 @@ export class ChildStoreManager {
|
||||
}
|
||||
|
||||
this.lifecycle.delete(directory)
|
||||
this.bootstrapQueue.delete(directory)
|
||||
this.manualBootstrapDemands.delete(directory)
|
||||
this.bootstrapStates.delete(directory)
|
||||
for (const demands of this.bootstrapDemandsByOwner.values()) demands.delete(directory)
|
||||
this.children.delete(directory)
|
||||
this.notifyRegistrySubscribers()
|
||||
const dispose = this.disposers.get(directory)
|
||||
@@ -186,6 +649,8 @@ export class ChildStoreManager {
|
||||
}
|
||||
|
||||
disposeAll() {
|
||||
this.disposed = true
|
||||
this.bootstrapGeneration += 1
|
||||
for (const directory of [...this.children.keys()]) {
|
||||
this.children.delete(directory)
|
||||
}
|
||||
@@ -193,6 +658,12 @@ export class ChildStoreManager {
|
||||
this.lifecycle.clear()
|
||||
this.pins.clear()
|
||||
this.disposers.clear()
|
||||
this.bootstrapQueue.clear()
|
||||
this.runningBootstraps.clear()
|
||||
this.bootstrapStates.clear()
|
||||
this.bootstrapDemandsByOwner.clear()
|
||||
this.manualBootstrapDemands.clear()
|
||||
this.notifyBootstrapSubscribers()
|
||||
}
|
||||
|
||||
subscribeRegistry(listener: () => void): () => void {
|
||||
|
||||
Reference in New Issue
Block a user