diff options
| author | Brendan Allan <[email protected]> | 2026-04-16 14:10:23 +0800 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-04-16 06:10:23 +0000 |
| commit | 97918500d4020a7b44f3636f23daabc8c477008b (patch) | |
| tree | 158d312230d0522fa702272fafcca9af7e1df544 /packages/app/src/context/global-sync | |
| parent | e2c08039624ac7c799ea5ba6ce94e3c671a8ed7c (diff) | |
| download | opencode-97918500d4020a7b44f3636f23daabc8c477008b.tar.gz opencode-97918500d4020a7b44f3636f23daabc8c477008b.zip | |
app: start migrating bootstrap data fetching to TanStack Query (#22756)
Diffstat (limited to 'packages/app/src/context/global-sync')
| -rw-r--r-- | packages/app/src/context/global-sync/bootstrap.ts | 269 | ||||
| -rw-r--r-- | packages/app/src/context/global-sync/child-store.ts | 1 | ||||
| -rw-r--r-- | packages/app/src/context/global-sync/types.ts | 1 |
3 files changed, 150 insertions, 121 deletions
diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts index 2f9147498..17fe726f9 100644 --- a/packages/app/src/context/global-sync/bootstrap.ts +++ b/packages/app/src/context/global-sync/bootstrap.ts @@ -18,6 +18,8 @@ import { reconcile, type SetStoreFunction, type Store } from "solid-js/store" import type { State, VcsCache } from "./types" import { cmp, normalizeAgentList, normalizeProviderList } from "./utils" import { formatServerError } from "@/utils/server-errors" +import { QueryClient, queryOptions, skipToken } from "@tanstack/solid-query" +import { loadSessionsQuery } from "../global-sync" type GlobalStore = { ready: boolean @@ -71,6 +73,7 @@ export async function bootstrapGlobal(input: { translate: (key: string, vars?: Record<string, string | number>) => string formatMoreCount: (count: number) => string setGlobalStore: SetStoreFunction<GlobalStore> + queryClient: QueryClient }) { const fast = [ () => @@ -80,11 +83,16 @@ export async function bootstrapGlobal(input: { }), ), () => - retry(() => - input.globalSDK.provider.list().then((x) => { - input.setGlobalStore("provider", normalizeProviderList(x.data!)) - }), - ), + input.queryClient.fetchQuery({ + ...loadProvidersQuery(null), + queryFn: () => + retry(() => + input.globalSDK.provider.list().then((x) => { + input.setGlobalStore("provider", normalizeProviderList(x.data!)) + return null + }), + ), + }), ] const slow = [ @@ -172,6 +180,12 @@ function warmSessions(input: { ).then(() => undefined) } +export const loadProvidersQuery = (directory: string | null) => + queryOptions<null>({ queryKey: [directory, "providers"], queryFn: skipToken }) + +export const loadAgentsQuery = (directory: string | null) => + queryOptions<null>({ queryKey: [directory, "agents"], queryFn: skipToken }) + export async function bootstrapDirectory(input: { directory: string sdk: OpencodeClient @@ -186,6 +200,7 @@ export async function bootstrapDirectory(input: { project: Project[] provider: ProviderListResponse } + queryClient: QueryClient }) { const loading = input.store.status !== "complete" const seededProject = projectID(input.directory, input.global.project) @@ -207,97 +222,7 @@ export async function bootstrapDirectory(input: { input.setStore("lsp", []) if (loading) input.setStore("status", "partial") - const fast = [ - () => retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", normalizeAgentList(x.data)))), - () => retry(() => input.sdk.config.get().then((x) => input.setStore("config", x.data!))), - () => retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))), - ] - - const slow = [ - () => - seededProject - ? Promise.resolve() - : retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id)), - () => - seededPath - ? Promise.resolve() - : retry(() => - input.sdk.path.get().then((x) => { - input.setStore("path", x.data!) - const next = projectID(x.data?.directory ?? input.directory, input.global.project) - if (next) input.setStore("project", next) - }), - ), - () => - retry(() => - input.sdk.vcs.get().then((x) => { - const next = x.data ?? input.store.vcs - input.setStore("vcs", next) - if (next) input.vcsCache.setStore("value", next) - }), - ), - () => retry(() => input.sdk.command.list().then((x) => input.setStore("command", x.data ?? []))), - () => - retry(() => - input.sdk.permission.list().then((x) => { - const ids = (x.data ?? []).map((perm) => perm?.sessionID).filter((id): id is string => !!id) - const grouped = groupBySession( - (x.data ?? []).filter((perm): perm is PermissionRequest => !!perm?.id && !!perm.sessionID), - ) - return warmSessions({ ids, store: input.store, setStore: input.setStore, sdk: input.sdk }).then(() => - batch(() => { - for (const sessionID of Object.keys(input.store.permission)) { - if (grouped[sessionID]) continue - input.setStore("permission", sessionID, []) - } - for (const [sessionID, permissions] of Object.entries(grouped)) { - input.setStore( - "permission", - sessionID, - reconcile( - permissions.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id)), - { key: "id" }, - ), - ) - } - }), - ) - }), - ), - () => - retry(() => - input.sdk.question.list().then((x) => { - const ids = (x.data ?? []).map((question) => question?.sessionID).filter((id): id is string => !!id) - const grouped = groupBySession((x.data ?? []).filter((q): q is QuestionRequest => !!q?.id && !!q.sessionID)) - return warmSessions({ ids, store: input.store, setStore: input.setStore, sdk: input.sdk }).then(() => - batch(() => { - for (const sessionID of Object.keys(input.store.question)) { - if (grouped[sessionID]) continue - input.setStore("question", sessionID, []) - } - for (const [sessionID, questions] of Object.entries(grouped)) { - input.setStore( - "question", - sessionID, - reconcile( - questions.filter((q) => !!q?.id).sort((a, b) => cmp(a.id, b.id)), - { key: "id" }, - ), - ) - } - }), - ) - }), - ), - () => Promise.resolve(input.loadSessions(input.directory)), - () => - retry(() => - input.sdk.mcp.status().then((x) => { - input.setStore("mcp", x.data!) - input.setStore("mcp_ready", true) - }), - ), - ] + const fast = [() => Promise.resolve(input.loadSessions(input.directory))] const errs = errors(await runAll(fast)) if (errs.length > 0) { @@ -310,36 +235,138 @@ export async function bootstrapDirectory(input: { }) } - await waitForPaint() - const slowErrs = errors(await runAll(slow)) - if (slowErrs.length > 0) { - console.error("Failed to finish bootstrap instance", slowErrs[0]) - const project = getFilename(input.directory) - showToast({ - variant: "error", - title: input.translate("toast.project.reloadFailed.title", { project }), - description: formatServerError(slowErrs[0], input.translate), - }) - } - - if (loading && errs.length === 0 && slowErrs.length === 0) input.setStore("status", "complete") + ;(async () => { + const slow = [ + () => + input.queryClient.ensureQueryData({ + ...loadAgentsQuery(input.directory), + queryFn: () => + retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", normalizeAgentList(x.data)))).then( + () => null, + ), + }), + () => retry(() => input.sdk.config.get().then((x) => input.setStore("config", x.data!))), + () => retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))), + () => + seededProject + ? Promise.resolve() + : retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id)), + () => + seededPath + ? Promise.resolve() + : retry(() => + input.sdk.path.get().then((x) => { + input.setStore("path", x.data!) + const next = projectID(x.data?.directory ?? input.directory, input.global.project) + if (next) input.setStore("project", next) + }), + ), + () => + retry(() => + input.sdk.vcs.get().then((x) => { + const next = x.data ?? input.store.vcs + input.setStore("vcs", next) + if (next) input.vcsCache.setStore("value", next) + }), + ), + () => retry(() => input.sdk.command.list().then((x) => input.setStore("command", x.data ?? []))), + () => + retry(() => + input.sdk.permission.list().then((x) => { + const ids = (x.data ?? []).map((perm) => perm?.sessionID).filter((id): id is string => !!id) + const grouped = groupBySession( + (x.data ?? []).filter((perm): perm is PermissionRequest => !!perm?.id && !!perm.sessionID), + ) + return warmSessions({ ids, store: input.store, setStore: input.setStore, sdk: input.sdk }).then(() => + batch(() => { + for (const sessionID of Object.keys(input.store.permission)) { + if (grouped[sessionID]) continue + input.setStore("permission", sessionID, []) + } + for (const [sessionID, permissions] of Object.entries(grouped)) { + input.setStore( + "permission", + sessionID, + reconcile( + permissions.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id)), + { key: "id" }, + ), + ) + } + }), + ) + }), + ), + () => + retry(() => + input.sdk.question.list().then((x) => { + const ids = (x.data ?? []).map((question) => question?.sessionID).filter((id): id is string => !!id) + const grouped = groupBySession((x.data ?? []).filter((q): q is QuestionRequest => !!q?.id && !!q.sessionID)) + return warmSessions({ ids, store: input.store, setStore: input.setStore, sdk: input.sdk }).then(() => + batch(() => { + for (const sessionID of Object.keys(input.store.question)) { + if (grouped[sessionID]) continue + input.setStore("question", sessionID, []) + } + for (const [sessionID, questions] of Object.entries(grouped)) { + input.setStore( + "question", + sessionID, + reconcile( + questions.filter((q) => !!q?.id).sort((a, b) => cmp(a.id, b.id)), + { key: "id" }, + ), + ) + } + }), + ) + }), + ), + () => Promise.resolve(input.loadSessions(input.directory)), + () => + retry(() => + input.sdk.mcp.status().then((x) => { + input.setStore("mcp", x.data!) + input.setStore("mcp_ready", true) + }), + ), + ] - const rev = (providerRev.get(input.directory) ?? 0) + 1 - providerRev.set(input.directory, rev) - void retry(() => input.sdk.provider.list()) - .then((x) => { - if (providerRev.get(input.directory) !== rev) return - input.setStore("provider", normalizeProviderList(x.data!)) - input.setStore("provider_ready", true) - }) - .catch((err) => { - if (providerRev.get(input.directory) !== rev) return - console.error("Failed to refresh provider list", err) + await waitForPaint() + const slowErrs = errors(await runAll(slow)) + if (slowErrs.length > 0) { + console.error("Failed to finish bootstrap instance", slowErrs[0]) const project = getFilename(input.directory) showToast({ variant: "error", title: input.translate("toast.project.reloadFailed.title", { project }), - description: formatServerError(err, input.translate), + description: formatServerError(slowErrs[0], input.translate), }) + } + + if (loading && errs.length === 0 && slowErrs.length === 0) input.setStore("status", "complete") + + const rev = (providerRev.get(input.directory) ?? 0) + 1 + providerRev.set(input.directory, rev) + void input.queryClient.ensureQueryData({ + ...loadSessionsQuery(input.directory), + queryFn: () => + retry(() => input.sdk.provider.list()) + .then((x) => { + if (providerRev.get(input.directory) !== rev) return + input.setStore("provider", normalizeProviderList(x.data!)) + input.setStore("provider_ready", true) + }) + .catch((err) => { + if (providerRev.get(input.directory) !== rev) console.error("Failed to refresh provider list", err) + const project = getFilename(input.directory) + showToast({ + variant: "error", + title: input.translate("toast.project.reloadFailed.title", { project }), + description: formatServerError(err, input.translate), + }) + }) + .then(() => null), }) + })() } diff --git a/packages/app/src/context/global-sync/child-store.ts b/packages/app/src/context/global-sync/child-store.ts index 3fe67e4fb..6788e8cc5 100644 --- a/packages/app/src/context/global-sync/child-store.ts +++ b/packages/app/src/context/global-sync/child-store.ts @@ -182,6 +182,7 @@ export function createChildStoreManager(input: { limit: 5, message: {}, part: {}, + bootstrapPromise: Promise.resolve(), }) children[directory] = child disposers.set(directory, dispose) diff --git a/packages/app/src/context/global-sync/types.ts b/packages/app/src/context/global-sync/types.ts index e3ec83c5e..28b3705d1 100644 --- a/packages/app/src/context/global-sync/types.ts +++ b/packages/app/src/context/global-sync/types.ts @@ -72,6 +72,7 @@ export type State = { part: { [messageID: string]: Part[] } + bootstrapPromise: Promise<void> } export type VcsCache = { |
