Fix circular dependency and client-bundle server-only code in simulator routes

NCAAM and NCAAW simulators imported getParticipantSimulatorInputs from
models/simulator, which imports manifest.ts, which imports registry.ts —
creating a cycle back through registry.ts → simulator files. This caused
a TDZ ReferenceError in the client bundle.

Fix: inline a direct seasonParticipantSimulatorInputs query in both
simulators (they already run direct DB queries) instead of going through
the model layer, breaking the cycle.

Also moves getSimulatorInputPolicy from the component body to the loader
on the simulator setup page. The component calling it pulled input-policy.ts
→ manifest.ts → registry.ts → all simulator implementations into the client
bundle, which contain Node.js-only code and caused "Error loading route
module" in production.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Chris Parsons 2026-05-11 22:19:31 -07:00
parent e5295812f6
commit 7c94657417
3 changed files with 18 additions and 9 deletions

View file

@ -55,7 +55,9 @@ export async function loader({ params }: Route.LoaderArgs) {
input: inputMap.get(participant.id) ?? null,
}));
return { sportsSeason, participants, config, inputRows, readiness };
const inputPolicy = getSimulatorInputPolicy(config.config);
return { sportsSeason, participants, config, inputRows, readiness, inputPolicy };
}
interface ActionData {
@ -234,12 +236,11 @@ export async function action({ request, params }: Route.ActionArgs): Promise<Act
}
export default function AdminSportsSeasonSimulator({ loaderData }: Route.ComponentProps) {
const { sportsSeason, config, inputRows, readiness } = loaderData;
const { sportsSeason, config, inputRows, readiness, inputPolicy } = loaderData;
const actionData = useActionData<ActionData>();
const navigation = useNavigation();
const isSubmitting = navigation.state === "submitting";
const setupSections = config.profile.setupSections;
const inputPolicy = getSimulatorInputPolicy(config.config);
const sourceEloAlternatives = config.profile.derivableInputs?.sourceElo ?? [];
return (

View file

@ -48,7 +48,6 @@ import type { BracketRegion } from "~/lib/bracket-templates";
import { buildNCAA68SlotMap, matchIndexForSeedSlot, NCAA_68 } from "~/lib/bracket-templates";
import type { Simulator, SimulationResult } from "./types";
import { logger } from "~/lib/logger";
import { getParticipantSimulatorInputs } from "~/models/simulator";
// ─── Simulation parameters ────────────────────────────────────────────────────
@ -499,7 +498,10 @@ export class NCAAMSimulator implements Simulator {
.select({ id: schema.seasonParticipants.id, name: schema.seasonParticipants.name })
.from(schema.seasonParticipants)
.where(inArray(schema.seasonParticipants.id, participantIds)),
getParticipantSimulatorInputs(sportsSeasonId),
db
.select({ participantId: schema.seasonParticipantSimulatorInputs.participantId, rating: schema.seasonParticipantSimulatorInputs.rating })
.from(schema.seasonParticipantSimulatorInputs)
.where(eq(schema.seasonParticipantSimulatorInputs.sportsSeasonId, sportsSeasonId)),
]);
if (participantRows.length < participantIds.length) {
@ -514,7 +516,9 @@ export class NCAAMSimulator implements Simulator {
const inputById = new Map(simulatorInputs.map((input) => [input.participantId, input]));
const netRatingById = new Map<string, number>();
for (const { id, name } of participantRows) {
netRatingById.set(id, inputById.get(id)?.rating ?? getNetRating(name));
const rawRating = inputById.get(id)?.rating;
const rating = rawRating !== null && rawRating !== undefined ? Number(rawRating) : null;
netRatingById.set(id, (rating !== null && Number.isFinite(rating) ? rating : null) ?? getNetRating(name));
}
// 7. Build per-round O(1) lookup maps keyed by matchNumber.

View file

@ -46,7 +46,6 @@ import type { BracketRegion } from "~/lib/bracket-templates";
import { buildNCAA68SlotMap, matchIndexForSeedSlot, NCAA_68 } from "~/lib/bracket-templates";
import type { Simulator, SimulationResult } from "./types";
import { logger } from "~/lib/logger";
import { getParticipantSimulatorInputs } from "~/models/simulator";
// ─── Simulation parameters ────────────────────────────────────────────────────
@ -462,7 +461,10 @@ export class NCAAWSimulator implements Simulator {
.select({ id: schema.seasonParticipants.id, name: schema.seasonParticipants.name })
.from(schema.seasonParticipants)
.where(inArray(schema.seasonParticipants.id, participantIds)),
getParticipantSimulatorInputs(sportsSeasonId),
db
.select({ participantId: schema.seasonParticipantSimulatorInputs.participantId, rating: schema.seasonParticipantSimulatorInputs.rating })
.from(schema.seasonParticipantSimulatorInputs)
.where(eq(schema.seasonParticipantSimulatorInputs.sportsSeasonId, sportsSeasonId)),
]);
if (participantRows.length < participantIds.length) {
@ -477,7 +479,9 @@ export class NCAAWSimulator implements Simulator {
const inputById = new Map(simulatorInputs.map((input) => [input.participantId, input]));
const barthagById = new Map<string, number>();
for (const { id, name } of participantRows) {
barthagById.set(id, inputById.get(id)?.rating ?? getBarthagRating(name));
const rawRating = inputById.get(id)?.rating;
const rating = rawRating !== null && rawRating !== undefined ? Number(rawRating) : null;
barthagById.set(id, (rating !== null && Number.isFinite(rating) ? rating : null) ?? getBarthagRating(name));
}
// 7. Build per-round O(1) lookup maps keyed by matchNumber.