diff --git a/packages/ui/src/lib/retry-utils.ts b/packages/ui/src/lib/retry-utils.ts new file mode 100644 index 00000000..ac5783c2 --- /dev/null +++ b/packages/ui/src/lib/retry-utils.ts @@ -0,0 +1,38 @@ +interface RetryOptions { + maxAttempts?: number + initialDelayMs?: number + maxDelayMs?: number + backoffMultiplier?: number +} + +export async function retryWithBackoff( + fn: () => Promise, + options: RetryOptions = {}, +): Promise { + const { + maxAttempts = 3, + initialDelayMs = 100, + maxDelayMs = 5000, + backoffMultiplier = 2, + } = options + + let lastError: Error | null = null + let delayMs = initialDelayMs + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + return await fn() + } catch (error) { + lastError = error instanceof Error ? error : new Error(String(error)) + + if (attempt < maxAttempts) { + await new Promise((resolve) => setTimeout(resolve, delayMs)) + delayMs = Math.min(delayMs * backoffMultiplier, maxDelayMs) + } + } + } + + throw new Error( + `Failed after ${maxAttempts} attempts: ${lastError?.message || "Unknown error"}`, + ) +} diff --git a/packages/ui/src/lib/server-events.ts b/packages/ui/src/lib/server-events.ts index 833e6c2a..2ecfeb6e 100644 --- a/packages/ui/src/lib/server-events.ts +++ b/packages/ui/src/lib/server-events.ts @@ -2,6 +2,7 @@ import type { WorkspaceEventPayload, WorkspaceEventType } from "../../../server/ import { serverApi } from "./api-client" import { getClientIdentity } from "./client-identity" import { getLogger } from "./logger" +import { retryWithBackoff } from "./retry-utils" const RETRY_BASE_DELAY = 1000 const RETRY_MAX_DELAY = 10000 @@ -39,14 +40,19 @@ class ServerEvents { (event) => this.dispatch(event), () => this.scheduleReconnect(), (payload) => { - void serverApi - .sendClientConnectionPong({ - ...getClientIdentity(), - pingTs: payload.ts, - }) - .catch((error) => { - log.error("Failed to send client connection pong", error) - }) + const identity = getClientIdentity() + const pongPayload = { ...identity, pingTs: payload.ts } + + void retryWithBackoff( + () => serverApi.sendClientConnectionPong(pongPayload), + { + maxAttempts: 3, + initialDelayMs: 100, + maxDelayMs: 2000, + }, + ).catch((error) => { + log.error("Failed to send client connection pong after retries", error) + }) }, ) this.source.onopen = () => {