Backend Complete: Interface Refactoring & Service Container Enhancements
Repository Interface Optimization: - Created IBaseRepository.ts and IPaginatedRepository.ts - Refactored all 7 repository interfaces to extend base interfaces - Eliminated ~200 lines of redundant code (70% reduction) - Improved type safety and maintainability Dependency Injection Improvements: - Added EmailService and GameTokenService to DIContainer - Updated CreateUserCommandHandler constructor for DI - Updated RequestPasswordResetCommandHandler constructor for DI - Enhanced testability and service consistency Environment Configuration: - Created comprehensive .env.example with 40+ variables - Organized into 12 logical sections (Database, Security, Email, etc.) - Added security guidelines and best practices - Documented all backend environment requirements Documentation: - Added comprehensive codebase review - Created refactoring summary report - Added frontend implementation guide Impact: Improved code quality, reduced maintenance overhead, enhanced developer experience
This commit is contained in:
@@ -0,0 +1,290 @@
|
||||
import { StartGameCommand } from './StartGameCommand';
|
||||
import { GameAggregate, GameDeck, GameCard, DeckType, GameState } from '../../../Domain/Game/GameAggregate';
|
||||
import { DeckAggregate } from '../../../Domain/Deck/DeckAggregate';
|
||||
import { IGameRepository } from '../../../Domain/IRepository/IGameRepository';
|
||||
import { IDeckRepository } from '../../../Domain/IRepository/IDeckRepository';
|
||||
import { DIContainer } from '../../Services/DIContainer';
|
||||
import { RedisService } from '../../Services/RedisService';
|
||||
import { logOther, logError } from '../../Services/Logger';
|
||||
import { randomBytes } from 'crypto';
|
||||
import { GenerateBoardCommand } from './GenerateBoardCommand';
|
||||
|
||||
export interface ActiveGameData {
|
||||
gameId: string;
|
||||
gameCode: string;
|
||||
hostId?: string;
|
||||
maxPlayers: number;
|
||||
currentPlayers: GamePlayerData[];
|
||||
state: GameState;
|
||||
createdAt: Date;
|
||||
startedAt?: Date;
|
||||
currentTurn?: string;
|
||||
websocketRoom: string;
|
||||
}
|
||||
|
||||
export interface GamePlayerData {
|
||||
playerId: string;
|
||||
playerName?: string;
|
||||
joinedAt: Date;
|
||||
isOnline: boolean;
|
||||
position?: number;
|
||||
}
|
||||
|
||||
export class StartGameCommandHandler {
|
||||
private gameRepository: IGameRepository;
|
||||
private deckRepository: IDeckRepository;
|
||||
private redisService: RedisService;
|
||||
|
||||
constructor() {
|
||||
this.gameRepository = DIContainer.getInstance().gameRepository;
|
||||
this.deckRepository = DIContainer.getInstance().deckRepository;
|
||||
this.redisService = RedisService.getInstance();
|
||||
}
|
||||
|
||||
async handle(command: StartGameCommand): Promise<GameAggregate> {
|
||||
const startTime = performance.now();
|
||||
|
||||
try {
|
||||
logOther('Starting game creation', `deckCount: ${command.deckids.length}, maxPlayers: ${command.maxplayers}, loginType: ${command.logintype}`);
|
||||
|
||||
// Generate unique game code
|
||||
const gamecode = this.generateGameCode();
|
||||
|
||||
// Fetch all decks by IDs
|
||||
const decks = await this.fetchDecks(command.deckids);
|
||||
|
||||
// Validate we have 3 deck types
|
||||
this.validateDeckTypes(decks);
|
||||
|
||||
// Group decks by type and shuffle cards within each type
|
||||
const gamedecks = await this.createShuffledGameDecks(decks);
|
||||
|
||||
// Create the game aggregate
|
||||
const gameData: Partial<GameAggregate> = {
|
||||
gamecode,
|
||||
maxplayers: command.maxplayers,
|
||||
logintype: command.logintype,
|
||||
createdby: command.userid || null,
|
||||
orgid: command.orgid || null,
|
||||
gamedecks,
|
||||
players: [],
|
||||
started: false,
|
||||
finished: false,
|
||||
winner: null,
|
||||
state: GameState.WAITING,
|
||||
startdate: null,
|
||||
enddate: null
|
||||
};
|
||||
|
||||
// Save the game to database
|
||||
const savedGame = await this.gameRepository.create(gameData);
|
||||
|
||||
// Create Redis object for real-time game management
|
||||
await this.createGameInRedis(savedGame, command.userid);
|
||||
|
||||
// Trigger async board generation (don't block game creation)
|
||||
this.triggerAsyncBoardGeneration(savedGame.id).catch((error: Error) => {
|
||||
logError('Async board generation failed', error);
|
||||
});
|
||||
|
||||
const endTime = performance.now();
|
||||
logOther('Game created successfully', `gameId: ${savedGame.id}, gameCode: ${savedGame.gamecode}, executionTime: ${Math.round(endTime - startTime)}ms`);
|
||||
|
||||
return savedGame;
|
||||
|
||||
} catch (error) {
|
||||
const endTime = performance.now();
|
||||
logError('Failed to create game', error instanceof Error ? error : new Error(String(error)));
|
||||
logOther('Game creation failed', `executionTime: ${Math.round(endTime - startTime)}ms`);
|
||||
throw new Error('Failed to start game: ' + (error instanceof Error ? error.message : String(error)));
|
||||
}
|
||||
}
|
||||
|
||||
private generateGameCode(): string {
|
||||
// Generate a 6-character alphanumeric game code
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
let result = '';
|
||||
const randomBytesArray = randomBytes(6);
|
||||
|
||||
for (let i = 0; i < 6; i++) {
|
||||
result += chars[randomBytesArray[i] % chars.length];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async fetchDecks(deckIds: string[]): Promise<DeckAggregate[]> {
|
||||
const decks: DeckAggregate[] = [];
|
||||
|
||||
for (const deckId of deckIds) {
|
||||
const deck = await this.deckRepository.findById(deckId);
|
||||
if (!deck) {
|
||||
throw new Error(`Deck with ID ${deckId} not found`);
|
||||
}
|
||||
decks.push(deck);
|
||||
}
|
||||
|
||||
return decks;
|
||||
}
|
||||
|
||||
private validateDeckTypes(decks: DeckAggregate[]): void {
|
||||
const deckTypes = new Set(decks.map(deck => deck.type));
|
||||
|
||||
// Check if we have all 3 required deck types (LUCK=0, JOKER=1, QUESTION=2)
|
||||
const requiredTypes = [0, 1, 2]; // Based on Type enum in DeckAggregate
|
||||
const missingTypes = requiredTypes.filter(type => !deckTypes.has(type));
|
||||
|
||||
if (missingTypes.length > 0) {
|
||||
throw new Error(`Missing required deck types: ${missingTypes.join(', ')}. Game requires LUCK, JOKER, and QUESTION deck types.`);
|
||||
}
|
||||
|
||||
logOther('Deck types validation passed', `foundTypes: [${Array.from(deckTypes).join(', ')}]`);
|
||||
}
|
||||
|
||||
private async createShuffledGameDecks(decks: DeckAggregate[]): Promise<GameDeck[]> {
|
||||
// Group decks by type
|
||||
const decksByType = new Map<number, DeckAggregate[]>();
|
||||
|
||||
decks.forEach(deck => {
|
||||
if (!decksByType.has(deck.type)) {
|
||||
decksByType.set(deck.type, []);
|
||||
}
|
||||
decksByType.get(deck.type)!.push(deck);
|
||||
});
|
||||
|
||||
const gamedecks: GameDeck[] = [];
|
||||
|
||||
// Process each deck type
|
||||
for (const [deckType, typeDecks] of decksByType) {
|
||||
// Collect all cards from decks of this type
|
||||
const allCards: GameCard[] = [];
|
||||
|
||||
typeDecks.forEach(deck => {
|
||||
deck.cards.forEach(card => {
|
||||
const gameCard: GameCard = {
|
||||
cardid: this.generateCardId(),
|
||||
question: card.text,
|
||||
answer: card.answer || undefined,
|
||||
consequence: card.consequence || null,
|
||||
played: false,
|
||||
playerid: undefined
|
||||
};
|
||||
allCards.push(gameCard);
|
||||
});
|
||||
});
|
||||
|
||||
// Shuffle all cards of this type
|
||||
const shuffledCards = this.shuffleArray(allCards);
|
||||
|
||||
// Create game deck for this type
|
||||
const gameDeck: GameDeck = {
|
||||
deckid: typeDecks[0].id, // Use first deck ID as representative
|
||||
decktype: this.mapDeckTypeToGameDeckType(deckType),
|
||||
cards: shuffledCards
|
||||
};
|
||||
|
||||
gamedecks.push(gameDeck);
|
||||
|
||||
logOther('Created shuffled game deck', `type: ${deckType}, cardCount: ${shuffledCards.length}, sourceDecks: ${typeDecks.length}`);
|
||||
}
|
||||
|
||||
return gamedecks;
|
||||
}
|
||||
|
||||
private mapDeckTypeToGameDeckType(deckType: number): DeckType {
|
||||
// Map DeckAggregate.Type to GameAggregate.DeckType
|
||||
switch (deckType) {
|
||||
case 0: return DeckType.LUCK; // LUCK = 0
|
||||
case 1: return DeckType.JOCKER; // JOKER = 1
|
||||
case 2: return DeckType.QUEST; // QUESTION = 2
|
||||
default: throw new Error(`Unknown deck type: ${deckType}`);
|
||||
}
|
||||
}
|
||||
|
||||
private shuffleArray<T>(array: T[]): T[] {
|
||||
const shuffled = [...array];
|
||||
for (let i = shuffled.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
||||
}
|
||||
return shuffled;
|
||||
}
|
||||
|
||||
private generateCardId(): string {
|
||||
return randomBytes(8).toString('hex');
|
||||
}
|
||||
|
||||
private async createGameInRedis(game: GameAggregate, hostId?: string): Promise<void> {
|
||||
try {
|
||||
const redisKey = `game:${game.id}`;
|
||||
|
||||
const gameData: ActiveGameData = {
|
||||
gameId: game.id,
|
||||
gameCode: game.gamecode,
|
||||
hostId: hostId,
|
||||
maxPlayers: game.maxplayers,
|
||||
currentPlayers: [],
|
||||
state: game.state,
|
||||
createdAt: game.createdate,
|
||||
websocketRoom: `game_${game.gamecode}`
|
||||
};
|
||||
|
||||
// Store game data in Redis with TTL (24 hours)
|
||||
await this.redisService.setWithExpiry(redisKey, JSON.stringify(gameData), 24 * 60 * 60);
|
||||
|
||||
// Create game room for WebSocket connections
|
||||
await this.redisService.set(`game_room:${game.gamecode}`, game.id);
|
||||
|
||||
logOther('Game created in Redis', {
|
||||
gameId: game.id,
|
||||
gameCode: game.gamecode,
|
||||
hostId: hostId,
|
||||
websocketRoom: gameData.websocketRoom,
|
||||
redisKey
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logError('Failed to create game in Redis', error instanceof Error ? error : new Error(String(error)));
|
||||
// Don't throw error here - Redis failure shouldn't prevent game creation
|
||||
logOther('Game created successfully despite Redis error', {
|
||||
gameId: game.id,
|
||||
gameCode: game.gamecode
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async triggerAsyncBoardGeneration(gameId: string): Promise<void> {
|
||||
try {
|
||||
// Calculate default field counts based on game configuration
|
||||
// For now, use reasonable defaults - this should be configurable by host in the future
|
||||
const maxSpecialFieldsPercentage = parseInt(process.env.MAX_SPECIAL_FIELDS_PERCENTAGE || '67');
|
||||
const maxSpecialFields = Math.floor((100 * maxSpecialFieldsPercentage) / 100);
|
||||
|
||||
// Default distribution: 60% positive, 25% negative, 15% luck
|
||||
const positiveFieldCount = Math.floor(maxSpecialFields * 0.6);
|
||||
const negativeFieldCount = Math.floor(maxSpecialFields * 0.25);
|
||||
const luckFieldCount = Math.floor(maxSpecialFields * 0.15);
|
||||
|
||||
const command: GenerateBoardCommand = {
|
||||
gameId,
|
||||
positiveFieldCount,
|
||||
negativeFieldCount,
|
||||
luckFieldCount
|
||||
};
|
||||
|
||||
logOther(`Triggering async board generation for game ${gameId}`, {
|
||||
positiveFieldCount,
|
||||
negativeFieldCount,
|
||||
luckFieldCount,
|
||||
totalSpecialFields: positiveFieldCount + negativeFieldCount + luckFieldCount
|
||||
});
|
||||
|
||||
// Execute board generation in background
|
||||
await DIContainer.getInstance().generateBoardCommandHandler.execute(command);
|
||||
|
||||
} catch (error) {
|
||||
logError(`Async board generation failed for game ${gameId}`, error as Error);
|
||||
// Don't propagate error - board generation failure shouldn't affect game creation
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user