summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-12-20 04:57:39 -0600
committerAdam <[email protected]>2025-12-20 04:57:39 -0600
commit49567fe61aea76871866bb816f27743f57708af2 (patch)
treeee42f7b3d33875a28668575d2a18c95a7cb9e436
parente5b3f796e4c2e2ff12939dfc5e523ab9bd042f51 (diff)
downloadopencode-49567fe61aea76871866bb816f27743f57708af2.tar.gz
opencode-49567fe61aea76871866bb816f27743f57708af2.zip
fix(desktop): add retries to init promises
-rw-r--r--packages/desktop/src/context/global-sync.tsx41
-rw-r--r--packages/desktop/src/context/sync.tsx9
-rw-r--r--packages/util/src/retry.ts41
3 files changed, 71 insertions, 20 deletions
diff --git a/packages/desktop/src/context/global-sync.tsx b/packages/desktop/src/context/global-sync.tsx
index fffef5b5f..27a89e7bc 100644
--- a/packages/desktop/src/context/global-sync.tsx
+++ b/packages/desktop/src/context/global-sync.tsx
@@ -18,6 +18,7 @@ import {
} from "@opencode-ai/sdk/v2/client"
import { createStore, produce, reconcile } from "solid-js/store"
import { Binary } from "@opencode-ai/util/binary"
+import { retry } from "@opencode-ai/util/retry"
import { useGlobalSDK } from "./global-sdk"
import { ErrorPage, type InitError } from "../pages/error"
import { createContext, useContext, onMount, type ParentProps, Switch, Match } from "solid-js"
@@ -145,7 +146,7 @@ function createGlobalSync() {
changes: () => sdk.file.status().then((x) => setStore("changes", x.data!)),
node: () => sdk.file.list({ path: "/" }).then((x) => setStore("node", x.data!)),
}
- await Promise.all(Object.values(load).map((p) => p().catch((e) => setGlobalStore("error", e))))
+ await Promise.all(Object.values(load).map((p) => retry(p).catch((e) => setGlobalStore("error", e))))
.then(() => setStore("ready", true))
.catch((e) => setGlobalStore("error", e))
}
@@ -295,21 +296,29 @@ function createGlobalSync() {
async function bootstrap() {
return Promise.all([
- globalSDK.client.path.get().then((x) => {
- setGlobalStore("path", x.data!)
- }),
- globalSDK.client.project.list().then(async (x) => {
- setGlobalStore(
- "project",
- x.data!.filter((p) => !p.worktree.includes("opencode-test")).sort((a, b) => a.id.localeCompare(b.id)),
- )
- }),
- globalSDK.client.provider.list().then((x) => {
- setGlobalStore("provider", x.data ?? {})
- }),
- globalSDK.client.provider.auth().then((x) => {
- setGlobalStore("provider_auth", x.data ?? {})
- }),
+ retry(() =>
+ globalSDK.client.path.get().then((x) => {
+ setGlobalStore("path", x.data!)
+ }),
+ ),
+ retry(() =>
+ globalSDK.client.project.list().then(async (x) => {
+ setGlobalStore(
+ "project",
+ x.data!.filter((p) => !p.worktree.includes("opencode-test")).sort((a, b) => a.id.localeCompare(b.id)),
+ )
+ }),
+ ),
+ retry(() =>
+ globalSDK.client.provider.list().then((x) => {
+ setGlobalStore("provider", x.data ?? {})
+ }),
+ ),
+ retry(() =>
+ globalSDK.client.provider.auth().then((x) => {
+ setGlobalStore("provider_auth", x.data ?? {})
+ }),
+ ),
])
.then(() => setGlobalStore("ready", true))
.catch((e) => setGlobalStore("error", e))
diff --git a/packages/desktop/src/context/sync.tsx b/packages/desktop/src/context/sync.tsx
index ca25cae98..941b8b629 100644
--- a/packages/desktop/src/context/sync.tsx
+++ b/packages/desktop/src/context/sync.tsx
@@ -1,6 +1,7 @@
import { produce } from "solid-js/store"
import { createMemo } from "solid-js"
import { Binary } from "@opencode-ai/util/binary"
+import { retry } from "@opencode-ai/util/retry"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { useGlobalSync } from "./global-sync"
import { useSDK } from "./sdk"
@@ -61,10 +62,10 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
},
async sync(sessionID: string, _isRetry = false) {
const [session, messages, todo, diff] = await Promise.all([
- sdk.client.session.get({ sessionID }, { throwOnError: true }),
- sdk.client.session.messages({ sessionID, limit: 100 }),
- sdk.client.session.todo({ sessionID }),
- sdk.client.session.diff({ sessionID }),
+ retry(() => sdk.client.session.get({ sessionID })),
+ retry(() => sdk.client.session.messages({ sessionID, limit: 100 })),
+ retry(() => sdk.client.session.todo({ sessionID })),
+ retry(() => sdk.client.session.diff({ sessionID })),
])
setStore(
produce((draft) => {
diff --git a/packages/util/src/retry.ts b/packages/util/src/retry.ts
new file mode 100644
index 000000000..0014a604c
--- /dev/null
+++ b/packages/util/src/retry.ts
@@ -0,0 +1,41 @@
+export interface RetryOptions {
+ attempts?: number
+ delay?: number
+ factor?: number
+ maxDelay?: number
+ retryIf?: (error: unknown) => boolean
+}
+
+const TRANSIENT_MESSAGES = [
+ "load failed",
+ "network connection was lost",
+ "network request failed",
+ "failed to fetch",
+ "econnreset",
+ "econnrefused",
+ "etimedout",
+ "socket hang up",
+]
+
+function isTransientError(error: unknown): boolean {
+ if (!error) return false
+ const message = String(error instanceof Error ? error.message : error).toLowerCase()
+ return TRANSIENT_MESSAGES.some((m) => message.includes(m))
+}
+
+export async function retry<T>(fn: () => Promise<T>, options: RetryOptions = {}): Promise<T> {
+ const { attempts = 3, delay = 500, factor = 2, maxDelay = 10000, retryIf = isTransientError } = options
+
+ let lastError: unknown
+ for (let attempt = 0; attempt < attempts; attempt++) {
+ try {
+ return await fn()
+ } catch (error) {
+ lastError = error
+ if (attempt === attempts - 1 || !retryIf(error)) throw error
+ const wait = Math.min(delay * Math.pow(factor, attempt), maxDelay)
+ await new Promise((resolve) => setTimeout(resolve, wait))
+ }
+ }
+ throw lastError
+}