Fix three timer-pause gaps from code review #75
6 changed files with 43 additions and 18 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import { useEffect, type MutableRefObject, type RefObject } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { getTeamForPick } from "~/lib/draft-order";
|
||||
import { clientExpiresAt } from "~/lib/draft-timer";
|
||||
import { getAutodraftLabel } from "~/components/AutodraftSettings";
|
||||
import type { getDraftPicksForSeason } from "~/models/draft-pick";
|
||||
import type { findDraftSlotsBySeasonId } from "~/models/draft-slot";
|
||||
|
|
@ -121,7 +122,7 @@ export function useDraftSocketEvents({
|
|||
}) => {
|
||||
setCurrentPick(data.pickNumber);
|
||||
setTeamTimers((prev) => ({ ...prev, [data.teamId]: data.timeRemaining }));
|
||||
setPickTimerExpiresAt({ teamId: data.teamId, expiresAt: Date.now() + data.timeRemaining * 1000 });
|
||||
setPickTimerExpiresAt({ teamId: data.teamId, expiresAt: clientExpiresAt(data.timeRemaining) });
|
||||
setIsOvernightPause(false);
|
||||
setOvernightResumesAt(null);
|
||||
};
|
||||
|
|
@ -140,9 +141,16 @@ export function useDraftSocketEvents({
|
|||
setOvernightResumesAt(data.resumesAtUTC ? new Date(data.resumesAtUTC) : null);
|
||||
};
|
||||
|
||||
const handleDraftPaused = () => {
|
||||
const handleDraftPaused = (data: { seasonId: string; paused: boolean; timers?: Array<{ teamId: string; timeRemaining: number }> }) => {
|
||||
setIsPaused(true);
|
||||
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 handleDraftCompleted = () => setIsDraftComplete(true);
|
||||
|
|
@ -249,7 +257,7 @@ export function useDraftSocketEvents({
|
|||
const now = Date.now();
|
||||
const activeTimer = data.timers.find((t) => t.expiresAt !== undefined && t.expiresAt > now);
|
||||
if (activeTimer) {
|
||||
setPickTimerExpiresAt({ teamId: activeTimer.teamId, expiresAt: now + activeTimer.timeRemaining * 1000 });
|
||||
setPickTimerExpiresAt({ teamId: activeTimer.teamId, expiresAt: clientExpiresAt(activeTimer.timeRemaining) });
|
||||
} else {
|
||||
setPickTimerExpiresAt(null);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,6 +113,15 @@ export function getInitialDraftSpeed(season: DraftSpeedSource): string {
|
|||
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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -67,17 +67,19 @@ export async function action(args: ActionFunctionArgs) {
|
|||
});
|
||||
|
||||
// Cancel the in-memory timeout and snapshot remaining time so resume is accurate.
|
||||
let snapshots: Array<{ teamId: string; timeRemaining: number }> = [];
|
||||
try {
|
||||
await onDraftPaused(seasonId);
|
||||
snapshots = await onDraftPaused(seasonId);
|
||||
} catch (err) {
|
||||
logger.error("[Pause] onDraftPaused failed:", err);
|
||||
}
|
||||
|
||||
// Emit socket event
|
||||
// Emit socket event with snapshotted timer values so clients sync immediately.
|
||||
try {
|
||||
getSocketIO().to(`draft-${seasonId}`).emit("draft-paused", {
|
||||
seasonId,
|
||||
paused: true,
|
||||
timers: snapshots,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Socket.IO error:", error);
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ import { useDraftNotifications } from "~/hooks/useDraftNotifications";
|
|||
import { NotificationSettings } from "~/components/NotificationSettings";
|
||||
import { AutodraftBadgeWithPopover, AutodraftSettings } from "~/components/AutodraftSettings";
|
||||
import { toast } from "sonner";
|
||||
import { formatClockTime, getTimerColorClass } from "~/lib/draft-timer";
|
||||
import { clientExpiresAt, formatClockTime, getTimerColorClass } from "~/lib/draft-timer";
|
||||
import { Users, LayoutGrid, ListChecks, Settings, ListOrdered } from "lucide-react";
|
||||
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);
|
||||
if (!active?.picksExpiresAt) return null;
|
||||
const secondsRemaining = Math.ceil((active.picksExpiresAt.getTime() - loaderTimerNow) / 1000);
|
||||
return { teamId: active.teamId, expiresAt: Date.now() + secondsRemaining * 1000 };
|
||||
return { teamId: active.teamId, expiresAt: clientExpiresAt(secondsRemaining) };
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ interface ServerToClientEvents {
|
|||
"team-connected": (data: { teamId: string }) => void;
|
||||
"team-disconnected": (data: { teamId: string }) => void;
|
||||
"connected-teams-list": (data: { teamIds: string[] }) => void;
|
||||
"draft-paused": (data: { seasonId: string; paused: boolean }) => void;
|
||||
"draft-paused": (data: { seasonId: string; paused: boolean; timers?: Array<{ teamId: string; timeRemaining: number }> }) => void;
|
||||
"draft-resumed": (data: { seasonId: string; paused: boolean }) => 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;
|
||||
|
|
|
|||
|
|
@ -84,8 +84,9 @@ function cancelOvernightResumeTimeout(seasonId: string): void {
|
|||
async function pauseDraftOnError(seasonId: string, teamId: string): Promise<void> {
|
||||
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));
|
||||
const timers = await onDraftPaused(seasonId);
|
||||
try {
|
||||
getSocketIO().to(`draft-${seasonId}`).emit("draft-paused", { seasonId, paused: true });
|
||||
getSocketIO().to(`draft-${seasonId}`).emit("draft-paused", { seasonId, paused: true, timers });
|
||||
} catch (err) {
|
||||
logger.error("[Timer] Failed to emit draft-paused:", err);
|
||||
}
|
||||
|
|
@ -411,7 +412,7 @@ export async function rescheduleTimer(seasonId: string): Promise<void> {
|
|||
* 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.
|
||||
*/
|
||||
export async function onDraftPaused(seasonId: string): Promise<void> {
|
||||
export async function onDraftPaused(seasonId: string): Promise<Array<{ teamId: string; timeRemaining: number }>> {
|
||||
cancelPickTimeout(seasonId);
|
||||
cancelOvernightResumeTimeout(seasonId);
|
||||
|
||||
|
|
@ -422,14 +423,19 @@ export async function onDraftPaused(seasonId: string): Promise<void> {
|
|||
isNotNull(schema.draftTimers.picksExpiresAt),
|
||||
),
|
||||
});
|
||||
for (const timer of activeTimers) {
|
||||
if (!timer.picksExpiresAt) continue;
|
||||
const remaining = Math.max(0, Math.floor((timer.picksExpiresAt.getTime() - now) / 1000));
|
||||
await db
|
||||
.update(schema.draftTimers)
|
||||
.set({ timeRemaining: remaining, picksExpiresAt: null, picksStartedAt: null, updatedAt: new Date() })
|
||||
.where(eq(schema.draftTimers.id, timer.id));
|
||||
}
|
||||
const snapshots = await Promise.all(
|
||||
activeTimers
|
||||
.filter((t) => t.picksExpiresAt !== null)
|
||||
.map(async (timer) => {
|
||||
const remaining = Math.max(0, Math.floor(((timer.picksExpiresAt?.getTime() ?? now) - now) / 1000));
|
||||
await db
|
||||
.update(schema.draftTimers)
|
||||
.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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue