brackt/app/services/draft-autostart.ts

100 lines
2.8 KiB
TypeScript
Raw Permalink Normal View History

import * as schema from "~/database/schema";
import { eq } from "drizzle-orm";
import { logger } from "~/lib/logger";
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
import type { Server as SocketIOServer } from "socket.io";
type Db = PostgresJsDatabase<typeof schema>;
export interface StartDraftOptions {
seasonId: string;
actorUserId: string;
actorDisplayName: string;
db: Db;
io: SocketIOServer;
}
export interface StartDraftResult {
success: boolean;
error?: string;
}
/**
* Core draft-start logic shared by the commissioner HTTP route and the
* auto-start timer. Accepts `db` directly (not via AsyncLocalStorage) so it
* works both inside React Router request context and in the server timer loop.
*/
export async function startDraft(options: StartDraftOptions): Promise<StartDraftResult> {
const { seasonId, actorUserId, actorDisplayName, db, io } = options;
const season = await db.query.seasons.findFirst({
where: eq(schema.seasons.id, seasonId),
});
if (!season) {
return { success: false, error: "Season not found" };
}
// Idempotency guard — status may have changed between the outer check and here
if (season.status !== "pre_draft") {
return { success: false, error: "Draft already started or completed" };
}
const draftSlots = await db.query.draftSlots.findMany({
where: eq(schema.draftSlots.seasonId, seasonId),
});
if (draftSlots.length === 0) {
return { success: false, error: "No draft slots found for this season" };
}
await db
.update(schema.seasons)
.set({
status: "draft",
currentPickNumber: 1,
draftStartedAt: new Date(),
})
.where(eq(schema.seasons.id, seasonId));
// Standard mode: each pick gets exactly the increment time.
// Chess clock mode: each team starts with the full initial bank.
const initialTime =
season.draftTimerMode === "standard"
? season.draftIncrementTime || 30
: season.draftInitialTime || 120;
await db.delete(schema.draftTimers).where(eq(schema.draftTimers.seasonId, seasonId));
await db.insert(schema.draftTimers).values(
draftSlots.map((slot) => ({
seasonId,
teamId: slot.teamId,
timeRemaining: initialTime,
}))
);
await db.insert(schema.commissionerAuditLog).values({
seasonId,
leagueId: season.leagueId,
actorUserId,
actorDisplayName,
action: "draft_started",
affectedTeamIds: [],
details: { pickNumber: 1, autoStarted: actorUserId === "system" },
});
try {
io.to(`draft-${seasonId}`).emit("draft-started", {
seasonId,
currentPickNumber: 1,
});
} catch (err) {
// Socket emit failure is non-fatal; DB state is already committed
logger.error("[startDraft] Socket emit failed:", err);
}
return { success: true };
}