diff --git a/packages/app/src/context/global-sync/child-store.test.ts b/packages/app/src/context/global-sync/child-store.test.ts index 30dda86919b6..23813f5c04fb 100644 --- a/packages/app/src/context/global-sync/child-store.test.ts +++ b/packages/app/src/context/global-sync/child-store.test.ts @@ -1,13 +1,42 @@ -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) => [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() @@ -15,6 +44,7 @@ describe("createChildStoreManager", () => { }) if (!owner) throw new Error("owner required") + const createChildStoreManager = await createManager() const manager = createChildStoreManager({ owner, isBooting: () => false, @@ -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((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) + } + }) + }) + }) + }) }) diff --git a/packages/app/src/context/global-sync/child-store.ts b/packages/app/src/context/global-sync/child-store.ts index 0138310cdccd..3bbfaae8e977 100644 --- a/packages/app/src/context/global-sync/child-store.ts +++ b/packages/app/src/context/global-sync/child-store.ts @@ -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 { @@ -37,6 +37,7 @@ export function createChildStoreManager(input: { const iconCache = new Map() const lifecycle = new Map() const pins = new Map() + const [bootstrapEnabled, setBootstrapEnabled] = createStore>({}) const ownerPins = new WeakMap>() const disposers = new Map void>() @@ -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() @@ -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), ], })) @@ -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) } @@ -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) }