import { asClass, asFunction, createContainer, InjectionMode, type AwilixContainer } from 'awilix'; import type { BaseAppConfig as StockBotAppConfig, UnifiedAppConfig } from '@stock-bot/config'; import { toUnifiedConfig } from '@stock-bot/config'; import { HandlerRegistry } from '@stock-bot/handler-registry'; import { appConfigSchema, type AppConfig } from '../config/schemas'; import { registerApplicationServices, registerCacheServices, registerCoreServices, registerDatabaseServices, } from '../registrations'; import { HandlerScanner } from '../scanner'; import { ServiceLifecycleManager } from '../utils/lifecycle'; import type { ContainerBuildOptions, ServiceDefinitions } from './types'; export class ServiceContainerBuilder { private config: Partial = {}; private unifiedConfig: UnifiedAppConfig | null = null; private options: ContainerBuildOptions = { enableCache: true, enableQueue: true, enableMongoDB: true, enablePostgres: true, enableQuestDB: true, enableBrowser: true, enableProxy: true, skipInitialization: false, initializationTimeout: 30000, }; withConfig(config: AppConfig | StockBotAppConfig | UnifiedAppConfig): this { // Convert to unified config format this.unifiedConfig = toUnifiedConfig(config); this.config = this.transformStockBotConfig(this.unifiedConfig); return this; } withOptions(options: Partial): this { Object.assign(this.options, options); return this; } enableService( service: keyof Omit, enabled = true ): this { this.options[service] = enabled; return this; } skipInitialization(skip = true): this { this.options.skipInitialization = skip; return this; } async build(): Promise> { // Validate and prepare config const validatedConfig = this.prepareConfig(); // Create container const container = createContainer({ injectionMode: InjectionMode.PROXY, strict: true, }); // Register services this.registerServices(container, validatedConfig); // Initialize services if not skipped if (!this.options.skipInitialization) { const lifecycleManager = new ServiceLifecycleManager(); await lifecycleManager.initializeServices(container, this.options.initializationTimeout); } return container; } private prepareConfig(): AppConfig { const finalConfig = this.applyServiceOptions(this.config); return appConfigSchema.parse(finalConfig); } private applyServiceOptions(config: Partial): AppConfig { // Ensure questdb config has the right field names for DI const questdbConfig = config.questdb ? { ...config.questdb, influxPort: (config.questdb as any).influxPort || (config.questdb as any).ilpPort || 9009, } : { enabled: true, host: 'localhost', httpPort: 9000, pgPort: 8812, influxPort: 9009, database: 'questdb', }; return { redis: config.redis || { enabled: this.options.enableCache ?? true, host: 'localhost', port: 6379, db: 0, }, mongodb: config.mongodb || { enabled: this.options.enableMongoDB ?? true, uri: '', database: '', }, postgres: config.postgres || { enabled: this.options.enablePostgres ?? true, host: 'localhost', port: 5432, database: 'postgres', user: 'postgres', password: 'postgres', }, questdb: this.options.enableQuestDB ? questdbConfig : undefined, proxy: this.options.enableProxy ? config.proxy || { enabled: false, cachePrefix: 'proxy:', ttl: 3600 } : undefined, browser: this.options.enableBrowser ? config.browser || { headless: true, timeout: 30000 } : undefined, queue: this.options.enableQueue ? config.queue || { enabled: true, workers: 1, concurrency: 1, enableScheduledJobs: true, delayWorkerStart: false, defaultJobOptions: { attempts: 3, backoff: { type: 'exponential' as const, delay: 1000 }, removeOnComplete: 100, removeOnFail: 50, }, } : undefined, service: config.service, }; } private registerServices( container: AwilixContainer, config: AppConfig ): void { // Register handler infrastructure first container.register({ handlerRegistry: asClass(HandlerRegistry).singleton(), handlerScanner: asClass(HandlerScanner).singleton(), }); registerCoreServices(container, config); registerCacheServices(container, config); registerDatabaseServices(container, config); registerApplicationServices(container, config); // Register service container aggregate container.register({ serviceContainer: asFunction( ({ config: _config, logger, cache, globalCache, proxyManager, browser, queueManager, mongoClient, postgresClient, questdbClient, }) => ({ logger, cache, globalCache, proxy: proxyManager, // Map proxyManager to proxy browser, queue: queueManager, // Map queueManager to queue mongodb: mongoClient, // Map mongoClient to mongodb postgres: postgresClient, // Map postgresClient to postgres questdb: questdbClient, // Map questdbClient to questdb }) ).singleton(), }); } private transformStockBotConfig(config: UnifiedAppConfig): Partial { // Unified config already has flat structure, just extract what we need // Handle questdb field name mapping const questdb = config.questdb ? { enabled: config.questdb.enabled || true, host: config.questdb.host || 'localhost', httpPort: config.questdb.httpPort || 9000, pgPort: config.questdb.pgPort || 8812, influxPort: (config.questdb as any).influxPort || (config.questdb as any).ilpPort || 9009, database: config.questdb.database || 'questdb', } : undefined; return { redis: config.redis, mongodb: config.mongodb, postgres: config.postgres, questdb, queue: config.queue, browser: config.browser, proxy: config.proxy, service: config.service, }; } }