Files
gridpilot.gg/adapters/racing/persistence/inmemory/InMemoryStandingRepository.ts
Marc Mintel 6df38a462a
Some checks failed
CI / lint-typecheck (pull_request) Failing after 4m50s
CI / tests (pull_request) Has been skipped
CI / contract-tests (pull_request) Has been skipped
CI / e2e-tests (pull_request) Has been skipped
CI / comment-pr (pull_request) Has been skipped
CI / commit-types (pull_request) Has been skipped
integration tests
2026-01-23 11:44:59 +01:00

277 lines
12 KiB
TypeScript

/**
* Infrastructure Adapter: InMemoryStandingRepository
*
* In-memory implementation of IStandingRepository.
* Stores data in Map structure and calculates standings from race results.
*/
import { Standing } from '@core/racing/domain/entities/Standing';
import type { StandingRepository } from '@core/racing/domain/repositories/StandingRepository';
import type { ResultRepository } from '@core/racing/domain/repositories/ResultRepository';
import type { RaceRepository } from '@core/racing/domain/repositories/RaceRepository';
import type { LeagueRepository } from '@core/racing/domain/repositories/LeagueRepository';
import type { Logger } from '@core/shared/domain/Logger';
export class InMemoryStandingRepository implements StandingRepository {
private standings: Map<string, Standing>;
private resultRepository: ResultRepository | null;
private raceRepository: RaceRepository | null;
private leagueRepository: LeagueRepository | null;
private readonly logger: Logger;
private readonly pointsSystems: Record<string, Record<number, number>>;
constructor(
logger: Logger,
pointsSystems: Record<string, Record<number, number>>,
resultRepository?: ResultRepository | null,
raceRepository?: RaceRepository | null,
leagueRepository?: LeagueRepository | null
) {
this.logger = logger;
this.pointsSystems = pointsSystems;
this.logger.info('InMemoryStandingRepository initialized.');
this.standings = new Map();
this.resultRepository = resultRepository ?? null;
this.raceRepository = raceRepository ?? null;
this.leagueRepository = leagueRepository ?? null;
}
private getKey(leagueId: string, driverId: string): string {
return `${leagueId}:${driverId}`;
}
async findByLeagueId(leagueId: string): Promise<Standing[]> {
this.logger.debug(`Finding standings for league id: ${leagueId}`);
try {
const standings = Array.from(this.standings.values())
.filter(standing => standing.leagueId.toString() === leagueId)
.sort((a, b) => {
if (a.position.toNumber() !== b.position.toNumber()) {
return a.position.toNumber() - b.position.toNumber();
}
return b.points.toNumber() - a.points.toNumber();
});
this.logger.info(`Found ${standings.length} standings for league id: ${leagueId}.`);
return standings;
} catch (error) {
this.logger.error(`Error finding standings for league id ${leagueId}:`, error instanceof Error ? error : new Error(String(error)));
throw error;
}
}
async findByDriverIdAndLeagueId(driverId: string, leagueId: string): Promise<Standing | null> {
this.logger.debug(`Finding standing for driver: ${driverId}, league: ${leagueId}`);
try {
const key = this.getKey(leagueId, driverId);
const standing = this.standings.get(key) ?? null;
if (standing) {
this.logger.info(`Found standing for driver: ${driverId}, league: ${leagueId}.`);
} else {
this.logger.warn(`Standing for driver ${driverId}, league ${leagueId} not found.`);
}
return standing;
} catch (error) {
this.logger.error(`Error finding standing for driver ${driverId}, league ${leagueId}:`, error instanceof Error ? error : new Error(String(error)));
throw error;
}
}
async findAll(): Promise<Standing[]> {
this.logger.debug('Finding all standings.');
try {
const standings = Array.from(this.standings.values());
this.logger.info(`Found ${standings.length} standings.`);
return standings;
} catch (error) {
this.logger.error('Error finding all standings:', error instanceof Error ? error : new Error(String(error)));
throw error;
}
}
async save(standing: Standing): Promise<Standing> {
this.logger.debug(`Saving standing for league: ${standing.leagueId}, driver: ${standing.driverId}`);
try {
const key = this.getKey(standing.leagueId.toString(), standing.driverId.toString());
if (this.standings.has(key)) {
this.logger.debug(`Updating existing standing for league: ${standing.leagueId}, driver ${standing.driverId}.`);
} else {
this.logger.debug(`Creating new standing for league: ${standing.leagueId}, driver: ${standing.driverId}.`);
}
this.standings.set(key, standing);
this.logger.info(`Standing for league ${standing.leagueId}, driver ${standing.driverId} saved successfully.`);
return standing;
} catch (error) {
this.logger.error(`Error saving standing for league ${standing.leagueId}, driver ${standing.driverId}:`, error instanceof Error ? error : new Error(String(error)));
throw error;
}
}
async saveMany(standings: Standing[]): Promise<Standing[]> {
this.logger.debug(`Saving ${standings.length} standings.`);
try {
standings.forEach(standing => {
const key = this.getKey(standing.leagueId.toString(), standing.driverId.toString());
this.standings.set(key, standing);
});
this.logger.info(`${standings.length} standings saved successfully.`);
return standings;
} catch (error) {
this.logger.error(`Error saving many standings:`, error instanceof Error ? error : new Error(String(error)));
throw error;
}
}
async delete(leagueId: string, driverId: string): Promise<void> {
this.logger.debug(`Deleting standing for league: ${leagueId}, driver: ${driverId}`);
try {
const key = this.getKey(leagueId, driverId);
if (this.standings.delete(key)) {
this.logger.info(`Standing for league ${leagueId}, driver ${driverId} deleted successfully.`);
} else {
this.logger.warn(`Standing for league ${leagueId}, driver ${driverId} not found for deletion.`);
}
} catch (error) {
this.logger.error(`Error deleting standing for league ${leagueId}, driver ${driverId}:`, error instanceof Error ? error : new Error(String(error)));
throw error;
}
}
async deleteByLeagueId(leagueId: string): Promise<void> {
this.logger.debug(`Deleting all standings for league id: ${leagueId}`);
try {
const toDelete = Array.from(this.standings.values())
.filter(standing => standing.leagueId.toString() === leagueId);
toDelete.forEach(standing => {
const key = this.getKey(standing.leagueId.toString(), standing.driverId.toString());
this.standings.delete(key);
});
this.logger.info(`Deleted ${toDelete.length} standings for league id: ${leagueId}.`);
} catch (error) {
this.logger.error(`Error deleting standings by league id ${leagueId}:`, error instanceof Error ? error : new Error(String(error)));
throw error;
}
}
async exists(leagueId: string, driverId: string): Promise<boolean> {
this.logger.debug(`Checking existence of standing for league: ${leagueId}, driver: ${driverId}`);
try {
const key = this.getKey(leagueId, driverId);
const exists = this.standings.has(key);
this.logger.debug(`Standing for league ${leagueId}, driver ${driverId} exists: ${exists}.`);
return exists;
} catch (error) {
this.logger.error(`Error checking existence of standing for league ${leagueId}, driver ${driverId}:`, error instanceof Error ? error : new Error(String(error)));
throw error;
}
}
async clear(): Promise<void> {
this.logger.debug('Clearing all standings.');
this.standings.clear();
}
async recalculate(leagueId: string): Promise<Standing[]> {
this.logger.debug(`Recalculating standings for league id: ${leagueId}`);
try {
if (!this.resultRepository || !this.raceRepository || !this.leagueRepository) {
this.logger.error('Cannot recalculate standings: missing required repositories.');
throw new Error('Cannot recalculate standings: missing required repositories');
}
const league = await this.leagueRepository.findById(leagueId);
if (!league) {
this.logger.warn(`League with ID ${leagueId} not found during recalculation.`);
throw new Error(`League with ID ${leagueId} not found`);
}
this.logger.debug(`League ${leagueId} found for recalculation.`);
const resolvedPointsSystem =
league.settings.customPoints ??
this.pointsSystems[league.settings.pointsSystem] ??
this.pointsSystems['f1-2024'];
if (!resolvedPointsSystem) {
this.logger.error(`No points system configured for league ${leagueId}.`);
throw new Error('No points system configured for league');
}
this.logger.debug(`Resolved points system for league ${leagueId}.`);
const races = await this.raceRepository.findCompletedByLeagueId(leagueId);
this.logger.debug(`Found ${races.length} completed races for league ${leagueId}.`);
if (races.length === 0) {
this.logger.warn(`No completed races found for league ${leagueId}. Standings will be empty.`);
return [];
}
const allResults = await Promise.all(
races.map(async (race: any) => {
this.logger.debug(`Fetching results for race ${race.id}.`);
const results = await this.resultRepository!.findByRaceId(race.id);
this.logger.debug(`Found ${results.length} results for race ${race.id}.`);
return results;
})
);
const results = allResults.flat();
this.logger.debug(`Collected ${results.length} results from all completed races.`);
const standingsMap = new Map<string, Standing>();
const normalizePosition = (position: unknown): number => {
if (typeof position === 'number') return position;
if (typeof position === 'string') return Number(position);
if (position && typeof (position as { toNumber?: unknown }).toNumber === 'function') {
return (position as { toNumber: () => number }).toNumber();
}
return Number(position);
};
results.forEach((result: any) => {
const driverIdStr = result.driverId.toString();
let standing = standingsMap.get(driverIdStr);
if (!standing) {
standing = Standing.create({
leagueId,
driverId: driverIdStr,
});
this.logger.debug(`Created new standing for driver ${driverIdStr} in league ${leagueId}.`);
}
const position = normalizePosition((result as { position: unknown }).position);
standing = standing.addRaceResult(position, resolvedPointsSystem);
standingsMap.set(driverIdStr, standing);
this.logger.debug(`Driver ${driverIdStr} in league ${leagueId} accumulated ${standing.points} points.`);
});
this.logger.debug(`Calculated initial standings for ${standingsMap.size} drivers.`);
const sortedStandings = Array.from(standingsMap.values())
.sort((a, b) => {
if (b.points.toNumber() !== a.points.toNumber()) {
return b.points.toNumber() - a.points.toNumber();
}
if (b.wins !== a.wins) {
return b.wins - a.wins;
}
return b.racesCompleted - a.racesCompleted;
});
this.logger.debug(`Sorted standings for ${sortedStandings.length} drivers.`);
const updatedStandings = sortedStandings.map((standing, index) => {
const newStanding = standing.updatePosition(index + 1);
this.logger.debug(`Assigned position ${newStanding.position} to driver ${newStanding.driverId}.`);
return newStanding;
});
await this.saveMany(updatedStandings);
this.logger.info(`Successfully recalculated and saved standings for league ${leagueId}.`);
return updatedStandings;
} catch (error) {
this.logger.error(`Error recalculating standings for league ${leagueId}:`, error instanceof Error ? error : new Error(String(error)));
throw error;
}
}
}