Fix draft timer display drifting due to server-client clock skew #74
2 changed files with 13 additions and 7 deletions
|
|
@ -121,7 +121,7 @@ export function useDraftSocketEvents({
|
||||||
}) => {
|
}) => {
|
||||||
setCurrentPick(data.pickNumber);
|
setCurrentPick(data.pickNumber);
|
||||||
setTeamTimers((prev) => ({ ...prev, [data.teamId]: data.timeRemaining }));
|
setTeamTimers((prev) => ({ ...prev, [data.teamId]: data.timeRemaining }));
|
||||||
setPickTimerExpiresAt({ teamId: data.teamId, expiresAt: data.expiresAt });
|
setPickTimerExpiresAt({ teamId: data.teamId, expiresAt: Date.now() + data.timeRemaining * 1000 });
|
||||||
setIsOvernightPause(false);
|
setIsOvernightPause(false);
|
||||||
setOvernightResumesAt(null);
|
setOvernightResumesAt(null);
|
||||||
};
|
};
|
||||||
|
|
@ -248,8 +248,8 @@ export function useDraftSocketEvents({
|
||||||
// so a reconnect after autopick fired doesn't briefly show a stale countdown.
|
// so a reconnect after autopick fired doesn't briefly show a stale countdown.
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const activeTimer = data.timers.find((t) => t.expiresAt !== undefined && t.expiresAt > now);
|
const activeTimer = data.timers.find((t) => t.expiresAt !== undefined && t.expiresAt > now);
|
||||||
if (activeTimer?.expiresAt) {
|
if (activeTimer) {
|
||||||
setPickTimerExpiresAt({ teamId: activeTimer.teamId, expiresAt: activeTimer.expiresAt });
|
setPickTimerExpiresAt({ teamId: activeTimer.teamId, expiresAt: now + activeTimer.timeRemaining * 1000 });
|
||||||
} else {
|
} else {
|
||||||
setPickTimerExpiresAt(null);
|
setPickTimerExpiresAt(null);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,7 @@ export async function loader(args: Route.LoaderArgs) {
|
||||||
getSeasonTimers(seasonId),
|
getSeasonTimers(seasonId),
|
||||||
getSeasonAutodraftSettings(seasonId),
|
getSeasonAutodraftSettings(seasonId),
|
||||||
]);
|
]);
|
||||||
|
const loaderTimerNow = Date.now();
|
||||||
|
|
||||||
const userQueue = userTeam
|
const userQueue = userTeam
|
||||||
? await getTeamQueue(userTeam.id).catch((err) => {
|
? await getTeamQueue(userTeam.id).catch((err) => {
|
||||||
|
|
@ -177,6 +178,7 @@ export async function loader(args: Route.LoaderArgs) {
|
||||||
userQueue,
|
userQueue,
|
||||||
userWatchlist: userWatchlist.map((w) => w.participantId),
|
userWatchlist: userWatchlist.map((w) => w.participantId),
|
||||||
timers,
|
timers,
|
||||||
|
loaderTimerNow,
|
||||||
autodraftSettings,
|
autodraftSettings,
|
||||||
userAutodraftSettings,
|
userAutodraftSettings,
|
||||||
isCommissioner: !!isCommissioner,
|
isCommissioner: !!isCommissioner,
|
||||||
|
|
@ -196,6 +198,7 @@ export default function DraftRoom() {
|
||||||
userQueue,
|
userQueue,
|
||||||
userWatchlist,
|
userWatchlist,
|
||||||
timers,
|
timers,
|
||||||
|
loaderTimerNow,
|
||||||
autodraftSettings,
|
autodraftSettings,
|
||||||
userAutodraftSettings,
|
userAutodraftSettings,
|
||||||
isCommissioner,
|
isCommissioner,
|
||||||
|
|
@ -500,11 +503,14 @@ export default function DraftRoom() {
|
||||||
}, []);
|
}, []);
|
||||||
useEffect(() => () => { animationTimersRef.current.forEach(clearTimeout); }, []);
|
useEffect(() => () => { animationTimersRef.current.forEach(clearTimeout); }, []);
|
||||||
|
|
||||||
// Client-side countdown: updated every second from the server-provided expiresAt timestamp.
|
// Client-side countdown: always anchored to the client clock (not the server timestamp) to avoid skew.
|
||||||
const [pickTimerExpiresAt, setPickTimerExpiresAt] = useState<{ teamId: string; expiresAt: number } | null>(() => {
|
const [pickTimerExpiresAt, setPickTimerExpiresAt] = useState<{ teamId: string; expiresAt: number } | null>(() => {
|
||||||
// Seed from initial loader data if a timer is already running.
|
// loaderTimerNow is server-side Date.now() — subtract from server expiresAt to get ms remaining
|
||||||
const active = timers.find((t) => t.picksExpiresAt && t.picksExpiresAt.getTime() > Date.now());
|
// without skew, then re-anchor to client clock.
|
||||||
return active ? { teamId: active.teamId, expiresAt: active.picksExpiresAt?.getTime() ?? 0 } : null;
|
const active = timers.find((t) => t.picksExpiresAt && t.picksExpiresAt.getTime() > loaderTimerNow);
|
||||||
|
if (!active?.picksExpiresAt) return null;
|
||||||
|
const secondsRemaining = Math.ceil((active.picksExpiresAt.getTime() - loaderTimerNow) / 1000);
|
||||||
|
return { teamId: active.teamId, expiresAt: Date.now() + secondsRemaining * 1000 };
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue