Просмотр исходного кода

fix(desktop): add retries to init promises

Adam 3 месяцев назад
Родитель
Сommit
49567fe61a

+ 25 - 16
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))

+ 5 - 4
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) => {

+ 41 - 0
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
+}