import { InMemorySessionRepository } from '@/packages/infrastructure/repositories/InMemorySessionRepository'; import { MockBrowserAutomationAdapter } from '@/packages/infrastructure/adapters/automation/MockBrowserAutomationAdapter'; import { BrowserDevToolsAdapter } from '@/packages/infrastructure/adapters/automation/BrowserDevToolsAdapter'; import { NutJsAutomationAdapter } from '@/packages/infrastructure/adapters/automation/NutJsAutomationAdapter'; import { MockAutomationEngineAdapter } from '@/packages/infrastructure/adapters/automation/MockAutomationEngineAdapter'; import { PermissionService } from '@/packages/infrastructure/adapters/automation/PermissionService'; import { FixtureServerService } from '@/packages/infrastructure/adapters/automation/FixtureServerService'; import { StartAutomationSessionUseCase } from '@/packages/application/use-cases/StartAutomationSessionUseCase'; import { loadAutomationConfig, getAutomationMode, AutomationMode } from '@/packages/infrastructure/config'; import { PinoLogAdapter } from '@/packages/infrastructure/adapters/logging/PinoLogAdapter'; import { NoOpLogAdapter } from '@/packages/infrastructure/adapters/logging/NoOpLogAdapter'; import { loadLoggingConfig } from '@/packages/infrastructure/config/LoggingConfig'; import type { ISessionRepository } from '@/packages/application/ports/ISessionRepository'; import type { IBrowserAutomation } from '@/packages/application/ports/IBrowserAutomation'; import type { IAutomationEngine } from '@/packages/application/ports/IAutomationEngine'; import type { ILogger } from '@/packages/application/ports/ILogger'; import type { IFixtureServerService } from '@/packages/infrastructure/adapters/automation/FixtureServerService'; export interface BrowserConnectionResult { success: boolean; error?: string; } /** * Create logger based on environment configuration. * In test environment, returns NoOpLogAdapter for silent logging. */ function createLogger(): ILogger { const config = loadLoggingConfig(); if (process.env.NODE_ENV === 'test') { return new NoOpLogAdapter(); } return new PinoLogAdapter(config); } /** * Create browser automation adapter based on configuration mode. * * Mode mapping: * - 'development' → BrowserDevToolsAdapter with fixture server URL * - 'production' → NutJsAutomationAdapter with iRacing window * - 'test' → MockBrowserAutomationAdapter * * @param mode - The automation mode from configuration * @param logger - Logger instance for the adapter * @returns IBrowserAutomation adapter instance */ function createBrowserAutomationAdapter(mode: AutomationMode, logger: ILogger): IBrowserAutomation { const config = loadAutomationConfig(); switch (mode) { case 'development': return new BrowserDevToolsAdapter({ debuggingPort: config.devTools?.debuggingPort ?? 9222, browserWSEndpoint: config.devTools?.browserWSEndpoint, defaultTimeout: config.defaultTimeout, launchBrowser: true, headless: false, startUrl: `http://localhost:${config.fixtureServer?.port ?? 3456}/01-hosted-racing.html`, }, logger.child({ adapter: 'BrowserDevTools' })); case 'production': return new NutJsAutomationAdapter({ mouseSpeed: config.nutJs?.mouseSpeed, keyboardDelay: config.nutJs?.keyboardDelay, defaultTimeout: config.defaultTimeout, }, logger.child({ adapter: 'NutJs' })); case 'test': default: return new MockBrowserAutomationAdapter(); } } export class DIContainer { private static instance: DIContainer; private logger: ILogger; private sessionRepository: ISessionRepository; private browserAutomation: IBrowserAutomation; private automationEngine: IAutomationEngine; private startAutomationUseCase: StartAutomationSessionUseCase; private automationMode: AutomationMode; private permissionService: PermissionService; private fixtureServer: IFixtureServerService | null = null; private fixtureServerInitialized: boolean = false; private constructor() { // Initialize logger first - it's needed by other components this.logger = createLogger(); this.automationMode = getAutomationMode(); this.logger.info('DIContainer initializing', { automationMode: this.automationMode, nodeEnv: process.env.NODE_ENV }); const config = loadAutomationConfig(); this.sessionRepository = new InMemorySessionRepository(); this.browserAutomation = createBrowserAutomationAdapter(config.mode, this.logger); this.automationEngine = new MockAutomationEngineAdapter( this.browserAutomation, this.sessionRepository ); this.startAutomationUseCase = new StartAutomationSessionUseCase( this.automationEngine, this.browserAutomation, this.sessionRepository ); this.permissionService = new PermissionService( this.logger.child({ service: 'PermissionService' }) ); this.logger.info('DIContainer initialized', { automationMode: config.mode, sessionRepositoryType: 'InMemorySessionRepository', browserAutomationType: this.getBrowserAutomationType(config.mode) }); } private getBrowserAutomationType(mode: AutomationMode): string { switch (mode) { case 'development': return 'BrowserDevToolsAdapter'; case 'production': return 'NutJsAutomationAdapter'; case 'test': default: return 'MockBrowserAutomationAdapter'; } } public static getInstance(): DIContainer { if (!DIContainer.instance) { DIContainer.instance = new DIContainer(); } return DIContainer.instance; } public getStartAutomationUseCase(): StartAutomationSessionUseCase { return this.startAutomationUseCase; } public getSessionRepository(): ISessionRepository { return this.sessionRepository; } public getAutomationEngine(): IAutomationEngine { return this.automationEngine; } public getAutomationMode(): AutomationMode { return this.automationMode; } public getBrowserAutomation(): IBrowserAutomation { return this.browserAutomation; } public getLogger(): ILogger { return this.logger; } public getPermissionService(): PermissionService { return this.permissionService; } /** * Initialize fixture server for development mode. * Starts an embedded HTTP server serving static HTML fixtures. * This should be called before initializing browser connection. */ private async initializeFixtureServer(): Promise { const config = loadAutomationConfig(); if (!config.fixtureServer?.autoStart) { this.logger.debug('Fixture server auto-start disabled'); return { success: true }; } if (this.fixtureServerInitialized) { this.logger.debug('Fixture server already initialized'); return { success: true }; } this.fixtureServer = new FixtureServerService(); const port = config.fixtureServer.port; const fixturesPath = config.fixtureServer.fixturesPath; try { await this.fixtureServer.start(port, fixturesPath); const isReady = await this.fixtureServer.waitForReady(5000); if (!isReady) { throw new Error('Fixture server failed to become ready within timeout'); } this.fixtureServerInitialized = true; this.logger.info(`Fixture server started on port ${port}`, { port, fixturesPath, baseUrl: this.fixtureServer.getBaseUrl() }); return { success: true }; } catch (error) { const errorMsg = error instanceof Error ? error.message : 'Failed to start fixture server'; this.logger.error('Fixture server initialization failed', error instanceof Error ? error : new Error(errorMsg)); return { success: false, error: errorMsg }; } } /** * Initialize browser connection based on mode. * In development mode, starts fixture server (if configured) then connects to browser via CDP. * In production mode, connects to iRacing window via nut.js. * In test mode, returns success immediately (no connection needed). */ public async initializeBrowserConnection(): Promise { this.logger.info('Initializing browser connection', { mode: this.automationMode }); if (this.automationMode === 'development') { const fixtureResult = await this.initializeFixtureServer(); if (!fixtureResult.success) { return fixtureResult; } try { const devToolsAdapter = this.browserAutomation as BrowserDevToolsAdapter; await devToolsAdapter.connect(); this.logger.info('Browser connection established', { mode: 'development', adapter: 'BrowserDevTools' }); return { success: true }; } catch (error) { const errorMsg = error instanceof Error ? error.message : 'Failed to connect to browser'; this.logger.error('Browser connection failed', error instanceof Error ? error : new Error(errorMsg), { mode: 'development' }); return { success: false, error: errorMsg }; } } if (this.automationMode === 'production') { try { const nutJsAdapter = this.browserAutomation as NutJsAutomationAdapter; const result = await nutJsAdapter.connect(); if (!result.success) { this.logger.error('Browser connection failed', new Error(result.error || 'Unknown error'), { mode: 'production' }); return { success: false, error: result.error }; } this.logger.info('Browser connection established', { mode: 'production', adapter: 'NutJs' }); return { success: true }; } catch (error) { const errorMsg = error instanceof Error ? error.message : 'Failed to initialize nut.js'; this.logger.error('Browser connection failed', error instanceof Error ? error : new Error(errorMsg), { mode: 'production' }); return { success: false, error: errorMsg }; } } this.logger.debug('Test mode - no browser connection needed'); return { success: true }; // Test mode doesn't need connection } /** * Get the fixture server instance (may be null if not in development mode or not auto-started). */ public getFixtureServer(): IFixtureServerService | null { return this.fixtureServer; } /** * Shutdown the container and cleanup resources. * Should be called when the application is closing. */ public async shutdown(): Promise { this.logger.info('DIContainer shutting down'); if (this.fixtureServer?.isRunning()) { try { await this.fixtureServer.stop(); this.logger.info('Fixture server stopped'); } catch (error) { this.logger.error('Error stopping fixture server', error instanceof Error ? error : new Error('Unknown error')); } } if (this.browserAutomation && 'disconnect' in this.browserAutomation) { try { await (this.browserAutomation as BrowserDevToolsAdapter).disconnect(); this.logger.info('Browser automation disconnected'); } catch (error) { this.logger.error('Error disconnecting browser automation', error instanceof Error ? error : new Error('Unknown error')); } } this.logger.info('DIContainer shutdown complete'); } /** * Reset the singleton instance (useful for testing with different configurations). */ public static resetInstance(): void { DIContainer.instance = undefined as unknown as DIContainer; } }