Files
MikrotikManager/lib/data-source.tsx

146 lines
5.8 KiB
TypeScript

"use client"
import { createContext, useContext, useEffect, useState, useCallback, useMemo, useRef } from "react"
import {
configuredBackendUrl,
defaultDataSourceMode,
isBackendUrlLocked,
isMockDataSourceAvailable,
LOCAL_DEFAULT_BACKEND_URL,
resolveStoredBackendUrl,
} from "@/lib/backend-url"
import { getBackendHealth } from "@/shared/api/backend-health"
// ── types ─────────────────────────────────────────────────────────────────────
export type DataSourceMode = "mock" | "live"
/** Режим «Живые» в настройках: моковые данные не подставлять. */
export function isLiveDataSourceMode(mode: DataSourceMode): boolean {
return mode === "live"
}
interface DataSourceContextValue {
mode: DataSourceMode
setMode: (m: DataSourceMode) => void
backendUrl: string
setBackendUrl: (url: string) => void
backendUrlLocked: boolean
mockModeAvailable: boolean
/** localStorage для mode/backendUrl применён на клиенте (после первого paint). */
prefsHydrated: boolean
/** undefined = не проверялось, true = OK, false = недоступен */
backendStatus: boolean | undefined
backendChecking: boolean
checkBackend: () => Promise<void>
}
// ── context ───────────────────────────────────────────────────────────────────
const DataSourceContext = createContext<DataSourceContextValue | null>(null)
const LS_MODE = "routerlists:data-source"
const LS_BACKEND = "routerlists:backend-url"
function readStoredMode(): DataSourceMode {
if (!isMockDataSourceAvailable()) return "live"
if (typeof window === "undefined") return defaultDataSourceMode()
const stored = localStorage.getItem(LS_MODE) as DataSourceMode | null
if (stored === "live") return "live"
if (stored === "mock") return "mock"
return defaultDataSourceMode()
}
function initialBackendUrl(): string {
if (typeof window === "undefined") {
const cfg = configuredBackendUrl()
if (cfg.kind === "same-origin") return ""
if (cfg.kind === "fixed") return cfg.url
return LOCAL_DEFAULT_BACKEND_URL
}
return resolveStoredBackendUrl(localStorage.getItem(LS_BACKEND))
}
function normalizeBackendUrl(url: string): string {
return url.trim().replace(/\/$/, "")
}
export function DataSourceProvider({ children }: { children: React.ReactNode }) {
const [mode, setModeState] = useState<DataSourceMode>(defaultDataSourceMode)
const [backendUrl, setBackendUrlState] = useState(initialBackendUrl)
const [prefsHydrated, setPrefsHydrated] = useState(false)
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(() => {
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) => {
if (!mockModeAvailable && m === "mock") return
setModeState(m)
localStorage.setItem(LS_MODE, m)
}, [mockModeAvailable])
const setBackendUrl = useCallback((url: string) => {
if (backendUrlLocked) return
const normalized = normalizeBackendUrl(url)
setBackendUrlState(normalized)
localStorage.setItem(LS_BACKEND, normalized)
}, [backendUrlLocked])
const checkBackend = useCallback(async () => {
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 {
await getBackendHealth(backendUrl, controller.signal)
if (current()) setHealth({token:healthToken,value:true,checking:false})
} catch {
if (current()) setHealth({token:healthToken,value:false,checking:false})
}
}, [backendUrl, healthToken])
useEffect(() => {
if (!prefsHydrated || mode !== "live") return
queueMicrotask(() => { void checkBackend() })
}, [prefsHydrated, mode, checkBackend])
return (
<DataSourceContext.Provider
value={{ mode, setMode, backendUrl, setBackendUrl, backendUrlLocked, mockModeAvailable, prefsHydrated, backendStatus, backendChecking, checkBackend }}
>
{children}
</DataSourceContext.Provider>
)
}
export function useDataSource(): DataSourceContextValue {
const ctx = useContext(DataSourceContext)
if (!ctx) throw new Error("useDataSource must be used inside <DataSourceProvider>")
return ctx
}