diff --git a/.github/workflows/build-sign-notarize.yml b/.github/workflows/build-sign-notarize.yml index d9f6751..f7eb0db 100644 --- a/.github/workflows/build-sign-notarize.yml +++ b/.github/workflows/build-sign-notarize.yml @@ -41,7 +41,7 @@ jobs: steps: - uses: teaxyz/setup@v0 with: - version: 0.25.0 + version: 0.26.2 - uses: actions/checkout@v3 - name: get gui version @@ -282,4 +282,4 @@ jobs: run: | export S3_INSTALLERS_KEY=s3://preview.gui.tea.xyz/$prefix/dist.tgz aws s3 cp dist.tgz $S3_INSTALLERS_KEY - echo s3-key=$S3_INSTALLERS_KEY >> $GITHUB_OUTPUT \ No newline at end of file + echo s3-key=$S3_INSTALLERS_KEY >> $GITHUB_OUTPUT diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d1d4af6..ac2e0bd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: steps: - uses: teaxyz/setup@v0 with: - version: 0.25.0 + version: 0.26.2 - uses: actions/checkout@v3 - name: install app dependencies run: tea -E xc setup @@ -51,7 +51,7 @@ jobs: steps: - uses: teaxyz/setup@v0 with: - version: 0.25.0 + version: 0.26.2 - uses: actions/checkout@v3 - name: cache node_modules build # TODO: cache issue in our self-hosted macos runner ESPIPE: invalid seek, read @@ -257,4 +257,4 @@ jobs: ```bash http://preview.gui.tea.xyz.s3-website-us-east-1.amazonaws.com/${{ needs.changes.outputs.preview_folder }}/${{ steps.app_files.outputs.dmg_x86 }} ``` - copy-paste into a browser to download \ No newline at end of file + copy-paste into a browser to download diff --git a/modules/desktop/src/libs/native-electron.ts b/modules/desktop/src/libs/native-electron.ts index 235a361..ff26916 100644 --- a/modules/desktop/src/libs/native-electron.ts +++ b/modules/desktop/src/libs/native-electron.ts @@ -11,7 +11,6 @@ * - connect to a local platform api and returns a data */ -import semverCompare from "semver/functions/compare"; import type { Package, Review, AirtablePost, Bottle } from "@tea/ui/types"; import { type GUIPackage, type DeviceAuth, type Session, AuthStatus } from "./types"; @@ -21,26 +20,24 @@ import { installPackageCommand } from "./native/cli"; import { get as apiGet } from "$libs/v1-client"; import axios from "axios"; +import withRetry from "./utils/retry"; const log = window.require("electron-log"); const { ipcRenderer, shell } = window.require("electron"); -let retryLimit = 0; export async function getDistPackages(): Promise { - let packages: Package[] = []; try { - const req = await axios.get( - "https://s3.amazonaws.com/preview.gui.tea.xyz/packages.json" - ); - log.info("packages received:", req.data.length); - packages = req.data; + return withRetry(async () => { + const req = await axios.get( + "https://s3.amazonaws.com/preview.gui.tea.xyz/packages.json" + ); + log.info("packages received:", req.data.length); + return req.data; + }); } catch (error) { - retryLimit++; log.error("getDistPackagesList:", error); - if (retryLimit < 3) packages = await getDistPackages(); + return []; } - retryLimit = 0; - return packages; } export async function getInstalledPackages(): Promise { @@ -135,36 +132,31 @@ export async function getDeviceAuth(deviceId: string): Promise { export async function getPackageBottles(packageName: string): Promise { try { - const pkg = await apiGet(`packages/${packageName.replaceAll("/", ":")}`); - log.info(`got ${pkg?.bottles?.length || 0} bottles for ${packageName}`); - return (pkg && pkg.bottles) || []; + return withRetry(async () => { + const pkg = await apiGet(`packages/${packageName.replaceAll("/", ":")}`); + log.info(`got ${pkg?.bottles?.length || 0} bottles for ${packageName}`); + return (pkg && pkg.bottles) || []; + }); } catch (error) { - log.error(error); + log.error("getPackageBottles:", error); return []; } } -const retryGetPackage: { [key: string]: number } = {}; export async function getPackage(packageName: string): Promise> { - let pkg: Partial = {}; try { - const data = await apiGet>(`packages/${packageName.replaceAll("/", ":")}`); - if (data) { - pkg = data; - } else { - throw new Error(`package:${packageName} not found`); - } + return await withRetry(async () => { + const data = await apiGet>(`packages/${packageName.replaceAll("/", ":")}`); + if (data) { + return data; + } else { + throw new Error(`package:${packageName} not found`); + } + }); } catch (error) { - log.error(error); - retryGetPackage[packageName] = (retryGetPackage[packageName] || 0) + 1; - if (retryGetPackage[packageName] < 3) { - pkg = await getPackage(packageName); - } else { - log.info(`failed to get package:${packageName} after 3 tries`); - } + log.error("getPackage:", error); + return {}; } - - return pkg; } export const getSession = async (): Promise => { diff --git a/modules/desktop/src/libs/utils/retry.ts b/modules/desktop/src/libs/utils/retry.ts new file mode 100644 index 0000000..08bd8b1 --- /dev/null +++ b/modules/desktop/src/libs/utils/retry.ts @@ -0,0 +1,34 @@ +const log = window.require("electron-log"); + +export type RetryOptions = { + // Number of times to retry. default 10 + maxRetries?: number; + // Initial delay in ms. default 100 + initialDelayMs?: number; + // Maximum delay in ms. default 5000 + maxDelayMs?: number; +}; + +// Retry a function up to maxRetries times, with exponential backoff +// With defaults retry cadence will look like this: +// 100ms, 200ms, 400ms, 800ms, 1600ms, 3200ms, 5000ms, 5000ms, 5000ms, 5000ms +export default async function withRetry( + fn: () => Promise, + { maxRetries = 10, initialDelayMs = 100, maxDelayMs = 5000 }: RetryOptions = {} +) { + let retries = 0; + let currentDelay = initialDelayMs; + while (retries <= maxRetries) { + try { + return await fn(); + } catch (err) { + log.error(err); + retries++; + await wait(currentDelay); + currentDelay = Math.min(currentDelay * 2, maxDelayMs); + } + } + throw new Error(`Failed after ${maxRetries} retries`); +} + +const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/modules/desktop/src/libs/v1-client.ts b/modules/desktop/src/libs/v1-client.ts index a47d678..bcb5be3 100644 --- a/modules/desktop/src/libs/v1-client.ts +++ b/modules/desktop/src/libs/v1-client.ts @@ -9,33 +9,25 @@ export async function get( urlPath: string, params?: { [key: string]: string } ): Promise { - try { - console.log(`GET /v1/${urlPath}`); + console.log(`GET /v1/${urlPath}`); - const [session] = await Promise.all([getSession()]); + const [session] = await Promise.all([getSession()]); - const headers = - session?.device_id && session?.user - ? await getHeaders(`GET/${urlPath}`, session) - : { Authorization: "public " }; + const headers = + session?.device_id && session?.user + ? await getHeaders(`GET/${urlPath}`, session) + : { Authorization: "public " }; - const req = await axios.request({ - method: "GET", - baseURL: "https://api.tea.xyz", - url: ["v1", ...urlPath.split("/")].filter((p) => p).join("/"), - headers, - params - }); + const req = await axios.request({ + method: "GET", + baseURL: "https://api.tea.xyz", + url: ["v1", ...urlPath.split("/")].filter((p) => p).join("/"), + headers, + params, + validateStatus: (status) => status >= 200 && status < 300 + }); - if (req.status == 200) { - return req.data as T; - } else { - return await get(urlPath, params || {}); - } - } catch (error) { - console.error("ERROR", error); - return null; - } + return req.data as T; } async function getHeaders(path: string, session: Session) {