mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-11 05:36:10 -05:00
Refactor
This commit is contained in:
@@ -36,7 +36,7 @@ export function endDroppedTeamMatches(
|
||||
return Math.random() < 0.5 ? match.opponent1.id : match.opponent2.id;
|
||||
})();
|
||||
|
||||
const stored = store.select("match", match.id);
|
||||
const stored = store.matchById(match.id);
|
||||
if (!stored) throw Error("Match not found.");
|
||||
|
||||
propagator.updateMatch(
|
||||
|
||||
@@ -16,7 +16,7 @@ export function reportResult(
|
||||
const store = new Store(data);
|
||||
const propagator = new Propagator(store);
|
||||
|
||||
const stored = store.select("match", input.matchId);
|
||||
const stored = store.matchById(input.matchId);
|
||||
invariant(stored, "Match not found");
|
||||
|
||||
propagator.updateMatch(
|
||||
|
||||
@@ -20,13 +20,13 @@ export function resetMatchResults(
|
||||
const store = new Store(data);
|
||||
const propagator = new Propagator(store);
|
||||
|
||||
const stored = store.select("match", matchId);
|
||||
const stored = store.matchById(matchId);
|
||||
if (!stored) throw Error("Match not found.");
|
||||
|
||||
const stage = store.select("stage", stored.stageId);
|
||||
const stage = store.stageById(stored.stageId);
|
||||
if (!stage) throw Error("Stage not found.");
|
||||
|
||||
const group = store.select("group", stored.groupId);
|
||||
const group = store.groupById(stored.groupId);
|
||||
if (!group) throw Error("Group not found.");
|
||||
|
||||
const { roundNumber, roundCount } = propagator.getRoundPositionalInfo(
|
||||
@@ -55,7 +55,7 @@ export function resetMatchResults(
|
||||
throw Error("The match is locked.");
|
||||
|
||||
helpers.clearWinner(stored);
|
||||
propagator.applyMatchUpdate(stored);
|
||||
store.markMatchChanged(stored);
|
||||
|
||||
if (!helpers.isRoundRobin(stage) && !helpers.isSwiss(stage))
|
||||
propagator.updateRelatedMatches(stored);
|
||||
|
||||
@@ -6,78 +6,115 @@ import type {
|
||||
StageData,
|
||||
} from "../types";
|
||||
|
||||
interface TableTypes {
|
||||
stage: StageData;
|
||||
group: GroupData;
|
||||
round: RoundData;
|
||||
match: MatchData;
|
||||
}
|
||||
|
||||
type Table = keyof TableTypes;
|
||||
|
||||
/**
|
||||
* A working copy of BracketData with the same select/update semantics the old
|
||||
* storage had: selects return clones (in table order), updates replace the row
|
||||
* by id. Tracks which match rows were written so callers can emit a delta.
|
||||
* A working copy of BracketData. The input is cloned once on construction and
|
||||
* every row handed out afterwards is a live reference into that copy, so
|
||||
* mutating a row is the write. Callers report the match rows they mutated so
|
||||
* that a delta can be emitted.
|
||||
*/
|
||||
export class Store {
|
||||
readonly data: BracketData;
|
||||
private readonly touchedMatchIds = new Set<number>();
|
||||
private readonly stagesById: Map<number, StageData>;
|
||||
private readonly groupsById: Map<number, GroupData>;
|
||||
private readonly roundsById: Map<number, RoundData>;
|
||||
private readonly matchesById: Map<number, MatchData>;
|
||||
private readonly groupsByStageId: Map<number, GroupData[]>;
|
||||
private readonly roundsByGroupId: Map<number, RoundData[]>;
|
||||
private readonly matchesByRoundId: Map<number, MatchData[]>;
|
||||
private readonly changedMatchIds = new Set<number>();
|
||||
|
||||
constructor(data: BracketData) {
|
||||
this.data = structuredClone(data);
|
||||
|
||||
this.stagesById = indexById(this.data.stage);
|
||||
this.groupsById = indexById(this.data.group);
|
||||
this.roundsById = indexById(this.data.round);
|
||||
this.matchesById = indexById(this.data.match);
|
||||
|
||||
this.groupsByStageId = groupByKey(
|
||||
this.data.group,
|
||||
(group) => group.stageId,
|
||||
);
|
||||
this.roundsByGroupId = groupByKey(
|
||||
this.data.round,
|
||||
(round) => round.groupId,
|
||||
);
|
||||
this.matchesByRoundId = groupByKey(
|
||||
this.data.match,
|
||||
(match) => match.roundId,
|
||||
);
|
||||
}
|
||||
|
||||
select<T extends Table>(table: T, id: number): TableTypes[T] | null {
|
||||
const row = (this.data[table] as TableTypes[T][]).find((r) => r.id === id);
|
||||
return row ? structuredClone(row) : null;
|
||||
stageById(id: number): StageData | null {
|
||||
return this.stagesById.get(id) ?? null;
|
||||
}
|
||||
|
||||
selectAll<T extends Table>(
|
||||
table: T,
|
||||
filter: Partial<TableTypes[T]>,
|
||||
): TableTypes[T][] {
|
||||
return (this.data[table] as TableTypes[T][])
|
||||
.filter(makeFilter(filter))
|
||||
.map((row) => structuredClone(row));
|
||||
groupById(id: number): GroupData | null {
|
||||
return this.groupsById.get(id) ?? null;
|
||||
}
|
||||
|
||||
selectFirst<T extends Table>(
|
||||
table: T,
|
||||
filter: Partial<TableTypes[T]>,
|
||||
): TableTypes[T] | null {
|
||||
const results = this.selectAll(table, filter);
|
||||
return results.length > 0 ? results[0] : null;
|
||||
roundById(id: number): RoundData | null {
|
||||
return this.roundsById.get(id) ?? null;
|
||||
}
|
||||
|
||||
selectLast<T extends Table>(
|
||||
table: T,
|
||||
filter: Partial<TableTypes[T]>,
|
||||
): TableTypes[T] | null {
|
||||
const results = this.selectAll(table, filter);
|
||||
return results.length > 0 ? results[results.length - 1] : null;
|
||||
matchById(id: number): MatchData | null {
|
||||
return this.matchesById.get(id) ?? null;
|
||||
}
|
||||
|
||||
updateMatch(match: MatchData): void {
|
||||
const index = this.data.match.findIndex((m) => m.id === match.id);
|
||||
if (index === -1) throw Error("Could not update the match.");
|
||||
groupByNumber(stageId: number, groupNumber: number): GroupData | null {
|
||||
const groups = this.groupsByStageId.get(stageId);
|
||||
return groups?.find((group) => group.number === groupNumber) ?? null;
|
||||
}
|
||||
|
||||
this.data.match[index] = structuredClone(match);
|
||||
this.touchedMatchIds.add(match.id);
|
||||
roundByNumber(groupId: number, roundNumber: number): RoundData | null {
|
||||
const rounds = this.roundsByGroupId.get(groupId);
|
||||
return rounds?.find((round) => round.number === roundNumber) ?? null;
|
||||
}
|
||||
|
||||
matchByNumber(roundId: number, matchNumber: number): MatchData | null {
|
||||
const matches = this.matchesByRoundId.get(roundId);
|
||||
return matches?.find((match) => match.number === matchNumber) ?? null;
|
||||
}
|
||||
|
||||
roundCountInGroup(groupId: number): number {
|
||||
return this.roundsByGroupId.get(groupId)?.length ?? 0;
|
||||
}
|
||||
|
||||
matchCountInRound(roundId: number): number {
|
||||
return this.matchesByRoundId.get(roundId)?.length ?? 0;
|
||||
}
|
||||
|
||||
/** Records that a match row of this store was mutated. */
|
||||
markMatchChanged(match: MatchData): void {
|
||||
if (this.matchesById.get(match.id) !== match)
|
||||
throw Error("Match is not a row of this store.");
|
||||
|
||||
this.changedMatchIds.add(match.id);
|
||||
}
|
||||
|
||||
/** Returns the final version of every match row that was written during this operation. */
|
||||
changedMatches(): MatchData[] {
|
||||
return this.data.match.filter((match) =>
|
||||
this.touchedMatchIds.has(match.id),
|
||||
this.changedMatchIds.has(match.id),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function makeFilter<T extends object>(
|
||||
partial: Partial<T>,
|
||||
): (row: T) => boolean {
|
||||
const entries = Object.entries(partial);
|
||||
return (row) =>
|
||||
entries.every(([key, value]) => row[key as keyof T] === value);
|
||||
function indexById<T extends { id: number }>(rows: T[]): Map<number, T> {
|
||||
return new Map(rows.map((row) => [row.id, row]));
|
||||
}
|
||||
|
||||
function groupByKey<T>(rows: T[], key: (row: T) => number): Map<number, T[]> {
|
||||
const result = new Map<number, T[]>();
|
||||
|
||||
for (const row of rows) {
|
||||
const existing = result.get(key(row));
|
||||
if (existing) {
|
||||
existing.push(row);
|
||||
} else {
|
||||
result.set(key(row), [row]);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@ interface RoundPositionalInfo {
|
||||
|
||||
/**
|
||||
* Resolves the matches following another match and applies result propagation
|
||||
* to them. Port of the old base/getter.ts + base/updater.ts, reading and
|
||||
* writing a Store instead of storage.
|
||||
* to them. Port of the old base/getter.ts + base/updater.ts, mutating the rows
|
||||
* of a Store instead of writing to storage.
|
||||
*/
|
||||
export class Propagator {
|
||||
readonly store: Store;
|
||||
@@ -43,10 +43,10 @@ export class Propagator {
|
||||
match.roundId,
|
||||
);
|
||||
|
||||
const stage = this.store.select("stage", match.stageId);
|
||||
const stage = this.store.stageById(match.stageId);
|
||||
if (!stage) throw Error("Stage not found.");
|
||||
|
||||
const group = this.store.select("group", match.groupId);
|
||||
const group = this.store.groupById(match.groupId);
|
||||
if (!group) throw Error("Group not found.");
|
||||
|
||||
const matchLocation = helpers.getMatchLocation(stage.type, group.number);
|
||||
@@ -57,7 +57,7 @@ export class Propagator {
|
||||
/**
|
||||
* Updates a match based on a reported result.
|
||||
*
|
||||
* @param stored A reference to what will be updated in the storage.
|
||||
* @param stored The match row of the store that is mutated.
|
||||
* @param input Input of the update.
|
||||
* @param force Whether to force update matches that can't be played yet.
|
||||
*/
|
||||
@@ -69,11 +69,11 @@ export class Propagator {
|
||||
if (!force && matchStatus(this.store.data, stored.id) === "PENDING")
|
||||
throw Error("The match is locked.");
|
||||
|
||||
const stage = this.store.select("stage", stored.stageId);
|
||||
const stage = this.store.stageById(stored.stageId);
|
||||
if (!stage) throw Error("Stage not found.");
|
||||
|
||||
const resultChanged = helpers.setMatchResults(stored, input);
|
||||
this.applyMatchUpdate(stored);
|
||||
this.store.markMatchChanged(stored);
|
||||
|
||||
// Don't propagate if it's a simple score update.
|
||||
if (!resultChanged) return;
|
||||
@@ -86,15 +86,6 @@ export class Propagator {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the opponents of a match.
|
||||
*
|
||||
* @param match A match.
|
||||
*/
|
||||
applyMatchUpdate(match: MatchData): void {
|
||||
this.store.updateMatch(match);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the match(es) following the current match based on this match results.
|
||||
*
|
||||
@@ -169,7 +160,7 @@ export class Propagator {
|
||||
if (!nextMatches[0]) throw Error("First next match is null.");
|
||||
setNextOpponent(nextMatches[0], "opponent1", match, "opponent1");
|
||||
setNextOpponent(nextMatches[0], "opponent2", match, "opponent2");
|
||||
this.applyMatchUpdate(nextMatches[0]);
|
||||
this.store.markMatchChanged(nextMatches[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -197,7 +188,7 @@ export class Propagator {
|
||||
match,
|
||||
winnerSide && helpers.getOtherSide(winnerSide),
|
||||
);
|
||||
this.applyMatchUpdate(nextMatches[1]);
|
||||
this.store.markMatchChanged(nextMatches[1]);
|
||||
} else {
|
||||
const nextSideLB = helpers.getNextSideLoserBracket(
|
||||
match.number,
|
||||
@@ -221,7 +212,7 @@ export class Propagator {
|
||||
*/
|
||||
propagateByeWinners(match: MatchData): void {
|
||||
helpers.resolveByeWinner(match); // BYE propagation is only in non round-robin stages.
|
||||
this.applyMatchUpdate(match);
|
||||
this.store.markMatchChanged(match);
|
||||
|
||||
if (helpers.hasBye(match)) this.updateRelatedMatches(match);
|
||||
}
|
||||
@@ -236,16 +227,12 @@ export class Propagator {
|
||||
* @param roundId ID of the round.
|
||||
*/
|
||||
getRoundPositionalInfo(roundId: number): RoundPositionalInfo {
|
||||
const round = this.store.select("round", roundId);
|
||||
const round = this.store.roundById(roundId);
|
||||
if (!round) throw Error("Round not found.");
|
||||
|
||||
const rounds = this.store.selectAll("round", {
|
||||
groupId: round.groupId,
|
||||
});
|
||||
|
||||
return {
|
||||
roundNumber: round.number,
|
||||
roundCount: rounds.length,
|
||||
roundCount: this.store.roundCountInGroup(round.groupId),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -489,10 +476,7 @@ export class Propagator {
|
||||
stageType === "single_elimination"
|
||||
? 2 /* Consolation final */
|
||||
: 3; /* Grand final */
|
||||
const finalGroup = this.store.selectFirst("group", {
|
||||
stageId: stageId,
|
||||
number: groupNumber,
|
||||
});
|
||||
const finalGroup = this.store.groupByNumber(stageId, groupNumber);
|
||||
if (!finalGroup) return null;
|
||||
return finalGroup.id;
|
||||
}
|
||||
@@ -503,10 +487,7 @@ export class Propagator {
|
||||
* @param stageId ID of the stage.
|
||||
*/
|
||||
private getUpperBracket(stageId: number): GroupData {
|
||||
const winnerBracket = this.store.selectFirst("group", {
|
||||
stageId: stageId,
|
||||
number: 1,
|
||||
});
|
||||
const winnerBracket = this.store.groupByNumber(stageId, 1);
|
||||
if (!winnerBracket) throw Error("Winner bracket not found.");
|
||||
return winnerBracket;
|
||||
}
|
||||
@@ -519,16 +500,10 @@ export class Propagator {
|
||||
*/
|
||||
private participantCount(stageId: number): number {
|
||||
const upperBracket = this.getUpperBracket(stageId);
|
||||
const firstRound = this.store.selectFirst("round", {
|
||||
groupId: upperBracket.id,
|
||||
number: 1,
|
||||
});
|
||||
const firstRound = this.store.roundByNumber(upperBracket.id, 1);
|
||||
if (!firstRound) throw Error("First round not found.");
|
||||
|
||||
const firstRoundMatches = this.store.selectAll("match", {
|
||||
roundId: firstRound.id,
|
||||
});
|
||||
return firstRoundMatches.length * 2;
|
||||
return this.store.matchCountInRound(firstRound.id) * 2;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -537,7 +512,7 @@ export class Propagator {
|
||||
* @param stageId ID of the stage.
|
||||
*/
|
||||
private getLoserBracket(stageId: number): GroupData | null {
|
||||
return this.store.selectFirst("group", { stageId: stageId, number: 2 });
|
||||
return this.store.groupByNumber(stageId, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -590,17 +565,11 @@ export class Propagator {
|
||||
roundNumber: number,
|
||||
matchNumber: number,
|
||||
): MatchData {
|
||||
const round = this.store.selectFirst("round", {
|
||||
groupId: groupId,
|
||||
number: roundNumber,
|
||||
});
|
||||
const round = this.store.roundByNumber(groupId, roundNumber);
|
||||
|
||||
if (!round) throw Error("Round not found.");
|
||||
|
||||
const match = this.store.selectFirst("match", {
|
||||
roundId: round.id,
|
||||
number: matchNumber,
|
||||
});
|
||||
const match = this.store.matchByNumber(round.id, matchNumber);
|
||||
|
||||
if (!match) throw Error("Match not found.");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user