stock-bot/libs/core/di/src/container/builder.ts

214 lines
6.6 KiB
TypeScript

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<AppConfig> = {};
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<ContainerBuildOptions>): this {
Object.assign(this.options, options);
return this;
}
enableService(
service: keyof Omit<ContainerBuildOptions, 'skipInitialization' | 'initializationTimeout'>,
enabled = true
): this {
this.options[service] = enabled;
return this;
}
skipInitialization(skip = true): this {
this.options.skipInitialization = skip;
return this;
}
async build(): Promise<AwilixContainer<ServiceDefinitions>> {
// Validate and prepare config
const validatedConfig = this.prepareConfig();
// Create container
const container = createContainer<ServiceDefinitions>({
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>): 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<ServiceDefinitions>,
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<AppConfig> {
// 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,
};
}
}