tf_code/packages/app/src/utils/server-health.ts

92 lines
3.1 KiB
TypeScript

import { usePlatform } from "@/context/platform"
import type { ServerConnection } from "@/context/server"
import { createSdkForServer } from "./server"
export type ServerHealth = { healthy: boolean; version?: string }
interface CheckServerHealthOptions {
timeoutMs?: number
signal?: AbortSignal
retryCount?: number
retryDelayMs?: number
}
const defaultTimeoutMs = 3000
const defaultRetryCount = 2
const defaultRetryDelayMs = 100
function timeoutSignal(timeoutMs: number) {
const timeout = (AbortSignal as unknown as { timeout?: (ms: number) => AbortSignal }).timeout
if (timeout) {
try {
return {
signal: timeout.call(AbortSignal, timeoutMs),
clear: undefined as (() => void) | undefined,
}
} catch {}
}
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), timeoutMs)
return { signal: controller.signal, clear: () => clearTimeout(timer) }
}
function wait(ms: number, signal?: AbortSignal) {
return new Promise<void>((resolve, reject) => {
if (signal?.aborted) {
reject(new DOMException("Aborted", "AbortError"))
return
}
const timer = setTimeout(() => {
signal?.removeEventListener("abort", onAbort)
resolve()
}, ms)
const onAbort = () => {
clearTimeout(timer)
reject(new DOMException("Aborted", "AbortError"))
}
signal?.addEventListener("abort", onAbort, { once: true })
})
}
function retryable(error: unknown, signal?: AbortSignal) {
if (signal?.aborted) return false
if (!(error instanceof Error)) return false
if (error.name === "AbortError" || error.name === "TimeoutError") return false
if (error instanceof TypeError) return true
return /network|fetch|econnreset|econnrefused|enotfound|timedout/i.test(error.message)
}
export async function checkServerHealth(
server: ServerConnection.HttpBase,
fetch: typeof globalThis.fetch,
opts?: CheckServerHealthOptions,
): Promise<ServerHealth> {
const timeout = opts?.signal ? undefined : timeoutSignal(opts?.timeoutMs ?? defaultTimeoutMs)
const signal = opts?.signal ?? timeout?.signal
const retryCount = opts?.retryCount ?? defaultRetryCount
const retryDelayMs = opts?.retryDelayMs ?? defaultRetryDelayMs
const next = (count: number, error: unknown) => {
if (count >= retryCount || !retryable(error, signal)) return Promise.resolve({ healthy: false } as const)
return wait(retryDelayMs * (count + 1), signal)
.then(() => attempt(count + 1))
.catch(() => ({ healthy: false }))
}
const attempt = (count: number): Promise<ServerHealth> =>
createSdkForServer({
server,
fetch,
signal,
})
.global.health()
.then((x) => (x.error ? next(count, x.error) : { healthy: x.data?.healthy === true, version: x.data?.version }))
.catch((error) => next(count, error))
return attempt(0).finally(() => timeout?.clear?.())
}
export function useCheckServerHealth() {
const platform = usePlatform()
const fetcher = platform.fetch ?? globalThis.fetch
return (http: ServerConnection.HttpBase) => checkServerHealth(http, fetcher)
}