brackt/app/hooks/useDraftAuthRecovery.ts
Chris Parsons e3a7a1a9b1
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 1m24s
🚀 Deploy / ʦ TypeScript (pull_request) Successful in 1m13s
🚀 Deploy / 🔍 Lint (pull_request) Successful in 45s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
Fix autodraft settings panel not reflecting auto-disable after pick
When next_queue autodraft fires and turns itself off, two bugs prevented
the UI from behaving correctly:

1. After reconnect, useDraftAuthRecovery synced autodraftStatus (draft
   grid) but not userAutodraft (settings panel), leaving the settings
   badge stale until the next page load.

2. Toast notifications for autodraft auto-disable were unreliable: the
   condition checked prev.mode rather than why the server disabled it,
   causing the wrong toast for manual disables and queue-empty events.

Fix the reconnect sync by calling setUserAutodraft in the revalidation
effect alongside setAutodraftStatus. Fix the toast logic by adding a
reason field ("pick_complete" | "queue_empty") to the server's
autodraft-updated emit, so the client can show the right message without
guessing from local state. Also removes the now-unused userAutodraftRef.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 18:29:13 -07:00

224 lines
7.6 KiB
TypeScript

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<ReturnType<typeof getDraftPicksForSeason>>;
type Season = NonNullable<Awaited<ReturnType<typeof findSeasonWithTeamsAndLeague>>>;
type Timers = Awaited<ReturnType<typeof getSeasonTimers>>;
type AutodraftSettings = Awaited<ReturnType<typeof getSeasonAutodraftSettings>>;
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<string, number>) => Record<string, number>) => void;
setAutodraftStatus: (fn: () => Record<string, AutodraftStatusEntry>) => void;
setIsSyncing: (value: boolean) => void;
}
export function useDraftAuthRecovery({
reconnectCount,
revalidate,
revalidatorState,
currentUserId,
userAutodraftSettings,
draftPicks,
season,
userQueue,
timers,
autodraftSettings,
setPicks,
setCurrentPick,
setIsPaused,
setIsDraftComplete,
setQueue,
setTeamTimers,
setAutodraftStatus,
setIsSyncing,
}: 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,
});
// Re-fetch loader data after each reconnect so stale picks/timers are refreshed.
const revalidationRetryRef = useRef<ReturnType<typeof setTimeout> | 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]);
// Periodically ping the session endpoint to keep it alive during long drafts.
useEffect(() => {
if (authDegraded) return;
const FIFTEEN_MINUTES = 15 * 60 * 1000;
const id = setInterval(async () => {
const { data: session } = await authClient.getSession();
if (!session) setAuthDegraded(true);
}, FIFTEEN_MINUTES);
return () => clearInterval(id);
}, [authDegraded]);
// Track revalidation lifecycle to sync local state from fresh loader data.
const revalidatorStateRef = useRef(revalidatorState);
const isRevalidatingRef = useRef(false);
const pendingPicksDuringRevalidationRef = useRef<DraftPicks>([]);
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<string, AutodraftStatusEntry> = {};
autodraftSettings.forEach((setting) => {
status[setting.teamId] = {
isEnabled: setting.isEnabled,
mode: setting.mode,
queueOnly: setting.queueOnly,
};
});
return status;
});
if (userAutodraftSettings) {
setUserAutodraft({
isEnabled: userAutodraftSettings.isEnabled,
mode: userAutodraftSettings.mode,
queueOnly: userAutodraftSettings.queueOnly,
});
}
setIsSyncing(false);
}
}, [revalidatorState, draftPicks, season, userQueue, timers, autodraftSettings, currentUserId, userAutodraftSettings,
setPicks, setCurrentPick, setIsPaused, setIsDraftComplete, setQueue, setTeamTimers, setAutodraftStatus, setUserAutodraft, setIsSyncing]);
const authFetch = useCallback(async (url: string, init?: RequestInit): Promise<Response | null> => {
const response = await fetch(url, init);
if (response.status === 401) {
setAuthDegraded(true);
return null;
}
return response;
}, []);
return {
authDegraded,
authFetch,
userAutodraft,
setUserAutodraft,
isRevalidatingRef,
pendingPicksDuringRevalidationRef,
pendingQueueMutationsRef,
};
}