No overview comment at the top of the file.
- Imports
- no other modules
- Used by
- 1 route1 module1 component
Functions & values
functionsharedBracketIdxs
:18 Brackets that share their check-in with the given bracket: every bracket requiring check-in that starts at the same time. A team checked in to one of them is checked in to all, so a team moving between them keeps its check-in.
export function sharedBracketIdxs(
bracketIdx: number,
progression: ParsedBracket[],
): number[]
Unit tests (1)
functionisCheckedInToBrackets
:35 Is the team checked in to the brackets (from {@link sharedBracketIdxs})? The latest check-in or check-out among them decides.
export function isCheckedInToBrackets(
checkIns: CheckInRow[],
bracketIdxs: number[],
): boolean
Unit tests (1)
No overview comment at the top of the file.
- Imports
- no other modules
- Used by
- 1 loader1 action6 components4 other
Types
export type Type = (typeof types)[number];
interfacePickBanTeam
no JSDoc:27 export interface PickBanTeam {
id: number;
seed: number;
}interfaceTurnOfResult
no JSDoc:32 export interface TurnOfResult {
teamId: number;
action: ActionType;
stepCurrent?: number;
stepTotal?: number;
}A tournament team as far as map pool based pick/ban legality is concerned.
export type MapPoolTeam = {
id: number;
mapPool: Array<{ mode: ModeShort; stageId: StageId }> | null;
};typeCustomFlowValidationError
no JSDoc:779 export type CustomFlowValidationError =
(typeof CUSTOM_FLOW_VALIDATION_ERRORS)[keyof typeof CUSTOM_FLOW_VALIDATION_ERRORS];
Functions & values
export const types = [
"COUNTERPICK",
"COUNTERPICK_MODE_REPEAT_OK",
"BAN_2",
"CUSTOM",
] as const;
functionturnOf
no JSDoc:39 export function turnOf({
results,
maps,
teams,
mapList,
pickBanEventCount,
matchId,
}: {
results: Array<{ winnerTeamId: number }>;
maps: TournamentRoundMaps;
teams: [PickBanTeam, PickBanTeam];
mapList?: TournamentMapListMap[] | null;
pickBanEventCount?: number;
matchId: number;
}): TurnOfResult | nullfunctionresolveCurrentStep
no JSDoc:234 export function resolveCurrentStep({
eventCount,
preSet,
postGame,
resultsCount,
}: {
eventCount: number;
preSet: CustomPickBanStep[];
postGame: CustomPickBanStep[];
resultsCount: number;
}): CustomPickBanStep | nullUnit tests (6)
functionpostGameCycleIndex
:274 0-based post-game cycle of an event index (or, for a count of events done, how many cycles have started past the pre-set). Caller must ensure eventIndex >= preSetLength and postGameLength > 0.
export function postGameCycleIndex({
eventIndex,
preSetLength,
postGameLength,
}: {
eventIndex: number;
preSetLength: number;
postGameLength: number;
}): numberTeam responsible for the pick/ban event at the index, null if not pick/ban or undeterminable (CUSTOM ROLL step, insufficient results).
export function teamOfEvent({
eventIndex,
maps,
teams,
results,
matchId,
}: {
eventIndex: number;
maps: TournamentRoundMaps;
teams: [PickBanTeam, PickBanTeam];
results: Array<{ winnerTeamId: number }>;
matchId: number;
}): number | nullUnit tests (14)
functioncurrentTurnSessionStartedAt
:442 When the currently responsible team's pick/ban session started: on responsibility transitioning to it (consecutive actions by the same team continue it), or at the latest game result as a result always ends the prior session. matchStartedAt when no transitions exist yet, null when there is no current turn or matchStartedAt is unavailable.
export function currentTurnSessionStartedAt({
currentTurn,
events,
results,
matchStartedAt,
maps,
teams,
matchId,
}: {
currentTurn: TurnOfResult | null;
events: Array<{ createdAt: number }>;
results: Array<{ createdAt: number; winnerTeamId: number }>;
matchStartedAt: number | null;
maps: TournamentRoundMaps;
teams: [PickBanTeam, PickBanTeam];
matchId: number;
}): number | nullUnit tests (7)
functionisLegal
no JSDoc:488 export function isLegal({
map,
...rest
}: MapListWithStatusesArgs & { map: ModeWithStage })functionisModeLegal
no JSDoc:499 export function isModeLegal({
mode,
...rest
}: MapListWithStatusesArgs & { mode: ModeShort })Unit tests (3)
functionisStageLegal
no JSDoc:508 export function isStageLegal({
stageId,
...rest
}: MapListWithStatusesArgs & { stageId: StageId })functionmapsListWithLegality
no JSDoc:539 export function mapsListWithLegality(args: MapListWithStatusesArgs)
functionvalidateCustomFlowSection
no JSDoc:787 export function validateCustomFlowSection(
steps: ValidatableStep[],
section: "preSet" | "postGame",
): CustomFlowValidationError[]
Unit tests (27)
Unit tests not about one export (33)
Not shown, only used by tests: resolveTeamFromSide, randomWhoTeamIndex, PickBanEvent, CUSTOM_FLOW_VALIDATION_ERRORS
No overview comment at the top of the file.
- Imports
- ProgressionSkippedRounds
- Used by
- 1 action1 route1 component1 other
Types
typeEliminationBracketType
no JSDoc:58 export type EliminationBracketType =
| "single_elimination"
| "double_elimination";
Functions & values
functionresolvePreparedForTheBracket
:16 Prepared maps of the bracket, or of a "sibling" bracket at the same progression depth.
export function resolvePreparedForTheBracket({
preparedByBracket,
bracketIdx,
tournament,
}: {
preparedByBracket?: (PreparedMaps | null)[];
bracketIdx: number;
tournament: Tournament;
})functioneliminationTeamCountOptions
:99 Elimination team counts that are the "steps" affecting the round count, from currentCount up.
export function eliminationTeamCountOptions({
type,
currentCount,
}: {
type: EliminationBracketType;
currentCount: number;
})functionisValidMaxEliminationTeamCount
:115 Whether the count is a known "max" elimination team count.
export function isValidMaxEliminationTeamCount(count: number)
functioneliminationTeamCountPrefill
:129 Prefill for the "expected teams" selection, null when it can't be told yet e.g. registration is still open.
export function eliminationTeamCountPrefill({
tournament,
bracketIdx,
}: {
tournament: Tournament;
bracketIdx: number;
}): number | nullfunctionmapListRounds
:168 Rounds that each get a map list. Groups of an elimination bracket share a map list per section and position from its end, so the rounds of the group with the most of them.
export function mapListRounds(data: BracketData)
Unit tests (4)
{@link mapListRounds} with only their group and matches, the shape a map list is picked against.
export function mapListData(data: BracketData): BracketData
Unit tests (2)
functiontrimPreparedEliminationMaps
:206 Trims prepared elimination maps to the actual team count, null if not prepared or prepared for too few.
export function trimPreparedEliminationMaps({
preparedMaps,
teamCount,
...rest
}: TrimPreparedEliminationMapsAgs)Unit tests not about one export (44)
No overview comment at the top of the file.
- Imports
- SkippedRounds
- Used by
- 3 actions3 routes2 repositories8 modules2 components5 other
Types
interfaceDBSource
no JSDoc:12 export interface DBSource {
bracketIdx: number;
/** E.g. [1, 2] = top 2 teams, [-1] = last placing teams, [] = Swiss early advancers. */
placements: number[];
/** Highest value in `placements` means "and every placement after that" ("N+" syntax). Only valid with positive placements. */
rest?: boolean;
}interfaceEditableSource
no JSDoc:20 export interface EditableSource {
/** Frontend-only id while editing, replaced by an index once set. See DBSource.bracketIdx. */
bracketId: string;
/** User editable, "1-3" and "1,2,3" mean the same. See DBSource.placements for the validated version. */
placements: string;
}Only used inside the module
export interface InputBracket extends BracketBase {
id: string;
sources?: EditableSource[];
startTime?: Date;
/** Already underway */
disabled?: boolean;
}interfaceParsedBracket
no JSDoc:42 export interface ParsedBracket extends BracketBase {
sources?: DBSource[];
startTime?: number;
}typeValidationError
no JSDoc:47 export type ValidationError =
// user written placements can not be parsed
| {
type: "PLACEMENTS_PARSE_ERROR";
bracketIdx: number;
}
// from each bracket one placement can lead to only one bracket
| {
type: "SAME_PLACEMENT_TO_MULTIPLE_BRACKETS";
bracketIdxs: number[];
}
// e.g. if 1st and 3rd go somewhere then 2nd must also go somewhere
| {
type: "GAP_IN_PLACEMENTS";
bracketIdxs: number[];
}
// e.g. round robin group size 4 can't have a destination for 5th
| {
type: "TOO_MANY_PLACEMENTS";
bracketIdx: number;
}
// placements above the hard cap are nonsensical and bloat the settings JSON
| {
type: "PLACEMENT_TOO_HIGH";
bracketIdx: number;
}
// two brackets can not have the same name
| {
type: "DUPLICATE_BRACKET_NAME";
bracketIdxs: number[];
}
// bracket name can not be empty
| {
type: "NAME_MISSING";
bracketIdx: number;
}
// negative progression (e.g. losers of first round go somewhere) is only for elimination bracket
| {
type: "NEGATIVE_PROGRESSION";
bracketIdx: number;
}
// a single source can not take both top finishers and eliminated teams
| {
type: "MIXED_POSITIVE_NEGATIVE_PLACEMENTS";
bracketIdx: number;
}
// Swiss bracket with early advance/elimination must have a destination bracket
| {
type: "SWISS_EARLY_ADVANCE_NO_DESTINATION";
bracketIdx: number;
}
// A/B divisions setting is only valid on round robin brackets
| {
type: "AB_DIVISIONS_NOT_ROUND_ROBIN";
bracketIdx: number;
}
// A/B divisions setting is only valid on starting brackets (no sources)
| {
type: "AB_DIVISIONS_NOT_STARTING";
bracketIdx: number;
}
// A/B divisions need an even teamsPerGroup to split each group equally
| {
type: "AB_DIVISIONS_ODD_TEAMS_PER_GROUP";
bracketIdx: number;
}
// empty placements is only valid when sourcing from a Swiss bracket with early advance
| {
type: "EMPTY_PLACEMENTS_ON_NON_SWISS";
bracketIdx: number;
}
// one destination bracket can source each bracket only once
| {
type: "DUPLICATE_SOURCE_BRACKET";
bracketIdx: number;
}
// brackets can not source each other in a loop e.g. A sources B and B sources A
| {
type: "CYCLIC_PROGRESSION";
bracketIdxs: number[];
}
// teams that started in different brackets can never meet
| {
type: "MERGED_STARTING_BRACKETS";
bracketIdx: number;
}
// skipped rounds must belong to the bracket's format and every round depending on a skipped one must be skipped too
| {
type: "INVALID_SKIPPED_ROUNDS";
bracketIdx: number;
}
// elimination brackets can be split into a limited number of groups
| {
type: "INVALID_GROUP_COUNT";
bracketIdx: number;
};Functions & values
Validated brackets in the format ready for user input.
export function validatedBracketsToInputFormat(
brackets: ParsedBracket[],
): InputBracket[]
Unit tests (1)
functionplacementsToString
:170 [1, 2, 3] -> "1-3", [5, 6] with rest -> "5,6+"
export function placementsToString(placements: number[], rest = false): string
functionvalidatedBrackets
:210 User-entered bracket progression to validated brackets ready for the database, or errors.
export function validatedBrackets(
brackets: InputBracket[],
): ParsedBracket[] | ValidationError
functionbracketsToValidationError
:237 Errors in how the progression is laid out.
export function bracketsToValidationError(
brackets: ParsedBracket[],
): ValidationError | null
functionparsePlacements
:452 "1-3,5+" -> { placements: [1, 2, 3, 5], rest: true }, null if not valid placements syntax. Empty input is the Swiss early advance case.
export function parsePlacements(
placements: string,
): { placements: number[]; rest: boolean } | nullNarrows the return type of Progression.validatedBrackets to a successful validation.
export function isBrackets(
input: ParsedBracket[] | ValidationError,
): input is ParsedBracket[]
Narrows the return type of Progression.validatedBrackets to a failed validation.
export function isError(
input: ParsedBracket[] | ValidationError,
): input is ValidationError
Whether the bracket is the final stage that decides the final standings.
export function isFinals(idx: number, brackets: ParsedBracket[])
Unit tests (6)
functionfinalsBracketIdx
:957 Index of the last bracket of the main progression starting from startBracketIdx, e.g. a division's top cut.
export function finalsBracketIdx(
brackets: ParsedBracket[],
startBracketIdx = 0,
): number
functionhasAbDivisionsFinals
:965 Whether the finals bracket is an A/B divisions round robin.
export function hasAbDivisionsFinals(brackets: ParsedBracket[]): boolean
functionisUnderground
:975 Underground bracket = not part of the main progression, e.g. an optional bracket for early losers.
export function isUnderground(idx: number, brackets: ParsedBracket[])
Unit tests (7)
functionbracketDepth
:1012 Distance from a starting bracket (no sources): starting brackets 0, brackets sourced from them 1, etc.
export function bracketDepth(idx: number, brackets: ParsedBracket[]): number
Unit tests (6)
functionchangedBracketProgression
:1074 Indexes of brackets changed in any field.
export function changedBracketProgression(
oldProgression: ParsedBracket[],
newProgression: ParsedBracket[],
)
Unit tests (2)
Whether any field affecting the format changed.
export function changedBracketProgressionFormat(
oldProgression: ParsedBracket[],
newProgression: ParsedBracket[],
): boolean
functionchangedStartingBrackets
:1116 Returns true if the set of brackets that teams can start in changed
export function changedStartingBrackets(
oldProgression: ParsedBracket[],
newProgression: ParsedBracket[],
): boolean
functionbracketIdxsForStandings
:1133 Bracket order for standings: a participant's standing comes from the first bracket in this order they are in. Finals first; a bracket always comes after every bracket it advances teams to, so teams it eliminated end up below those that advanced. Underground brackets are omitted as they only break ties within their source bracket, see tiebrokenByUndergroundBrackets. Considers the brackets reachable from startingBracketIdx, for tournaments with many starting brackets (divisions).
export function bracketIdxsForStandings(
progression: ParsedBracket[],
startingBracketIdx = 0,
)
Unit tests (12)
functionbracketsReachableFrom
no JSDoc:1237 export function bracketsReachableFrom(
bracketIdx: number,
progression: ParsedBracket[],
visited: Set<number> = new Set(),
): number[]
functiondestinationsFromBracketIdx
no JSDoc:1262 export function destinationsFromBracketIdx(
sourceBracketIdx: number,
progression: ParsedBracket[],
): number[]
Unit tests (3)
functionundergroundBracketIdxs
:1282 Underground brackets (taking eliminated teams, negative placements) sourced from the given bracket.
export function undergroundBracketIdxs(
bracketIdx: number,
progression: ParsedBracket[],
): number[]
functiondestinationByPlacement
no JSDoc:1295 export function destinationByPlacement({
sourceBracketIdx,
placement,
progression,
}: {
sourceBracketIdx: number;
placement: number;
progression: ParsedBracket[];
}): number | nullUnit tests (4)
functionstartingBrackets
no JSDoc:1328 export function startingBrackets(progression: ParsedBracket[]): number[]
Unit tests (3)
functionsortedSourcesForSeeding
:1340 Orders sources for seeding: a better placement in a shared ancestor bracket seeds above a longer route there, e.g. the top 2 of pools directly over the winners of a redemption bracket sourcing pools placements 3-4. Sources sharing no ancestor keep their relative order.
export function sortedSourcesForSeeding(
sources: DBSource[],
progression: ParsedBracket[],
): DBSource[]
Unit tests (4)
Unit tests not about one export (56)
No overview comment at the top of the file.
- Imports
- no other modules
- Used by
- 3 modules1 component4 other
Functions & values
Named rounds that can be left unplayed, in the order they are played.
export const SKIPPABLE_ROUNDS = {
single_elimination: ["SEMIS", "FINALS", "THIRD_PLACE_MATCH"],
double_elimination: [
"WB_FINALS",
"LB_SEMIS",
"LB_FINALS",
"GRAND_FINALS",
"BRACKET_RESET",
],
} as const satisfies Record<EliminationType, SkippableRound[]>;constALL_SKIPPABLE_ROUNDS
:19 Every round that can be skipped in some bracket type.
export const ALL_SKIPPABLE_ROUNDS = [
...SKIPPABLE_ROUNDS.single_elimination,
...SKIPPABLE_ROUNDS.double_elimination,
] as const;
functionprerequisitesOf
:37 Rounds that must be played for the given round to be played.
export function prerequisitesOf(round: SkippableRound): SkippableRound[]
Skipped rounds after skipping round, which also skips every round depending on it.
export function withSkipped(
type: EliminationType,
skipped: SkippableRound[],
round: SkippableRound,
): SkippableRound[]
Skipped rounds after playing round, which also plays every round it depends on.
export function withPlayed(
type: EliminationType,
skipped: SkippableRound[],
round: SkippableRound,
): SkippableRound[]
Whether every round of the bracket type can be skipped and every round depending on a skipped one is skipped too.
export function isValid(
type: EliminationType,
skipped: SkippableRound[],
): boolean
Skipped rounds of the bracket type with their dependents included, in play order. Rounds of another bracket type are left out.
export function normalized(
type: EliminationType,
skipped: SkippableRound[] | undefined,
): SkippableRound[]
functionskippedRoundNumbers
:98 Round numbers of a section of one elimination group that are skipped. Rounds are identified from the end of the section, so e.g. "FINALS" is always the last round of the bracket, whatever its size.
export function skippedRoundNumbers({
type,
section,
roundCount,
skipped,
}: {
type: EliminationType;
section: RoundSection;
roundCount: number;
skipped: SkippableRound[];
}): Set<number>No overview comment at the top of the file.
- Imports
- RunningTournaments.serverTournament
- Used by
- 19 loaders17 actions14 routes2 modules4 components2 routines2 scripts8 other
Types
typeTournamentData
no JSDoc:119 export type TournamentData = NonNullable<Unwrapped<typeof tournamentData>>;
typeTournamentLayoutData
:122 What the layout ships: everything every view needs. Match data is loaded per bracket by the bracket views.
export type TournamentLayoutData = {
ctx: TournamentData["ctx"];
bracketsMeta: BracketDerivedMeta[];
};typeTournamentDataTeam
:128 No per member profile data, map pool or invite code, see {@link tournamentTeamsFullCached} for those.
export type TournamentDataTeam = Omit<
TournamentRepository.FindById["teams"][number],
"teamLogoUrl" | "pickupAvatarUrl" | "inviteCode"
> & {
/**
* Linked team logo, falling back to the pickup avatar once started. Views showing pickup avatars
* before that (own team, organizer) read them off {@link tournamentTeamsFullCached}, censored per viewer.
*/
logoUrl: string | null;
};typeTournamentTeamFull
:413 Team with its full roster.
export type TournamentTeamFull = Unwrapped<typeof tournamentTeamsFullCached>;
Functions & values
functiontournamentData
:42 Everything a tournament is made of including brackets and streams.
export async function tournamentData(tournamentId: number)
functionfetchTournamentStreams
:81 Read fresh, bypassing the cache whose copy (read once per fill) only serves server-side consumers.
export async function fetchTournamentStreams(
tournamentId: number,
): Promise<TournamentStream[]>
functionrequireTournamentVisible
:151 Every loader under the tournament layout must run this (normally via {@link tournamentFromParams}): single fetch reaches each without the layout loader's check.
export function requireTournamentVisible({
ctx,
user,
}: {
ctx: TournamentVisibilityCtx;
user: OptionalIdObject;
})functioncanSeeTournamentFriendCodes
:170 Organizers see the participants' friend codes only for a while after the start. Leagues run for many weeks, so theirs stay visible for longer.
export function canSeeTournamentFriendCodes({
ctx,
user,
}: {
ctx: TournamentFriendCodeCtx;
user: OptionalIdObject;
})functionisTournamentTeamInfoRevealed
:187 Pickup avatars and map pools of teams are only revealed to organizers (and the team itself) before the start.
export function isTournamentTeamInfoRevealed({
tournament,
user,
}: {
tournament: Pick<TournamentData, "ctx" | "data">;
user: OptionalIdObject;
})functionrequireTournamentOrganizer
:201 Guards a single _action branch; whole-route guards use {@link tournamentFromParams} with for: "organizer".
export function requireTournamentOrganizer(
tournament: Tournament,
user: AuthenticatedUser,
message = "Not an organizer",
)
functionrequireTournamentAdmin
:210 Throws an error toast unless the user is an admin.
export function requireTournamentAdmin(
tournament: Tournament,
user: AuthenticatedUser,
)
functiontournamentFromParams
:228 Preamble of to.$id.* loaders and actions: parses the id (404 on invalid), loads and guards.
- view: anyone the tournament is visible to; shared cached instance, amortizing bracket building. - action: any logged-in user; fresh read for actions doing their own per _action authorization. - organizer / admin: like action but others are redirected to the tournament front page.
export async function tournamentFromParams(
params: Params<string>,
opts: { for: "view" },
): Promise<{
tournament: Tournament;
tournamentId: number;
user: AuthenticatedUser | undefined;
}>;functiontournamentFromDB
no JSDoc:274 export async function tournamentFromDB(tournamentId: number)
functionnotifyTournamentStatusChanged
:288 Prompts the users' clients to refetch their header status after a change to the tournament. Fills the (just cleared) cache and syncs the registry from that one rebuild so the refetch reads post-change state and the revalidation that follows the action finds a warm cache.
export async function notifyTournamentStatusChanged(
tournamentId: number,
userIds: number[],
)
functiontournamentDataCached
no JSDoc:313 export async function tournamentDataCached(tournamentId: number)
functiontournamentSharedCached
:322 Shared by every request for the lifetime of the cache entry, so brackets are built once per fill.
export async function tournamentSharedCached(tournamentId: number)
State of every bracket without the match data it derives from.
export async function bracketsMetaCached(
tournamentId: number,
): Promise<BracketDerivedMeta[]>
functionserializeBracket
:356 In the shape {@link Tournament.withBrackets} revives. With a groupId only that group's rounds and matches are included, every group still listed so the view can offer switching.
export function serializeBracket(
bracket: Bracket,
args?: { groupId: number | null },
): SerializedBracketUnit tests (3)
functiontournamentTeamsFullCached
:426 Full rosters censored for the viewer. Own cache slice so the much smaller layout data needn't carry them.
export async function tournamentTeamsFullCached({
user,
tournamentId,
}: {
user?: { id: number };
tournamentId: number;
})functiontournamentTeamsFullInSeedOrder
:459 {@link tournamentTeamsFullCached} in seed order, which is not the order the rows come back in.
export async function tournamentTeamsFullInSeedOrder({
tournament,
user,
}: {
tournament: Tournament;
user?: { id: number };
})functionclearTournamentDataCache
no JSDoc:523 export function clearTournamentDataCache(tournamentId: number)
functionclearAllTournamentDataCache
no JSDoc:528 export function clearAllTournamentDataCache()
functionevictStaleRunningTournaments
:582 Evicts no longer live tournaments (e.g. abandoned ones whose latest day started over 6 hours ago).
export function evictStaleRunningTournaments()
Unit tests (5)
functionrefreshRunningTournaments
:606 E2E workers call this (via /refresh-caches) after writing tournaments straight into the database file.
export async function refreshRunningTournaments()