/** * React binding for the relay reconnect controller. * * Delegates all reconnect logic to the module-level `relayReconnectController` * singleton so that all mounted hook instances (banner + sidebar card) share * a single in-flight state. Deliberately uses `preconnect()` rather than the * full `reconnectCommunity()` path to avoid unmounting the React tree and * clearing draft state. * * See `relayReconnectController.ts` for the three-phase strategy details. */ import * as React from "react"; import { useQueryClient } from "@tanstack/react-query"; import { invoke } from "@tauri-apps/api/core"; import { toast } from "sonner"; import { relayClient } from "@/shared/api/relayClient"; import { isRelayDependentQuery } from "@/shared/api/relayQueryInvalidation"; import { relayReconnectController } from "@/shared/api/relayReconnectController"; function buildDeps(onSuccess: () => void, onBackstop: () => void) { return { preconnect: () => relayClient.preconnect(), hookConfigured: () => invoke("relay_reconnect_hook_configured"), runHook: () => invoke("relay_reconnect_hook"), subscribeToConnectionState: (listener: (state: string) => void) => relayClient.subscribeToConnectionState(listener), onSuccess, onBackstop, setTimeout: window.setTimeout.bind(window), clearTimeout: window.clearTimeout.bind(window), }; } export function useReconnectRelay(): { reconnect: () => Promise; isPending: boolean; isWaitingOnReconnectHook: boolean; } { const queryClient = useQueryClient(); const [controllerState, setControllerState] = React.useState(() => relayReconnectController.getState(), ); // Subscribe to controller state changes for the lifetime of this component. // Multiple hook instances receive the same state from the shared singleton. React.useEffect(() => { return relayReconnectController.subscribe(setControllerState); }, []); // Stable mutable refs for callbacks — updated every render so stale closures // are never captured, but the reconnect callback itself never changes identity. const onSuccessRef = React.useRef<(() => void) | null>(null); const onBackstopRef = React.useRef<(() => void) | null>(null); onSuccessRef.current = React.useCallback(() => { // Defer query invalidation so callers render the recovered state first. window.setTimeout(() => { void queryClient .invalidateQueries({ predicate: isRelayDependentQuery }) .catch((err) => { console.error( "[useReconnectRelay] failed to refresh queries after reconnect:", err, ); }); }, 0); }, [queryClient]); onBackstopRef.current = React.useCallback(() => { toast("Still trying to reconnect — check your network."); }, []); const reconnect = React.useCallback(async () => { const deps = buildDeps( () => onSuccessRef.current?.(), () => onBackstopRef.current?.(), ); return relayReconnectController.start(deps); }, []); return { reconnect, isPending: controllerState.isPending, isWaitingOnReconnectHook: controllerState.isWaitingOnReconnectHook, }; }