import { useState, useEffect, useCallback, useRef } from "react"; import { authClient } from "~/lib/auth-client"; import type { getDraftPicksForSeason } from "~/models/draft-pick"; import type { findSeasonWithTeamsAndLeague } from "~/models/season"; import type { getSeasonTimers } from "~/models/draft-timer"; import type { getSeasonAutodraftSettings } from "~/models/autodraft-settings"; import type { AutodraftStatusEntry, QueueItem } from "~/hooks/useDraftRoomState"; type DraftPicks = Awaited>; type Season = NonNullable>>; type Timers = Awaited>; type AutodraftSettings = Awaited>; interface UseDraftAuthRecoveryParams { reconnectCount: number; revalidate: () => void; revalidatorState: "idle" | "loading" | "submitting"; currentUserId: string | null | undefined; userAutodraftSettings: { isEnabled: boolean; mode: "next_pick" | "while_on"; queueOnly: boolean } | null; // Loader data for revalidation sync draftPicks: DraftPicks; season: Season; userQueue: QueueItem[]; timers: Timers; autodraftSettings: AutodraftSettings; // State setters to sync after revalidation setPicks: (picks: DraftPicks) => void; setCurrentPick: (pick: number) => void; setIsPaused: (paused: boolean) => void; setIsDraftComplete: (complete: boolean) => void; setQueue: (queue: QueueItem[]) => void; setTeamTimers: (fn: (prev: Record) => Record) => void; setAutodraftStatus: (fn: () => Record) => void; } export function useDraftAuthRecovery({ reconnectCount, revalidate, revalidatorState, currentUserId, userAutodraftSettings, draftPicks, season, userQueue, timers, autodraftSettings, setPicks, setCurrentPick, setIsPaused, setIsDraftComplete, setQueue, setTeamTimers, setAutodraftStatus, }: UseDraftAuthRecoveryParams) { const [authDegraded, setAuthDegraded] = useState(false); const [userAutodraft, setUserAutodraft] = useState({ isEnabled: userAutodraftSettings?.isEnabled || false, mode: (userAutodraftSettings?.mode || "next_pick") as "next_pick" | "while_on", queueOnly: userAutodraftSettings?.queueOnly || false, }); const userAutodraftRef = useRef(userAutodraft); useEffect(() => { userAutodraftRef.current = userAutodraft; }, [userAutodraft]); // Re-fetch loader data after each reconnect so stale picks/timers are refreshed. const revalidationRetryRef = useRef | null>(null); useEffect(() => { if (reconnectCount > 0) { revalidate(); if (revalidationRetryRef.current) { clearTimeout(revalidationRetryRef.current); } revalidationRetryRef.current = setTimeout(() => { revalidate(); revalidationRetryRef.current = null; }, 3000); } return () => { if (revalidationRetryRef.current) { clearTimeout(revalidationRetryRef.current); } }; }, [reconnectCount, revalidate]); // Re-check session when the tab becomes visible — cookie-based sessions don't // need manual token refresh, but a long-idle tab may have an expired session. useEffect(() => { if (authDegraded) return; let aborted = false; let inFlight = false; const handleVisibilityChange = async () => { if (document.visibilityState !== "visible" || inFlight) return; inFlight = true; try { const { data: session } = await authClient.getSession(); if (aborted) return; if (session) { if (!currentUserId) revalidate(); } else { setAuthDegraded(true); } } catch { if (!aborted) setAuthDegraded(true); } finally { inFlight = false; } }; document.addEventListener("visibilitychange", handleVisibilityChange); return () => { aborted = true; document.removeEventListener("visibilitychange", handleVisibilityChange); }; }, [currentUserId, authDegraded, revalidate]); // Track revalidation lifecycle to sync local state from fresh loader data. const revalidatorStateRef = useRef(revalidatorState); const isRevalidatingRef = useRef(false); const pendingPicksDuringRevalidationRef = useRef([]); const draftPicksAtRevalidationStartRef = useRef(draftPicks); const userQueueAtRevalidationStartRef = useRef(userQueue); const pendingQueueMutationsRef = useRef(0); useEffect(() => { const prev = revalidatorStateRef.current; revalidatorStateRef.current = revalidatorState; if (prev !== "loading" && revalidatorState === "loading") { isRevalidatingRef.current = true; pendingPicksDuringRevalidationRef.current = []; draftPicksAtRevalidationStartRef.current = draftPicks; userQueueAtRevalidationStartRef.current = userQueue; } else if (prev === "loading" && revalidatorState === "idle") { isRevalidatingRef.current = false; if (draftPicks === draftPicksAtRevalidationStartRef.current) { pendingPicksDuringRevalidationRef.current = []; return; } const dbPickIds = new Set(draftPicks.map((p) => p.id)); const missedPicks = pendingPicksDuringRevalidationRef.current.filter( (p) => !dbPickIds.has(p.id) ); pendingPicksDuringRevalidationRef.current = []; setPicks([...draftPicks, ...missedPicks]); setCurrentPick(season.currentPickNumber || 1); setIsPaused(season.draftPaused || false); setIsDraftComplete( season.status === "active" || season.status === "completed" ); if (currentUserId && userQueue !== userQueueAtRevalidationStartRef.current) { setQueue(userQueue); } if (timers.length > 0) { setTeamTimers((currentTimers) => { const updated = { ...currentTimers }; timers.forEach((timer) => { updated[timer.teamId] = timer.timeRemaining; }); return updated; }); } setAutodraftStatus(() => { const status: Record = {}; autodraftSettings.forEach((setting) => { status[setting.teamId] = { isEnabled: setting.isEnabled, mode: setting.mode, queueOnly: setting.queueOnly, }; }); return status; }); } }, [revalidatorState, draftPicks, season, userQueue, timers, autodraftSettings, currentUserId, setPicks, setCurrentPick, setIsPaused, setIsDraftComplete, setQueue, setTeamTimers, setAutodraftStatus]); const authFetch = useCallback(async (url: string, init?: RequestInit): Promise => { const response = await fetch(url, init); if (response.status === 401) { setAuthDegraded(true); return null; } return response; }, []); return { authDegraded, authFetch, userAutodraft, setUserAutodraft, userAutodraftRef, isRevalidatingRef, pendingPicksDuringRevalidationRef, pendingQueueMutationsRef, }; }