chore: refactoring and tests (#12629)
This commit is contained in:
@@ -5,10 +5,50 @@ 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) {
|
||||
return (AbortSignal as unknown as { timeout?: (ms: number) => AbortSignal }).timeout?.(timeoutMs)
|
||||
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(
|
||||
@@ -16,14 +56,24 @@ export async function checkServerHealth(
|
||||
fetch: typeof globalThis.fetch,
|
||||
opts?: CheckServerHealthOptions,
|
||||
): Promise<ServerHealth> {
|
||||
const signal = opts?.signal ?? timeoutSignal(opts?.timeoutMs ?? 3000)
|
||||
const sdk = createOpencodeClient({
|
||||
baseUrl: url,
|
||||
fetch,
|
||||
signal,
|
||||
})
|
||||
return sdk.global
|
||||
.health()
|
||||
.then((x) => ({ healthy: x.data?.healthy === true, version: x.data?.version }))
|
||||
.catch(() => ({ healthy: false }))
|
||||
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> =>
|
||||
createOpencodeClient({
|
||||
baseUrl: url,
|
||||
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?.())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user