Compare commits

..

No commits in common. "f4bf7ff723706d4097b29889947e07504a87dc60" and "079ecec1295b9bec213c50149c4e5f53428bb2ae" have entirely different histories.

6 changed files with 18 additions and 43 deletions

View file

@ -1,7 +1,6 @@
import { useEffect, type MutableRefObject, type RefObject } from "react"; import { useEffect, type MutableRefObject, type RefObject } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { getTeamForPick } from "~/lib/draft-order"; import { getTeamForPick } from "~/lib/draft-order";
import { clientExpiresAt } from "~/lib/draft-timer";
import { getAutodraftLabel } from "~/components/AutodraftSettings"; import { getAutodraftLabel } from "~/components/AutodraftSettings";
import type { getDraftPicksForSeason } from "~/models/draft-pick"; import type { getDraftPicksForSeason } from "~/models/draft-pick";
import type { findDraftSlotsBySeasonId } from "~/models/draft-slot"; import type { findDraftSlotsBySeasonId } from "~/models/draft-slot";
@ -122,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: clientExpiresAt(data.timeRemaining) }); setPickTimerExpiresAt({ teamId: data.teamId, expiresAt: Date.now() + data.timeRemaining * 1000 });
setIsOvernightPause(false); setIsOvernightPause(false);
setOvernightResumesAt(null); setOvernightResumesAt(null);
}; };
@ -141,16 +140,9 @@ export function useDraftSocketEvents({
setOvernightResumesAt(data.resumesAtUTC ? new Date(data.resumesAtUTC) : null); setOvernightResumesAt(data.resumesAtUTC ? new Date(data.resumesAtUTC) : null);
}; };
const handleDraftPaused = (data: { seasonId: string; paused: boolean; timers?: Array<{ teamId: string; timeRemaining: number }> }) => { const handleDraftPaused = () => {
setIsPaused(true); setIsPaused(true);
setPickTimerExpiresAt(null); setPickTimerExpiresAt(null);
if (data.timers?.length) {
setTeamTimers((prev) => {
const updated = { ...prev };
data.timers?.forEach((t) => { updated[t.teamId] = t.timeRemaining; });
return updated;
});
}
}; };
const handleDraftResumed = () => setIsPaused(false); const handleDraftResumed = () => setIsPaused(false);
const handleDraftCompleted = () => setIsDraftComplete(true); const handleDraftCompleted = () => setIsDraftComplete(true);
@ -257,7 +249,7 @@ export function useDraftSocketEvents({
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) { if (activeTimer) {
setPickTimerExpiresAt({ teamId: activeTimer.teamId, expiresAt: clientExpiresAt(activeTimer.timeRemaining) }); setPickTimerExpiresAt({ teamId: activeTimer.teamId, expiresAt: now + activeTimer.timeRemaining * 1000 });
} else { } else {
setPickTimerExpiresAt(null); setPickTimerExpiresAt(null);
} }

View file

@ -113,15 +113,6 @@ export function getInitialDraftSpeed(season: DraftSpeedSource): string {
return "standard"; return "standard";
} }
/**
* Anchors a server-provided remaining-seconds value to the local client clock.
* Use this everywhere we convert timeRemaining an expiresAt timestamp so the
* logic stays consistent across event handlers and initial state.
*/
export function clientExpiresAt(timeRemaining: number): number {
return Date.now() + timeRemaining * 1000;
}
/** /**
* Returns the Tailwind colour class(es) for a timer value. * Returns the Tailwind colour class(es) for a timer value.
* *

View file

@ -67,19 +67,17 @@ export async function action(args: ActionFunctionArgs) {
}); });
// Cancel the in-memory timeout and snapshot remaining time so resume is accurate. // Cancel the in-memory timeout and snapshot remaining time so resume is accurate.
let snapshots: Array<{ teamId: string; timeRemaining: number }> = [];
try { try {
snapshots = await onDraftPaused(seasonId); await onDraftPaused(seasonId);
} catch (err) { } catch (err) {
logger.error("[Pause] onDraftPaused failed:", err); logger.error("[Pause] onDraftPaused failed:", err);
} }
// Emit socket event with snapshotted timer values so clients sync immediately. // Emit socket event
try { try {
getSocketIO().to(`draft-${seasonId}`).emit("draft-paused", { getSocketIO().to(`draft-${seasonId}`).emit("draft-paused", {
seasonId, seasonId,
paused: true, paused: true,
timers: snapshots,
}); });
} catch (error) { } catch (error) {
logger.error("Socket.IO error:", error); logger.error("Socket.IO error:", error);

View file

@ -42,7 +42,7 @@ import { useDraftNotifications } from "~/hooks/useDraftNotifications";
import { NotificationSettings } from "~/components/NotificationSettings"; import { NotificationSettings } from "~/components/NotificationSettings";
import { AutodraftBadgeWithPopover, AutodraftSettings } from "~/components/AutodraftSettings"; import { AutodraftBadgeWithPopover, AutodraftSettings } from "~/components/AutodraftSettings";
import { toast } from "sonner"; import { toast } from "sonner";
import { clientExpiresAt, formatClockTime, getTimerColorClass } from "~/lib/draft-timer"; import { formatClockTime, getTimerColorClass } from "~/lib/draft-timer";
import { Users, LayoutGrid, ListChecks, Settings, ListOrdered } from "lucide-react"; import { Users, LayoutGrid, ListChecks, Settings, ListOrdered } from "lucide-react";
import type { Route } from "./+types/$leagueId.draft.$seasonId"; import type { Route } from "./+types/$leagueId.draft.$seasonId";
@ -510,7 +510,7 @@ export default function DraftRoom() {
const active = timers.find((t) => t.picksExpiresAt && t.picksExpiresAt.getTime() > loaderTimerNow); const active = timers.find((t) => t.picksExpiresAt && t.picksExpiresAt.getTime() > loaderTimerNow);
if (!active?.picksExpiresAt) return null; if (!active?.picksExpiresAt) return null;
const secondsRemaining = Math.ceil((active.picksExpiresAt.getTime() - loaderTimerNow) / 1000); const secondsRemaining = Math.ceil((active.picksExpiresAt.getTime() - loaderTimerNow) / 1000);
return { teamId: active.teamId, expiresAt: clientExpiresAt(secondsRemaining) }; return { teamId: active.teamId, expiresAt: Date.now() + secondsRemaining * 1000 };
}); });
useEffect(() => { useEffect(() => {

View file

@ -44,7 +44,7 @@ interface ServerToClientEvents {
"team-connected": (data: { teamId: string }) => void; "team-connected": (data: { teamId: string }) => void;
"team-disconnected": (data: { teamId: string }) => void; "team-disconnected": (data: { teamId: string }) => void;
"connected-teams-list": (data: { teamIds: string[] }) => void; "connected-teams-list": (data: { teamIds: string[] }) => void;
"draft-paused": (data: { seasonId: string; paused: boolean; timers?: Array<{ teamId: string; timeRemaining: number }> }) => void; "draft-paused": (data: { seasonId: string; paused: boolean }) => void;
"draft-resumed": (data: { seasonId: string; paused: boolean }) => void; "draft-resumed": (data: { seasonId: string; paused: boolean }) => void;
"participant-removed-from-queues": (data: { participantId: string }) => void; "participant-removed-from-queues": (data: { participantId: string }) => void;
"queue-updated": (data: { queue: Array<{ id: string; teamId: string; seasonId: string; participantId: string; queuePosition: number }> }) => void; "queue-updated": (data: { queue: Array<{ id: string; teamId: string; seasonId: string; participantId: string; queuePosition: number }> }) => void;

View file

@ -84,9 +84,8 @@ function cancelOvernightResumeTimeout(seasonId: string): void {
async function pauseDraftOnError(seasonId: string, teamId: string): Promise<void> { async function pauseDraftOnError(seasonId: string, teamId: string): Promise<void> {
logger.error(`[Timer] Pausing draft ${seasonId} — pick failed for team ${teamId}`); logger.error(`[Timer] Pausing draft ${seasonId} — pick failed for team ${teamId}`);
await db.update(schema.seasons).set({ draftPaused: true }).where(eq(schema.seasons.id, seasonId)); await db.update(schema.seasons).set({ draftPaused: true }).where(eq(schema.seasons.id, seasonId));
const timers = await onDraftPaused(seasonId);
try { try {
getSocketIO().to(`draft-${seasonId}`).emit("draft-paused", { seasonId, paused: true, timers }); getSocketIO().to(`draft-${seasonId}`).emit("draft-paused", { seasonId, paused: true });
} catch (err) { } catch (err) {
logger.error("[Timer] Failed to emit draft-paused:", err); logger.error("[Timer] Failed to emit draft-paused:", err);
} }
@ -412,7 +411,7 @@ export async function rescheduleTimer(seasonId: string): Promise<void> {
* Called by the pause route. Cancels the in-memory timeout and snapshots the * Called by the pause route. Cancels the in-memory timeout and snapshots the
* remaining time into timeRemaining so resume starts from exactly where it left off. * remaining time into timeRemaining so resume starts from exactly where it left off.
*/ */
export async function onDraftPaused(seasonId: string): Promise<Array<{ teamId: string; timeRemaining: number }>> { export async function onDraftPaused(seasonId: string): Promise<void> {
cancelPickTimeout(seasonId); cancelPickTimeout(seasonId);
cancelOvernightResumeTimeout(seasonId); cancelOvernightResumeTimeout(seasonId);
@ -423,19 +422,14 @@ export async function onDraftPaused(seasonId: string): Promise<Array<{ teamId: s
isNotNull(schema.draftTimers.picksExpiresAt), isNotNull(schema.draftTimers.picksExpiresAt),
), ),
}); });
const snapshots = await Promise.all( for (const timer of activeTimers) {
activeTimers if (!timer.picksExpiresAt) continue;
.filter((t) => t.picksExpiresAt !== null) const remaining = Math.max(0, Math.floor((timer.picksExpiresAt.getTime() - now) / 1000));
.map(async (timer) => { await db
const remaining = Math.max(0, Math.floor(((timer.picksExpiresAt?.getTime() ?? now) - now) / 1000)); .update(schema.draftTimers)
await db .set({ timeRemaining: remaining, picksExpiresAt: null, picksStartedAt: null, updatedAt: new Date() })
.update(schema.draftTimers) .where(eq(schema.draftTimers.id, timer.id));
.set({ timeRemaining: remaining, picksExpiresAt: null, picksStartedAt: null, updatedAt: new Date() }) }
.where(eq(schema.draftTimers.id, timer.id));
return { teamId: timer.teamId, timeRemaining: remaining };
})
);
return snapshots;
} }
/** /**