Skip to content
102 changes: 99 additions & 3 deletions packages/app/src/context/global-sync/child-store.test.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,50 @@
import { describe, expect, test } from "bun:test"
import { describe, expect, mock, test } from "bun:test"
import { createRoot, getOwner } from "solid-js"
import { createStore } from "solid-js/store"
import type { State } from "./types"
import { createChildStoreManager } from "./child-store"

const skipToken = Symbol("skipToken")

mock.module("@tanstack/solid-query", () => ({
skipToken,
queryOptions: (options: unknown) => options,
QueryClient: class {},
QueryClientProvider: (props: { children?: unknown }) => props.children,
useMutation: () => ({ mutateAsync: async () => {} }),
useQuery: () => ({ isLoading: false, data: undefined, refetch: async () => {} }),
useQueryClient: () => ({
fetchQuery: async () => undefined,
ensureQueryData: async () => undefined,
}),
useQueries: (factory: () => { queries: Array<{ queryFn?: unknown }> }) =>
factory().queries.map((query) => {
if (query.queryFn === skipToken || typeof query.queryFn !== "function") {
return { isLoading: false, data: undefined }
}
return { isLoading: false, data: query.queryFn() }
}),
}))

mock.module("@/utils/persist", () => ({
Persist: {
workspace: (directory: string, key: string) => ({ key: `${directory}:${key}` }),
},
persisted: (_target: unknown, store: ReturnType<typeof createStore>) => [store[0], store[1], null, () => true],
}))

const child = () => createStore({} as State)
const createManager = async () => (await import("./child-store")).createChildStoreManager

describe("createChildStoreManager", () => {
test("does not evict the active directory during mark", () => {
test("does not evict the active directory during mark", async () => {
const owner = createRoot((dispose) => {
const current = getOwner()
dispose()
return current
})
if (!owner) throw new Error("owner required")

const createChildStoreManager = await createManager()
const manager = createChildStoreManager({
owner,
isBooting: () => false,
Expand All @@ -37,4 +67,70 @@ describe("createChildStoreManager", () => {

expect(manager.children[directory]).toBeDefined()
})

test("bootstrap false does not load instance-scoped status queries", async () => {
const createChildStoreManager = await createManager()
await new Promise<void>((resolve, reject) => {
createRoot((dispose) => {
const owner = getOwner()
if (!owner) {
dispose()
reject(new Error("owner required"))
return
}

const calls: string[] = []
const manager = createChildStoreManager({
owner,
isBooting: () => false,
isLoadingSessions: () => false,
onBootstrap() {
calls.push("bootstrap")
},
onDispose() {},
translate: (key) => key,
getSdk: () =>
({
path: {
get: () => {
calls.push("path")
return Promise.resolve({ data: undefined })
},
},
mcp: {
status: () => {
calls.push("mcp")
return Promise.resolve({ data: {} })
},
},
lsp: {
status: () => {
calls.push("lsp")
return Promise.resolve({ data: [] })
},
},
provider: {
list: () => {
calls.push("provider")
return Promise.resolve({ data: { all: [], connected: [], default: {} } })
},
},
}) as never,
})

manager.child("/preview", { bootstrap: false })

queueMicrotask(() => {
try {
expect(calls).toEqual([])
dispose()
resolve()
} catch (error) {
dispose()
reject(error)
}
})
})
})
})
})
22 changes: 15 additions & 7 deletions packages/app/src/context/global-sync/child-store.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createRoot, getOwner, onCleanup, runWithOwner, type Owner } from "solid-js"
import { createStore, type SetStoreFunction, type Store } from "solid-js/store"
import { createStore, produce, type SetStoreFunction, type Store } from "solid-js/store"
import { Persist, persisted } from "@/utils/persist"
import type { OpencodeClient, ProviderListResponse, VcsInfo } from "@opencode-ai/sdk/v2/client"
import {
Expand Down Expand Up @@ -37,6 +37,7 @@ export function createChildStoreManager(input: {
const iconCache = new Map<string, IconCache>()
const lifecycle = new Map<string, DirState>()
const pins = new Map<string, number>()
const [bootstrapEnabled, setBootstrapEnabled] = createStore<Record<string, boolean>>({})
const ownerPins = new WeakMap<object, Set<string>>()
const disposers = new Map<string, () => void>()

Expand Down Expand Up @@ -110,6 +111,11 @@ export function createChildStoreManager(input: {
metaCache.delete(key)
iconCache.delete(key)
lifecycle.delete(key)
setBootstrapEnabled(
produce((draft) => {
delete draft[key]
}),
)
const dispose = disposers.get(key)
if (dispose) {
dispose()
Expand Down Expand Up @@ -178,10 +184,10 @@ export function createChildStoreManager(input: {

const [pathQuery, mcpQuery, lspQuery, providerQuery] = useQueries(() => ({
queries: [
loadPathQuery(key, sdk),
loadMcpQuery(key, sdk),
loadLspQuery(key, sdk),
loadProvidersQuery(key, sdk),
loadPathQuery(key, bootstrapEnabled[key] ? sdk : undefined),
loadMcpQuery(key, bootstrapEnabled[key] ? sdk : undefined),
loadLspQuery(key, bootstrapEnabled[key] ? sdk : undefined),
loadProvidersQuery(key, bootstrapEnabled[key] ? sdk : undefined),
],
}))

Expand Down Expand Up @@ -270,9 +276,10 @@ export function createChildStoreManager(input: {

function child(directory: string, options: ChildOptions = {}) {
const key = directoryKey(directory)
const shouldBootstrap = options.bootstrap ?? true
if (shouldBootstrap) setBootstrapEnabled(key, true)
const childStore = ensureChild(directory)
pinForOwner(key)
const shouldBootstrap = options.bootstrap ?? true
if (shouldBootstrap && childStore[0].status === "loading") {
input.onBootstrap(directory)
}
Expand All @@ -281,8 +288,9 @@ export function createChildStoreManager(input: {

function peek(directory: string, options: ChildOptions = {}) {
const key = directoryKey(directory)
const childStore = ensureChild(directory)
const shouldBootstrap = options.bootstrap ?? true
if (shouldBootstrap) setBootstrapEnabled(key, true)
const childStore = ensureChild(directory)
if (shouldBootstrap && childStore[0].status === "loading") {
input.onBootstrap(directory)
}
Expand Down
Loading