refactor adapters
This commit is contained in:
@@ -0,0 +1,302 @@
|
||||
/**
|
||||
* Infrastructure Adapter: InMemoryStandingRepository
|
||||
*
|
||||
* In-memory implementation of IStandingRepository.
|
||||
* Stores data in Map structure and calculates standings from race results.
|
||||
*/
|
||||
|
||||
import { Standing } from '@gridpilot/racing/domain/entities/Standing';
|
||||
import type { IStandingRepository } from '@gridpilot/racing/domain/repositories/IStandingRepository';
|
||||
import type { IResultRepository } from '@gridpilot/racing/domain/repositories/IResultRepository';
|
||||
import type { IRaceRepository } from '@gridpilot/racing/domain/repositories/IRaceRepository';
|
||||
import type { ILeagueRepository } from '@gridpilot/racing/domain/repositories/ILeagueRepository';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
/**
|
||||
* Points systems presets
|
||||
*/
|
||||
const POINTS_SYSTEMS: Record<string, Record<number, number>> = {
|
||||
'f1-2024': {
|
||||
1: 25, 2: 18, 3: 15, 4: 12, 5: 10,
|
||||
6: 8, 7: 6, 8: 4, 9: 2, 10: 1
|
||||
},
|
||||
'indycar': {
|
||||
1: 50, 2: 40, 3: 35, 4: 32, 5: 30,
|
||||
6: 28, 7: 26, 8: 24, 9: 22, 10: 20,
|
||||
11: 19, 12: 18, 13: 17, 14: 16, 15: 15
|
||||
}
|
||||
};
|
||||
|
||||
export class InMemoryStandingRepository implements IStandingRepository {
|
||||
private standings: Map<string, Standing>;
|
||||
private resultRepository: IResultRepository | null;
|
||||
private raceRepository: IRaceRepository | null;
|
||||
private leagueRepository: ILeagueRepository | null;
|
||||
private readonly logger: ILogger;
|
||||
|
||||
constructor(
|
||||
logger: ILogger,
|
||||
seedData?: Standing[],
|
||||
resultRepository?: IResultRepository | null,
|
||||
raceRepository?: IRaceRepository | null,
|
||||
leagueRepository?: ILeagueRepository | null
|
||||
) {
|
||||
this.logger = logger;
|
||||
this.logger.info('InMemoryStandingRepository initialized.');
|
||||
this.standings = new Map();
|
||||
this.resultRepository = resultRepository ?? null;
|
||||
this.raceRepository = raceRepository ?? null;
|
||||
this.leagueRepository = leagueRepository ?? null;
|
||||
|
||||
if (seedData) {
|
||||
seedData.forEach(standing => {
|
||||
const key = this.getKey(standing.leagueId, standing.driverId);
|
||||
this.standings.set(key, standing);
|
||||
this.logger.debug(`Seeded standing for league ${standing.leagueId}, driver ${standing.driverId}.`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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 === leagueId)
|
||||
.sort((a, b) => {
|
||||
// Sort by position (lower is better)
|
||||
if (a.position !== b.position) {
|
||||
return a.position - b.position;
|
||||
}
|
||||
// If positions are equal, sort by points (higher is better)
|
||||
return b.points - a.points;
|
||||
});
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
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, standing.driverId);
|
||||
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);
|
||||
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, standing.driverId);
|
||||
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);
|
||||
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);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async deleteByLeagueId(leagueId: string): Promise<void> {
|
||||
this.logger.debug(`Deleting all standings for league id: ${leagueId}`);
|
||||
try {
|
||||
const initialCount = Array.from(this.standings.values()).filter(s => s.leagueId === leagueId).length;
|
||||
const toDelete = Array.from(this.standings.values())
|
||||
.filter(standing => standing.leagueId === leagueId);
|
||||
|
||||
toDelete.forEach(standing => {
|
||||
const key = this.getKey(standing.leagueId, standing.driverId);
|
||||
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);
|
||||
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);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
// Get league to determine points system
|
||||
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.`);
|
||||
|
||||
// Get points system
|
||||
const resolvedPointsSystem =
|
||||
league.settings.customPoints ??
|
||||
POINTS_SYSTEMS[league.settings.pointsSystem] ??
|
||||
POINTS_SYSTEMS['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}.`);
|
||||
|
||||
// Get all completed races for the league
|
||||
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 [];
|
||||
}
|
||||
|
||||
// Get all results for these races
|
||||
const allResults = await Promise.all(
|
||||
races.map(async race => {
|
||||
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.`);
|
||||
|
||||
// Calculate standings per driver
|
||||
const standingsMap = new Map<string, Standing>();
|
||||
|
||||
results.forEach(result => {
|
||||
let standing = standingsMap.get(result.driverId);
|
||||
|
||||
if (!standing) {
|
||||
standing = Standing.create({
|
||||
leagueId,
|
||||
driverId: result.driverId,
|
||||
});
|
||||
this.logger.debug(`Created new standing for driver ${result.driverId} in league ${leagueId}.`);
|
||||
}
|
||||
|
||||
// Add points from this result
|
||||
standing = standing.addRaceResult(result.position, pointsSystem);
|
||||
standingsMap.set(result.driverId, standing);
|
||||
this.logger.debug(`Driver ${result.driverId} in league ${leagueId} accumulated ${standing.points} points.`);
|
||||
});
|
||||
this.logger.debug(`Calculated initial standings for ${standingsMap.size} drivers.`);
|
||||
|
||||
// Sort by points and assign positions
|
||||
const sortedStandings = Array.from(standingsMap.values())
|
||||
.sort((a, b) => {
|
||||
if (b.points !== a.points) {
|
||||
return b.points - a.points;
|
||||
}
|
||||
// Tie-breaker: most wins
|
||||
if (b.wins !== a.wins) {
|
||||
return b.wins - a.wins;
|
||||
}
|
||||
// Tie-breaker: most races completed
|
||||
return b.racesCompleted - a.racesCompleted;
|
||||
});
|
||||
this.logger.debug(`Sorted standings for ${sortedStandings.length} drivers.`);
|
||||
|
||||
// Assign positions
|
||||
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;
|
||||
});
|
||||
|
||||
// Save all standings
|
||||
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);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available points systems
|
||||
*/
|
||||
static getPointsSystems(): Record<string, Record<number, number>> {
|
||||
return POINTS_SYSTEMS;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user