"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 } // ── context ─────────────────────────────────────────────────────────────────── const DataSourceContext = createContext(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(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 ( {children} ) } export function useDataSource(): DataSourceContextValue { const ctx = useContext(DataSourceContext) if (!ctx) throw new Error("useDataSource must be used inside ") return ctx }