Files
opencode/packages/app/src/context/global-sync/child-store.ts
2026-02-12 19:38:06 -06:00

264 lines
7.9 KiB
TypeScript

import { createRoot, createEffect, getOwner, onCleanup, runWithOwner, type Accessor, type Owner } from "solid-js"
import { createStore, type SetStoreFunction, type Store } from "solid-js/store"
import { Persist, persisted } from "@/utils/persist"
import type { VcsInfo } from "@opencode-ai/sdk/v2/client"
import {
DIR_IDLE_TTL_MS,
MAX_DIR_STORES,
type ChildOptions,
type DirState,
type IconCache,
type MetaCache,
type ProjectMeta,
type State,
type VcsCache,
} from "./types"
import { canDisposeDirectory, pickDirectoriesToEvict } from "./eviction"
export function createChildStoreManager(input: {
owner: Owner
markStats: (activeDirectoryStores: number) => void
incrementEvictions: () => void
isBooting: (directory: string) => boolean
isLoadingSessions: (directory: string) => boolean
onBootstrap: (directory: string) => void
onDispose: (directory: string) => void
}) {
const children: Record<string, [Store<State>, SetStoreFunction<State>]> = {}
const vcsCache = new Map<string, VcsCache>()
const metaCache = new Map<string, MetaCache>()
const iconCache = new Map<string, IconCache>()
const lifecycle = new Map<string, DirState>()
const pins = new Map<string, number>()
const ownerPins = new WeakMap<object, Set<string>>()
const disposers = new Map<string, () => void>()
const mark = (directory: string) => {
if (!directory) return
lifecycle.set(directory, { lastAccessAt: Date.now() })
runEviction(directory)
}
const pin = (directory: string) => {
if (!directory) return
pins.set(directory, (pins.get(directory) ?? 0) + 1)
mark(directory)
}
const unpin = (directory: string) => {
if (!directory) return
const next = (pins.get(directory) ?? 0) - 1
if (next > 0) {
pins.set(directory, next)
return
}
pins.delete(directory)
runEviction()
}
const pinned = (directory: string) => (pins.get(directory) ?? 0) > 0
const pinForOwner = (directory: string) => {
const current = getOwner()
if (!current) return
if (current === input.owner) return
const key = current as object
const set = ownerPins.get(key)
if (set?.has(directory)) return
if (set) set.add(directory)
if (!set) ownerPins.set(key, new Set([directory]))
pin(directory)
onCleanup(() => {
const set = ownerPins.get(key)
if (set) {
set.delete(directory)
if (set.size === 0) ownerPins.delete(key)
}
unpin(directory)
})
}
function disposeDirectory(directory: string) {
if (
!canDisposeDirectory({
directory,
hasStore: !!children[directory],
pinned: pinned(directory),
booting: input.isBooting(directory),
loadingSessions: input.isLoadingSessions(directory),
})
) {
return false
}
vcsCache.delete(directory)
metaCache.delete(directory)
iconCache.delete(directory)
lifecycle.delete(directory)
const dispose = disposers.get(directory)
if (dispose) {
dispose()
disposers.delete(directory)
}
delete children[directory]
input.onDispose(directory)
input.markStats(Object.keys(children).length)
return true
}
function runEviction(skip?: string) {
const stores = Object.keys(children)
if (stores.length === 0) return
const list = pickDirectoriesToEvict({
stores,
state: lifecycle,
pins: new Set(stores.filter(pinned)),
max: MAX_DIR_STORES,
ttl: DIR_IDLE_TTL_MS,
now: Date.now(),
}).filter((directory) => directory !== skip)
if (list.length === 0) return
for (const directory of list) {
if (!disposeDirectory(directory)) continue
input.incrementEvictions()
}
}
function ensureChild(directory: string) {
if (!directory) console.error("No directory provided")
if (!children[directory]) {
const vcs = runWithOwner(input.owner, () =>
persisted(
Persist.workspace(directory, "vcs", ["vcs.v1"]),
createStore({ value: undefined as VcsInfo | undefined }),
),
)
if (!vcs) throw new Error("Failed to create persisted cache")
const vcsStore = vcs[0]
const vcsReady = vcs[3]
vcsCache.set(directory, { store: vcsStore, setStore: vcs[1], ready: vcsReady })
const meta = runWithOwner(input.owner, () =>
persisted(
Persist.workspace(directory, "project", ["project.v1"]),
createStore({ value: undefined as ProjectMeta | undefined }),
),
)
if (!meta) throw new Error("Failed to create persisted project metadata")
metaCache.set(directory, { store: meta[0], setStore: meta[1], ready: meta[3] })
const icon = runWithOwner(input.owner, () =>
persisted(
Persist.workspace(directory, "icon", ["icon.v1"]),
createStore({ value: undefined as string | undefined }),
),
)
if (!icon) throw new Error("Failed to create persisted project icon")
iconCache.set(directory, { store: icon[0], setStore: icon[1], ready: icon[3] })
const init = () =>
createRoot((dispose) => {
const child = createStore<State>({
project: "",
projectMeta: meta[0].value,
icon: icon[0].value,
provider: { all: [], connected: [], default: {} },
config: {},
path: { state: "", config: "", worktree: "", directory: "", home: "" },
status: "loading" as const,
agent: [],
command: [],
session: [],
sessionTotal: 0,
session_status: {},
session_diff: {},
todo: {},
permission: {},
question: {},
mcp: {},
lsp: [],
vcs: vcsStore.value,
limit: 5,
message: {},
part: {},
})
children[directory] = child
disposers.set(directory, dispose)
createEffect(() => {
if (!vcsReady()) return
const cached = vcsStore.value
if (!cached?.branch) return
child[1]("vcs", (value) => value ?? cached)
})
createEffect(() => {
child[1]("projectMeta", meta[0].value)
})
createEffect(() => {
child[1]("icon", icon[0].value)
})
})
runWithOwner(input.owner, init)
input.markStats(Object.keys(children).length)
}
mark(directory)
const childStore = children[directory]
if (!childStore) throw new Error("Failed to create store")
return childStore
}
function child(directory: string, options: ChildOptions = {}) {
const childStore = ensureChild(directory)
pinForOwner(directory)
const shouldBootstrap = options.bootstrap ?? true
if (shouldBootstrap && childStore[0].status === "loading") {
input.onBootstrap(directory)
}
return childStore
}
function projectMeta(directory: string, patch: ProjectMeta) {
const [store, setStore] = ensureChild(directory)
const cached = metaCache.get(directory)
if (!cached) return
const previous = store.projectMeta ?? {}
const icon = patch.icon ? { ...(previous.icon ?? {}), ...patch.icon } : previous.icon
const commands = patch.commands ? { ...(previous.commands ?? {}), ...patch.commands } : previous.commands
const next = {
...previous,
...patch,
icon,
commands,
}
cached.setStore("value", next)
setStore("projectMeta", next)
}
function projectIcon(directory: string, value: string | undefined) {
const [store, setStore] = ensureChild(directory)
const cached = iconCache.get(directory)
if (!cached) return
if (store.icon === value) return
cached.setStore("value", value)
setStore("icon", value)
}
return {
children,
ensureChild,
child,
projectMeta,
projectIcon,
mark,
pin,
unpin,
pinned,
disposeDirectory,
runEviction,
vcsCache,
metaCache,
iconCache,
}
}