/** * Awilix DI Container Setup * Creates a decoupled, reusable dependency injection container */ import { asFunction, asValue, createContainer, InjectionMode, type AwilixContainer } from 'awilix'; import { z } from 'zod'; import { Browser } from '@stock-bot/browser'; import { createCache, type CacheProvider } from '@stock-bot/cache'; import type { IServiceContainer } from '@stock-bot/handlers'; import { getLogger, type Logger } from '@stock-bot/logger'; import { MongoDBClient } from '@stock-bot/mongodb'; import { PostgreSQLClient } from '@stock-bot/postgres'; import { ProxyManager } from '@stock-bot/proxy'; import { QuestDBClient } from '@stock-bot/questdb'; import { type QueueManager } from '@stock-bot/queue'; // Configuration schema with validation const appConfigSchema = z.object({ redis: z.object({ enabled: z.boolean().optional(), host: z.string(), port: z.number(), password: z.string().optional(), username: z.string().optional(), db: z.number().optional(), }), mongodb: z.object({ enabled: z.boolean().optional(), uri: z.string(), database: z.string(), }), postgres: z.object({ enabled: z.boolean().optional(), host: z.string(), port: z.number(), database: z.string(), user: z.string(), password: z.string(), }), questdb: z .object({ enabled: z.boolean().optional(), host: z.string(), httpPort: z.number().optional(), pgPort: z.number().optional(), influxPort: z.number().optional(), database: z.string().optional(), }) .optional(), proxy: z .object({ cachePrefix: z.string().optional(), ttl: z.number().optional(), }) .optional(), browser: z .object({ headless: z.boolean().optional(), timeout: z.number().optional(), }) .optional(), }); export type AppConfig = z.infer; /** * Service type definitions for type-safe resolution */ export interface ServiceDefinitions { // Configuration config: AppConfig; logger: Logger; // Core services cache: CacheProvider | null; proxyManager: ProxyManager | null; browser: Browser; queueManager: QueueManager | null; // Database clients mongoClient: MongoDBClient | null; postgresClient: PostgreSQLClient | null; questdbClient: QuestDBClient | null; // Aggregate service container serviceContainer: IServiceContainer; } /** * Create and configure the DI container with type safety */ export function createServiceContainer(rawConfig: unknown): AwilixContainer { // Validate configuration const config = appConfigSchema.parse(rawConfig); const container = createContainer({ injectionMode: InjectionMode.PROXY, }); // Register configuration values const registrations: any = { // Configuration config: asValue(config), redisConfig: asValue(config.redis), mongoConfig: asValue(config.mongodb), postgresConfig: asValue(config.postgres), questdbConfig: asValue( config.questdb || { host: 'localhost', httpPort: 9000, pgPort: 8812, influxPort: 9009 } ), // Core services with dependency injection logger: asFunction(() => getLogger('app')).singleton(), }; // Conditionally register cache/dragonfly if (config.redis?.enabled !== false) { registrations.cache = asFunction(({ redisConfig, logger }) => createCache({ redisConfig, logger, keyPrefix: 'cache:', ttl: 3600, enableMetrics: true, }) ).singleton(); } else { registrations.cache = asValue(null); } // Proxy manager depends on cache registrations.proxyManager = asFunction(({ cache, config, logger }) => { if (!cache) { logger.warn('Cache is disabled, ProxyManager will have limited functionality'); return null; } const manager = new ProxyManager(cache, config.proxy || {}, logger); return manager; }).singleton(); // Conditionally register MongoDB client if (config.mongodb?.enabled !== false) { registrations.mongoClient = asFunction(({ mongoConfig, logger }) => { return new MongoDBClient(mongoConfig, logger); }).singleton(); } else { registrations.mongoClient = asValue(null); } // Conditionally register PostgreSQL client if (config.postgres?.enabled !== false) { registrations.postgresClient = asFunction(({ postgresConfig, logger }) => { return new PostgreSQLClient( { host: postgresConfig.host, port: postgresConfig.port, database: postgresConfig.database, username: postgresConfig.user, password: postgresConfig.password, }, logger ); }).singleton(); } else { registrations.postgresClient = asValue(null); } // Conditionally register QuestDB client if (config.questdb?.enabled !== false) { registrations.questdbClient = asFunction(({ questdbConfig, logger }) => { console.log('Creating QuestDB client with config:', questdbConfig); return new QuestDBClient( { host: questdbConfig.host, httpPort: questdbConfig.httpPort, pgPort: questdbConfig.pgPort, influxPort: questdbConfig.influxPort, database: questdbConfig.database, // QuestDB appears to require default credentials user: 'admin', password: 'quest', }, logger ); }).singleton(); } else { registrations.questdbClient = asValue(null); } // Queue manager - placeholder until decoupled from singleton registrations.queueManager = asFunction(({ redisConfig, cache, logger }) => { // Import dynamically to avoid circular dependency const { QueueManager } = require('@stock-bot/queue'); // Check if already initialized (singleton pattern) if (QueueManager.isInitialized()) { return QueueManager.getInstance(); } // Initialize if not already done return QueueManager.initialize({ redis: { host: redisConfig.host, port: redisConfig.port, db: redisConfig.db }, enableScheduledJobs: true, delayWorkerStart: true, // We'll start workers manually }); }).singleton(); // Browser automation registrations.browser = asFunction(({ config, logger }) => { return new Browser(logger, config.browser); }).singleton(); // Build the IServiceContainer for handlers registrations.serviceContainer = asFunction( cradle => ({ logger: cradle.logger, cache: cradle.cache, proxy: cradle.proxyManager, browser: cradle.browser, mongodb: cradle.mongoClient, postgres: cradle.postgresClient, questdb: cradle.questdbClient, queue: cradle.queueManager, }) as IServiceContainer ).singleton(); container.register(registrations); return container; } /** * Initialize async services after container creation */ export async function initializeServices(container: AwilixContainer): Promise { const logger = container.resolve('logger'); const config = container.resolve('config'); try { // Wait for cache to be ready first (if enabled) const cache = container.resolve('cache'); if (cache && typeof cache.waitForReady === 'function') { await cache.waitForReady(10000); logger.info('Cache is ready'); } else if (config.redis?.enabled === false) { logger.info('Cache is disabled'); } // Initialize proxy manager (depends on cache) const proxyManager = container.resolve('proxyManager'); if (proxyManager && typeof proxyManager.initialize === 'function') { await proxyManager.initialize(); logger.info('Proxy manager initialized'); } else { logger.info('Proxy manager is disabled (requires cache)'); } // Connect MongoDB client (if enabled) const mongoClient = container.resolve('mongoClient'); if (mongoClient && typeof mongoClient.connect === 'function') { await mongoClient.connect(); logger.info('MongoDB connected'); } else if (config.mongodb?.enabled === false) { logger.info('MongoDB is disabled'); } // Connect PostgreSQL client (if enabled) const postgresClient = container.resolve('postgresClient'); if (postgresClient && typeof postgresClient.connect === 'function') { await postgresClient.connect(); logger.info('PostgreSQL connected'); } else if (config.postgres?.enabled === false) { logger.info('PostgreSQL is disabled'); } // Connect QuestDB client (if enabled) const questdbClient = container.resolve('questdbClient'); if (questdbClient && typeof questdbClient.connect === 'function') { await questdbClient.connect(); logger.info('QuestDB connected'); } else if (config.questdb?.enabled === false) { logger.info('QuestDB is disabled'); } // Initialize browser if configured const browser = container.resolve('browser'); if (browser && typeof browser.initialize === 'function') { await browser.initialize(); logger.info('Browser initialized'); } logger.info('All services initialized successfully'); } catch (error) { logger.error('Failed to initialize services', { error }); throw error; } } // Export typed container export type ServiceContainer = AwilixContainer; export type ServiceCradle = ServiceDefinitions;