import 'reflect-metadata'; import { container, Lifecycle } from 'tsyringe'; import { DI_TOKENS } from './di-tokens'; import * as path from 'path'; import * as os from 'os'; // Domain & Application import type { SessionRepositoryPort } from '@gridpilot/automation/application/ports/SessionRepositoryPort'; import type { IBrowserAutomation } from '@gridpilot/automation/application/ports/ScreenAutomationPort'; import type { AutomationEnginePort } from '@gridpilot/automation/application/ports/AutomationEnginePort'; import type { AuthenticationServicePort } from '@gridpilot/automation/application/ports/AuthenticationServicePort'; import type { LoggerPort } from '@gridpilot/automation/application/ports/LoggerPort'; import type { OverlaySyncPort } from '@gridpilot/automation/application/ports/OverlaySyncPort'; import type { CheckoutServicePort } from '@gridpilot/automation/application/ports/CheckoutServicePort'; import { StartAutomationSessionUseCase } from '@gridpilot/automation/application/use-cases/StartAutomationSessionUseCase'; import { CheckAuthenticationUseCase } from '@gridpilot/automation/application/use-cases/CheckAuthenticationUseCase'; import { InitiateLoginUseCase } from '@gridpilot/automation/application/use-cases/InitiateLoginUseCase'; import { ClearSessionUseCase } from '@gridpilot/automation/application/use-cases/ClearSessionUseCase'; import { ConfirmCheckoutUseCase } from '@gridpilot/automation/application/use-cases/ConfirmCheckoutUseCase'; import { OverlaySyncService } from '@gridpilot/automation/application/services/OverlaySyncService'; import type { IAutomationLifecycleEmitter } from '@gridpilot/automation/infrastructure/adapters/IAutomationLifecycleEmitter'; // Infrastructure import { InMemorySessionRepository } from '@gridpilot/automation/infrastructure/repositories/InMemorySessionRepository'; import { MockBrowserAutomationAdapter, PlaywrightAutomationAdapter, AutomationAdapterMode, FixtureServer, } from '@gridpilot/automation/infrastructure/adapters/automation'; import { MockAutomationEngineAdapter } from '@gridpilot/automation/infrastructure/adapters/automation/engine/MockAutomationEngineAdapter'; import { AutomationEngineAdapter } from '@gridpilot/automation/infrastructure/adapters/automation/engine/AutomationEngineAdapter'; import { PinoLogAdapter } from '@gridpilot/automation/infrastructure/adapters/logging/PinoLogAdapter'; import { NoOpLogAdapter } from '@gridpilot/automation/infrastructure/adapters/logging/NoOpLogAdapter'; import { loadAutomationConfig, getAutomationMode, AutomationMode, BrowserModeConfigLoader, } from '@gridpilot/automation/infrastructure/config'; import { loadLoggingConfig } from '@gridpilot/automation/infrastructure/config/LoggingConfig'; // Electron app safe wrapper let electronApp: { getAppPath?: () => string; getPath?: (name: string) => string; isPackaged?: boolean; } | undefined; try { const _electron = require('electron'); electronApp = _electron?.app; } catch { electronApp = undefined; } /** * Resolve session data path with test-tolerance */ export function resolveSessionDataPath(): string { const userDataPath = electronApp?.getPath?.('userData') ?? path.join(process.cwd(), 'userData') ?? os.tmpdir(); return path.join(userDataPath, 'iracing-session'); } /** * Resolve template path with test-tolerance */ export function resolveTemplatePath(): string { const appPath = electronApp?.getAppPath?.() ?? process.cwd(); const isPackaged = electronApp?.isPackaged ?? false; if (isPackaged) { return path.join(appPath, 'resources/templates/iracing'); } return path.join(appPath, '../../resources/templates/iracing'); } /** * Determine adapter mode based on environment */ function getAdapterMode(envMode: AutomationMode): AutomationAdapterMode { return envMode === 'test' ? 'mock' : 'real'; } /** * Check if running in fixture hosted mode */ function isFixtureHostedMode(): boolean { return process.env.NODE_ENV === 'test' && process.env.COMPANION_FIXTURE_HOSTED === '1'; } /** * Configure the DI container with all bindings */ export function configureDIContainer(): void { // Clear any existing registrations container.clearInstances(); // Configuration values const automationMode = getAutomationMode(); const config = loadAutomationConfig(); const loggingConfig = loadLoggingConfig(); const fixtureMode = isFixtureHostedMode(); container.registerInstance(DI_TOKENS.AutomationMode, automationMode); // Logger (singleton) const logger = process.env.NODE_ENV === 'test' ? new NoOpLogAdapter() : new PinoLogAdapter(loggingConfig); container.registerInstance(DI_TOKENS.Logger, logger); // Browser Mode Config Loader (singleton) const browserModeConfigLoader = new BrowserModeConfigLoader(); if (process.env.NODE_ENV === 'development') { browserModeConfigLoader.setDevelopmentMode('headed'); } container.registerInstance(DI_TOKENS.BrowserModeConfigLoader, browserModeConfigLoader); // Session Repository (singleton) container.register( DI_TOKENS.SessionRepository, { useClass: InMemorySessionRepository }, { lifecycle: Lifecycle.Singleton } ); // Browser Automation Adapter (singleton) const browserModeConfig = browserModeConfigLoader.load(); const adapterMode = getAdapterMode(config.mode); const absoluteTemplatePath = resolveTemplatePath(); const sessionDataPath = resolveSessionDataPath(); const safeAppPath = electronApp?.getAppPath?.() ?? process.cwd(); const safeIsPackaged = electronApp?.isPackaged ?? false; logger.debug('Resolved paths', { absoluteTemplatePath, sessionDataPath, appPath: safeAppPath, isPackaged: safeIsPackaged, cwd: process.cwd(), }); logger.info('Creating browser automation adapter', { envMode: config.mode, adapterMode, browserMode: browserModeConfig.mode, browserModeSource: browserModeConfig.source, }); let browserAutomation: PlaywrightAutomationAdapter | MockBrowserAutomationAdapter; switch (config.mode) { case 'production': case 'development': browserAutomation = new PlaywrightAutomationAdapter( { headless: browserModeConfig.mode === 'headless', mode: adapterMode, userDataDir: sessionDataPath, baseUrl: fixtureMode ? 'http://localhost:3456' : '', }, logger.child({ adapter: 'Playwright', mode: adapterMode }), browserModeConfigLoader ); break; case 'test': default: if (fixtureMode) { browserAutomation = new PlaywrightAutomationAdapter( { headless: browserModeConfig.mode === 'headless', timeout: config.defaultTimeout ?? 10_000, baseUrl: 'http://localhost:3456', mode: 'real', userDataDir: sessionDataPath, }, logger.child({ adapter: 'Playwright', mode: 'real' }), browserModeConfigLoader ); } else { browserAutomation = new MockBrowserAutomationAdapter(); } break; } container.registerInstance( DI_TOKENS.BrowserAutomation, browserAutomation ); // Checkout Service (singleton, backed by browser automation) container.registerInstance( DI_TOKENS.CheckoutService, browserAutomation as unknown as CheckoutServicePort ); // Automation Engine (singleton) const sessionRepository = container.resolve(DI_TOKENS.SessionRepository); let automationEngine: AutomationEnginePort; if (fixtureMode) { automationEngine = new AutomationEngineAdapter( browserAutomation, sessionRepository ); } else { automationEngine = new MockAutomationEngineAdapter( browserAutomation, sessionRepository ); } container.registerInstance( DI_TOKENS.AutomationEngine, automationEngine ); // Fixture Server (singleton, nullable) if (fixtureMode) { container.registerInstance(DI_TOKENS.FixtureServer, new FixtureServer()); } // Use Cases - create singleton instance directly const startAutomationUseCase = new StartAutomationSessionUseCase( automationEngine, browserAutomation, sessionRepository ); container.registerInstance(DI_TOKENS.StartAutomationUseCase, startAutomationUseCase); // Authentication-related use cases (only if adapter supports it) if (browserAutomation instanceof PlaywrightAutomationAdapter && !fixtureMode) { const authService = browserAutomation as AuthenticationServicePort; container.registerInstance( DI_TOKENS.CheckAuthenticationUseCase, new CheckAuthenticationUseCase(authService) ); container.registerInstance( DI_TOKENS.InitiateLoginUseCase, new InitiateLoginUseCase(authService) ); container.registerInstance( DI_TOKENS.ClearSessionUseCase, new ClearSessionUseCase(authService) ); container.registerInstance( DI_TOKENS.AuthenticationService, authService ); } // Overlay Sync Service - create singleton instance directly const lifecycleEmitter = browserAutomation as unknown as IAutomationLifecycleEmitter; const publisher = { publish: async (event: unknown) => { try { logger.debug?.('OverlaySyncPublisher.publish', { event }); } catch { // swallow } }, }; const overlaySyncService = new OverlaySyncService({ lifecycleEmitter, publisher, logger, }); container.registerInstance(DI_TOKENS.OverlaySyncPort, overlaySyncService); logger.info('DI container configured', { automationMode: config.mode, sessionRepositoryType: 'InMemorySessionRepository', browserAutomationType: browserAutomation.constructor.name, }); } /** * Reset the container (for testing) */ export function resetDIContainer(): void { container.clearInstances(); } /** * Get the TSyringe container instance */ export function getDIContainer() { return container; }