fix(settings): проверять доступность текущего источника без гонок

This commit is contained in:
Denozordec committed 2026-10-03 14:30:41 +07:00
1 parent b6f1b72500
commit e5842b415b
8 files changed
+72 -17

No files matched your search

+35 -14
View File
@@ -1,6 +1,6 @@
"use client"
import { createContext, useContext, useEffect, useState, useCallback } from "react"
import { createContext, useContext, useEffect, useState, useCallback, useMemo, useRef } from "react"
import {
configuredBackendUrl,
defaultDataSourceMode,
@@ -9,7 +9,7 @@ import {
LOCAL_DEFAULT_BACKEND_URL,
resolveStoredBackendUrl,
} from "@/lib/backend-url"
import { resolveApiUrl } from "@/shared/api/http-client"
import { getBackendHealth } from "@/shared/api/backend-health"
// ── types ─────────────────────────────────────────────────────────────────────
@@ -31,6 +31,7 @@ interface DataSourceContextValue {
prefsHydrated: boolean
/** undefined = не проверялось, true = OK, false = недоступен */
backendStatus: boolean | undefined
backendChecking: boolean
checkBackend: () => Promise<void>
}
@@ -68,16 +69,30 @@ export function DataSourceProvider({ children }: { children: React.ReactNode })
const [mode, setModeState] = useState<DataSourceMode>(defaultDataSourceMode)
const [backendUrl, setBackendUrlState] = useState(initialBackendUrl)
const [prefsHydrated, setPrefsHydrated] = useState(false)
const [backendStatus, setBackendStatus] = useState<boolean | undefined>(undefined)
const healthToken = useMemo(() => Symbol(backendUrl), [backendUrl])
const [health, setHealth] = useState<{ token: symbol; value: boolean | undefined; checking: boolean }>(() => ({ token: healthToken, value: undefined, checking: false }))
const backendStatus = health.token === healthToken ? health.value : undefined
const backendChecking = health.token === healthToken && health.checking
const healthSession = useRef<{token:symbol;active:boolean;revision:number;controller?:AbortController}>({token:healthToken,active:true,revision:0})
useEffect(() => {
const scope = {token:healthToken,active:true,revision:0,controller:undefined as AbortController | undefined}
healthSession.current = scope
return () => { scope.active = false; scope.controller?.abort() }
}, [healthToken])
const backendUrlLocked = isBackendUrlLocked()
const mockModeAvailable = isMockDataSourceAvailable()
useEffect(() => {
const storedMode = readStoredMode()
const url = resolveStoredBackendUrl(localStorage.getItem(LS_BACKEND))
setModeState(storedMode)
setBackendUrlState(url)
setPrefsHydrated(true)
let active = true
queueMicrotask(() => {
if (!active) return
const storedMode = readStoredMode()
const url = resolveStoredBackendUrl(localStorage.getItem(LS_BACKEND))
setModeState(storedMode)
setBackendUrlState(url)
setPrefsHydrated(true)
})
return () => { active = false }
}, [])
const setMode = useCallback((m: DataSourceMode) => {
@@ -94,14 +109,20 @@ export function DataSourceProvider({ children }: { children: React.ReactNode })
}, [backendUrlLocked])
const checkBackend = useCallback(async () => {
const healthUrl = resolveApiUrl(backendUrl, "/health")
const scope = healthSession.current
if (!scope.active || scope.token !== healthToken) return
const revision = ++scope.revision
scope.controller?.abort()
const controller = new AbortController(); scope.controller = controller
const current = () => scope.active && scope.revision === revision && !controller.signal.aborted
setHealth({token:healthToken,value:undefined,checking:true})
try {
const res = await fetch(healthUrl, { signal: AbortSignal.timeout(3000) })
setBackendStatus(res.ok)
await getBackendHealth(backendUrl, controller.signal)
if (current()) setHealth({token:healthToken,value:true,checking:false})
} catch {
setBackendStatus(false)
if (current()) setHealth({token:healthToken,value:false,checking:false})
}
}, [backendUrl])
}, [backendUrl, healthToken])
useEffect(() => {
if (!prefsHydrated || mode !== "live") return
@@ -110,7 +131,7 @@ export function DataSourceProvider({ children }: { children: React.ReactNode })
return (
<DataSourceContext.Provider
value={{ mode, setMode, backendUrl, setBackendUrl, backendUrlLocked, mockModeAvailable, prefsHydrated, backendStatus, checkBackend }}
value={{ mode, setMode, backendUrl, setBackendUrl, backendUrlLocked, mockModeAvailable, prefsHydrated, backendStatus, backendChecking, checkBackend }}
>
{children}
</DataSourceContext.Provider>