import { createContext, type ReactNode, useCallback, useContext, useMemo, useRef, useState, } from "react"; import { ConfirmModal } from "@/components/dialog/ConfirmModal"; import i18next from "@/lib/i18n"; // Nothing on the daemon path carries a deadline, so a hung call would leave the // modal spinning with no way out. Cancel comes back once the wait stops looking // normal, and the wait is abandoned entirely at the deadline. const CANCELLABLE_AFTER_MS = 15_000; const TIMEOUT_MS = 30_000; const withTimeout = async (action: () => Promise) => { let timer: ReturnType | undefined; const expiry = new Promise((_, reject) => { timer = setTimeout( () => reject(new Error(i18next.t("error.daemon_unreachable"))), TIMEOUT_MS, ); }); try { await Promise.race([action(), expiry]); } finally { clearTimeout(timer); } }; export type ConfirmOptions = { title: ReactNode; description: ReactNode; confirmLabel: string; cancelLabel?: string; danger?: boolean; onConfirm?: () => Promise; }; type DialogContextValue = { confirm: (options: ConfirmOptions) => Promise; }; const DialogContext = createContext(null); type Settler = { resolve: (result: boolean) => void; reject: (reason: unknown) => void }; export function DialogProvider({ children }: Readonly<{ children: ReactNode }>) { const [open, setOpen] = useState(false); const [busy, setBusy] = useState(false); const [stalled, setStalled] = useState(false); const [options, setOptions] = useState(null); const resolverRef = useRef(null); const confirm = useCallback((opts: ConfirmOptions) => { setOptions(opts); setOpen(true); return new Promise((resolve, reject) => { resolverRef.current = { resolve, reject }; }); }, []); const take = (expected?: Settler | null) => { const settler = resolverRef.current; if (expected && settler !== expected) return null; resolverRef.current = null; setBusy(false); setStalled(false); setOpen(false); return settler; }; const handleConfirm = async () => { const action = options?.onConfirm; if (!action) { take()?.resolve(true); return; } const dispatched = resolverRef.current; setBusy(true); const stallTimer = setTimeout(() => setStalled(true), CANCELLABLE_AFTER_MS); try { await withTimeout(action); take(dispatched)?.resolve(true); } catch (e) { take(dispatched)?.reject(e); } finally { clearTimeout(stallTimer); } }; const value = useMemo(() => ({ confirm }), [confirm]); return ( {children} void handleConfirm()} onCancel={() => take()?.resolve(false)} /> ); } export const useConfirm = () => { const ctx = useContext(DialogContext); if (!ctx) throw new Error("useConfirm must be used within a DialogProvider"); return ctx.confirm; };