diff options
| author | Adam <[email protected]> | 2026-03-25 06:25:57 -0500 |
|---|---|---|
| committer | Adam <[email protected]> | 2026-03-25 06:25:57 -0500 |
| commit | 1041ae91d1a39401fe099747e3bc093bdcdaa079 (patch) | |
| tree | ca5515910ad01f76639577ef8e3a991b644a5ade /packages/app/src/context/global-sync | |
| parent | 898456a25cf2edbfc4ae4961b37424f633419dd6 (diff) | |
| download | opencode-1041ae91d1a39401fe099747e3bc093bdcdaa079.tar.gz opencode-1041ae91d1a39401fe099747e3bc093bdcdaa079.zip | |
Reapply "fix(app): startup efficiency"
This reverts commit 898456a25cf2edbfc4ae4961b37424f633419dd6.
Diffstat (limited to 'packages/app/src/context/global-sync')
| -rw-r--r-- | packages/app/src/context/global-sync/bootstrap.ts | 337 |
1 files changed, 206 insertions, 131 deletions
diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts index 13494b7ad..47be3abcb 100644 --- a/packages/app/src/context/global-sync/bootstrap.ts +++ b/packages/app/src/context/global-sync/bootstrap.ts @@ -31,73 +31,102 @@ type GlobalStore = { reload: undefined | "pending" | "complete" } +function waitForPaint() { + return new Promise<void>((resolve) => { + let done = false + const finish = () => { + if (done) return + done = true + resolve() + } + const timer = setTimeout(finish, 50) + if (typeof requestAnimationFrame !== "function") return + requestAnimationFrame(() => { + clearTimeout(timer) + finish() + }) + }) +} + +function errors(list: PromiseSettledResult<unknown>[]) { + return list.filter((item): item is PromiseRejectedResult => item.status === "rejected").map((item) => item.reason) +} + +function runAll(list: Array<() => Promise<unknown>>) { + return Promise.allSettled(list.map((item) => item())) +} + +function showErrors(input: { + errors: unknown[] + title: string + translate: (key: string, vars?: Record<string, string | number>) => string + formatMoreCount: (count: number) => string +}) { + if (input.errors.length === 0) return + const message = formatServerError(input.errors[0], input.translate) + const more = input.errors.length > 1 ? input.formatMoreCount(input.errors.length - 1) : "" + showToast({ + variant: "error", + title: input.title, + description: message + more, + }) +} + export async function bootstrapGlobal(input: { globalSDK: OpencodeClient - connectErrorTitle: string - connectErrorDescription: string requestFailedTitle: string translate: (key: string, vars?: Record<string, string | number>) => string formatMoreCount: (count: number) => string setGlobalStore: SetStoreFunction<GlobalStore> }) { - const health = await input.globalSDK.global - .health() - .then((x) => x.data) - .catch(() => undefined) - if (!health?.healthy) { - showToast({ - variant: "error", - title: input.connectErrorTitle, - description: input.connectErrorDescription, - }) - input.setGlobalStore("ready", true) - return - } + const fast = [ + () => + retry(() => + input.globalSDK.path.get().then((x) => { + input.setGlobalStore("path", x.data!) + }), + ), + () => + retry(() => + input.globalSDK.global.config.get().then((x) => { + input.setGlobalStore("config", x.data!) + }), + ), + () => + retry(() => + input.globalSDK.provider.list().then((x) => { + input.setGlobalStore("provider", normalizeProviderList(x.data!)) + }), + ), + ] - const tasks = [ - retry(() => - input.globalSDK.path.get().then((x) => { - input.setGlobalStore("path", x.data!) - }), - ), - retry(() => - input.globalSDK.global.config.get().then((x) => { - input.setGlobalStore("config", x.data!) - }), - ), - retry(() => - input.globalSDK.project.list().then((x) => { - const projects = (x.data ?? []) - .filter((p) => !!p?.id) - .filter((p) => !!p.worktree && !p.worktree.includes("opencode-test")) - .slice() - .sort((a, b) => cmp(a.id, b.id)) - input.setGlobalStore("project", projects) - }), - ), - retry(() => - input.globalSDK.provider.list().then((x) => { - input.setGlobalStore("provider", normalizeProviderList(x.data!)) - }), - ), - retry(() => - input.globalSDK.provider.auth().then((x) => { - input.setGlobalStore("provider_auth", x.data ?? {}) - }), - ), + const slow = [ + () => + retry(() => + input.globalSDK.project.list().then((x) => { + const projects = (x.data ?? []) + .filter((p) => !!p?.id) + .filter((p) => !!p.worktree && !p.worktree.includes("opencode-test")) + .slice() + .sort((a, b) => cmp(a.id, b.id)) + input.setGlobalStore("project", projects) + }), + ), ] - const results = await Promise.allSettled(tasks) - const errors = results.filter((r): r is PromiseRejectedResult => r.status === "rejected").map((r) => r.reason) - if (errors.length) { - const message = formatServerError(errors[0], input.translate) - const more = errors.length > 1 ? input.formatMoreCount(errors.length - 1) : "" - showToast({ - variant: "error", - title: input.requestFailedTitle, - description: message + more, - }) - } + showErrors({ + errors: errors(await runAll(fast)), + title: input.requestFailedTitle, + translate: input.translate, + formatMoreCount: input.formatMoreCount, + }) + await waitForPaint() + showErrors({ + errors: errors(await runAll(slow)), + title: input.requestFailedTitle, + translate: input.translate, + formatMoreCount: input.formatMoreCount, + }) input.setGlobalStore("ready", true) } @@ -111,6 +140,10 @@ function groupBySession<T extends { id: string; sessionID: string }>(input: T[]) }, {}) } +function projectID(directory: string, projects: Project[]) { + return projects.find((project) => project.worktree === directory || project.sandboxes?.includes(directory))?.id +} + export async function bootstrapDirectory(input: { directory: string sdk: OpencodeClient @@ -119,88 +152,130 @@ export async function bootstrapDirectory(input: { vcsCache: VcsCache loadSessions: (directory: string) => Promise<void> | void translate: (key: string, vars?: Record<string, string | number>) => string + global: { + config: Config + project: Project[] + provider: ProviderListResponse + } }) { - if (input.store.status !== "complete") input.setStore("status", "loading") - - const blockingRequests = { - project: () => input.sdk.project.current().then((x) => input.setStore("project", x.data!.id)), - provider: () => - input.sdk.provider.list().then((x) => { - input.setStore("provider", normalizeProviderList(x.data!)) - }), - agent: () => input.sdk.app.agents().then((x) => input.setStore("agent", x.data ?? [])), - config: () => input.sdk.config.get().then((x) => input.setStore("config", x.data!)), + const loading = input.store.status !== "complete" + const seededProject = projectID(input.directory, input.global.project) + if (seededProject) input.setStore("project", seededProject) + if (input.store.provider.all.length === 0 && input.global.provider.all.length > 0) { + input.setStore("provider", input.global.provider) + } + if (Object.keys(input.store.config).length === 0 && Object.keys(input.global.config).length > 0) { + input.setStore("config", input.global.config) + } + if (loading) input.setStore("status", "partial") + + const fast = [ + () => + seededProject + ? Promise.resolve() + : retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id)), + () => retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", x.data ?? []))), + () => retry(() => input.sdk.config.get().then((x) => input.setStore("config", x.data!))), + () => + 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.session.status().then((x) => input.setStore("session_status", x.data!))), + () => + retry(() => + input.sdk.vcs.get().then((x) => { + const next = x.data ?? input.store.vcs + input.setStore("vcs", next) + if (next?.branch) 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 grouped = groupBySession( + (x.data ?? []).filter((perm): perm is PermissionRequest => !!perm?.id && !!perm.sessionID), + ) + 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 grouped = groupBySession((x.data ?? []).filter((q): q is QuestionRequest => !!q?.id && !!q.sessionID)) + 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" }, + ), + ) + } + }) + }), + ), + ] + + const slow = [ + () => + retry(() => + input.sdk.provider.list().then((x) => { + input.setStore("provider", normalizeProviderList(x.data!)) + }), + ), + () => Promise.resolve(input.loadSessions(input.directory)), + () => retry(() => input.sdk.mcp.status().then((x) => input.setStore("mcp", x.data!))), + () => retry(() => input.sdk.lsp.status().then((x) => input.setStore("lsp", x.data!))), + ] + + const errs = errors(await runAll(fast)) + if (errs.length > 0) { + console.error("Failed to bootstrap instance", errs[0]) + const project = getFilename(input.directory) + showToast({ + variant: "error", + title: input.translate("toast.project.reloadFailed.title", { project }), + description: formatServerError(errs[0], input.translate), + }) } - try { - await Promise.all(Object.values(blockingRequests).map((p) => retry(p))) - } catch (err) { - console.error("Failed to bootstrap instance", 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), }) - input.setStore("status", "partial") - return } - if (input.store.status !== "complete") input.setStore("status", "partial") - - Promise.all([ - input.sdk.path.get().then((x) => input.setStore("path", x.data!)), - input.sdk.command.list().then((x) => input.setStore("command", x.data ?? [])), - input.sdk.session.status().then((x) => input.setStore("session_status", x.data!)), - input.loadSessions(input.directory), - input.sdk.mcp.status().then((x) => input.setStore("mcp", x.data!)), - input.sdk.lsp.status().then((x) => input.setStore("lsp", x.data!)), - input.sdk.vcs.get().then((x) => { - const next = x.data ?? input.store.vcs - input.setStore("vcs", next) - if (next?.branch) input.vcsCache.setStore("value", next) - }), - input.sdk.permission.list().then((x) => { - const grouped = groupBySession( - (x.data ?? []).filter((perm): perm is PermissionRequest => !!perm?.id && !!perm.sessionID), - ) - 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" }, - ), - ) - } - }) - }), - input.sdk.question.list().then((x) => { - const grouped = groupBySession((x.data ?? []).filter((q): q is QuestionRequest => !!q?.id && !!q.sessionID)) - 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" }, - ), - ) - } - }) - }), - ]).then(() => { - input.setStore("status", "complete") - }) + if (loading && errs.length === 0 && slowErrs.length === 0) input.setStore("status", "complete") } |
