refactored di into more composable parts
This commit is contained in:
parent
177fe30586
commit
26ebc77fe6
22 changed files with 908 additions and 281 deletions
Binary file not shown.
|
|
@ -6,8 +6,6 @@
|
|||
import { initializeServiceConfig } from '@stock-bot/config';
|
||||
import {
|
||||
ServiceApplication,
|
||||
createServiceContainerFromConfig,
|
||||
initializeServices as initializeAwilixServices,
|
||||
} from '@stock-bot/di';
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
|
||||
|
|
@ -52,19 +50,25 @@ const app = new ServiceApplication(
|
|||
|
||||
// Container factory function
|
||||
async function createContainer(config: any) {
|
||||
const container = createServiceContainerFromConfig(config, {
|
||||
enableQuestDB: false, // Data ingestion doesn't need QuestDB yet
|
||||
enableMongoDB: true,
|
||||
enablePostgres: true,
|
||||
enableCache: true,
|
||||
enableQueue: true,
|
||||
enableBrowser: true, // Data ingestion needs browser for web scraping
|
||||
enableProxy: true, // Data ingestion needs proxy for rate limiting
|
||||
});
|
||||
await initializeAwilixServices(container);
|
||||
const { ServiceContainerBuilder } = await import('@stock-bot/di');
|
||||
|
||||
const container = await new ServiceContainerBuilder()
|
||||
.withConfig(config)
|
||||
.withOptions({
|
||||
enableQuestDB: false, // Data ingestion doesn't need QuestDB yet
|
||||
enableMongoDB: true,
|
||||
enablePostgres: true,
|
||||
enableCache: true,
|
||||
enableQueue: true,
|
||||
enableBrowser: true, // Data ingestion needs browser for web scraping
|
||||
enableProxy: true, // Data ingestion needs proxy for rate limiting
|
||||
})
|
||||
.build(); // This automatically initializes services
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
|
||||
// Start the service
|
||||
app.start(createContainer, createRoutes, initializeAllHandlers).catch(error => {
|
||||
const logger = getLogger('data-ingestion');
|
||||
|
|
|
|||
|
|
@ -4,16 +4,16 @@
|
|||
*/
|
||||
|
||||
import { Browser } from '@stock-bot/browser';
|
||||
import { createCache, type CacheProvider } from '@stock-bot/cache';
|
||||
import { type CacheProvider } from '@stock-bot/cache';
|
||||
import type { AppConfig as StockBotAppConfig } from '@stock-bot/config';
|
||||
import type { IServiceContainer } from '@stock-bot/handlers';
|
||||
import { getLogger, type Logger } from '@stock-bot/logger';
|
||||
import { 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';
|
||||
import { asFunction, asValue, createContainer, InjectionMode, type AwilixContainer } from 'awilix';
|
||||
import { type AwilixContainer } from 'awilix';
|
||||
import { z } from 'zod';
|
||||
|
||||
// Configuration schema with validation
|
||||
|
|
@ -97,217 +97,22 @@ export interface ServiceDefinitions {
|
|||
* Create and configure the DI container with type safety
|
||||
*/
|
||||
export function createServiceContainer(rawConfig: unknown): AwilixContainer<ServiceDefinitions> {
|
||||
// Validate configuration
|
||||
const config = appConfigSchema.parse(rawConfig);
|
||||
|
||||
const container = createContainer<ServiceDefinitions>({
|
||||
injectionMode: InjectionMode.PROXY,
|
||||
});
|
||||
|
||||
// Register configuration values
|
||||
const registrations: Record<string, unknown> = {
|
||||
// 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 instances
|
||||
if (config.redis?.enabled !== false) {
|
||||
// Main cache instance
|
||||
registrations.cache = asFunction(({ redisConfig, logger }) =>
|
||||
createCache({
|
||||
redisConfig,
|
||||
logger,
|
||||
keyPrefix: '', // No prefix at this level, namespaces will handle it
|
||||
ttl: 3600,
|
||||
enableMetrics: true,
|
||||
})
|
||||
).singleton();
|
||||
} else {
|
||||
registrations.cache = asValue(null);
|
||||
}
|
||||
|
||||
// Proxy manager creates its own namespaced cache
|
||||
registrations.proxyManager = asFunction(({ cache, config, logger }) => {
|
||||
if (!cache) {
|
||||
logger.warn('Cache is disabled, ProxyManager will have limited functionality');
|
||||
return null;
|
||||
}
|
||||
const { NamespacedCache } = require('@stock-bot/cache');
|
||||
const proxyCache = new NamespacedCache(cache, 'proxy');
|
||||
const manager = new ProxyManager(proxyCache, 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 }) => {
|
||||
logger.debug('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 - conditionally registered with logger injection
|
||||
if (config.redis?.enabled !== false && config.queue?.enabled !== false) {
|
||||
registrations.queueManager = asFunction(({ redisConfig, logger }) => {
|
||||
const { QueueManager } = require('@stock-bot/queue');
|
||||
|
||||
return new QueueManager({
|
||||
redis: {
|
||||
host: redisConfig.host,
|
||||
port: redisConfig.port,
|
||||
db: redisConfig.db,
|
||||
password: redisConfig.password,
|
||||
username: redisConfig.username,
|
||||
},
|
||||
enableScheduledJobs: true,
|
||||
delayWorkerStart: true, // We'll start workers manually
|
||||
}, logger); // Pass logger to QueueManager
|
||||
}).singleton();
|
||||
} else {
|
||||
registrations.queueManager = asValue(null);
|
||||
}
|
||||
|
||||
// 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;
|
||||
// Deprecated - use the new modular structure
|
||||
const { createServiceContainer: newCreateServiceContainer } = require('./container/factory');
|
||||
return newCreateServiceContainer(rawConfig);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initialize async services after container creation
|
||||
*/
|
||||
export async function initializeServices(container: AwilixContainer): Promise<void> {
|
||||
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;
|
||||
}
|
||||
// Deprecated - use the new modular structure
|
||||
const { initializeServices: newInitializeServices } = await import('./container/factory');
|
||||
return newInitializeServices(container as any);
|
||||
}
|
||||
|
||||
|
||||
// Export typed container
|
||||
export type ServiceContainer = AwilixContainer<ServiceDefinitions>;
|
||||
export type ServiceCradle = ServiceDefinitions;
|
||||
|
|
@ -333,66 +138,8 @@ export function createServiceContainerFromConfig(
|
|||
appConfig: StockBotAppConfig,
|
||||
options: ServiceContainerOptions = {}
|
||||
): AwilixContainer<ServiceDefinitions> {
|
||||
// Apply defaults for options
|
||||
const {
|
||||
enableQuestDB = true,
|
||||
enableMongoDB = true,
|
||||
enablePostgres = true,
|
||||
enableCache = true,
|
||||
enableQueue = true,
|
||||
enableBrowser = true,
|
||||
enableProxy = true,
|
||||
} = options;
|
||||
|
||||
// Build the config object expected by createServiceContainer
|
||||
const containerConfig = {
|
||||
redis: {
|
||||
enabled: enableCache && appConfig.database?.dragonfly ? true : false,
|
||||
host: appConfig.database?.dragonfly?.host || 'localhost',
|
||||
port: appConfig.database?.dragonfly?.port || 6379,
|
||||
password: appConfig.database?.dragonfly?.password,
|
||||
db: appConfig.database?.dragonfly?.db || 0,
|
||||
},
|
||||
mongodb: {
|
||||
enabled: enableMongoDB && appConfig.database?.mongodb ? true : false,
|
||||
uri: appConfig.database?.mongodb?.uri ||
|
||||
`mongodb://${appConfig.database?.mongodb?.user || ''}:${appConfig.database?.mongodb?.password || ''}@${appConfig.database?.mongodb?.host || 'localhost'}:${appConfig.database?.mongodb?.port || 27017}/${appConfig.database?.mongodb?.database || 'test'}?authSource=${appConfig.database?.mongodb?.authSource || 'admin'}`,
|
||||
database: appConfig.database?.mongodb?.database || 'test',
|
||||
},
|
||||
postgres: {
|
||||
enabled: enablePostgres && appConfig.database?.postgres ? true : false,
|
||||
host: appConfig.database?.postgres?.host || 'localhost',
|
||||
port: appConfig.database?.postgres?.port || 5432,
|
||||
database: appConfig.database?.postgres?.database || 'test',
|
||||
user: appConfig.database?.postgres?.user || 'test',
|
||||
password: appConfig.database?.postgres?.password || 'test',
|
||||
},
|
||||
questdb: enableQuestDB && appConfig.database?.questdb ? {
|
||||
enabled: true,
|
||||
host: appConfig.database.questdb.host || 'localhost',
|
||||
httpPort: appConfig.database.questdb.httpPort || 9000,
|
||||
pgPort: appConfig.database.questdb.pgPort || 8812,
|
||||
influxPort: appConfig.database.questdb.ilpPort || 9009,
|
||||
database: appConfig.database.questdb.database || 'questdb',
|
||||
} : {
|
||||
enabled: false,
|
||||
host: 'localhost',
|
||||
httpPort: 9000,
|
||||
pgPort: 8812,
|
||||
influxPort: 9009,
|
||||
},
|
||||
proxy: enableProxy ? {
|
||||
cachePrefix: 'proxy:',
|
||||
ttl: 3600,
|
||||
} : undefined,
|
||||
browser: enableBrowser ? {
|
||||
headless: true,
|
||||
timeout: 30000,
|
||||
} : undefined,
|
||||
queue: {
|
||||
enabled: enableQueue && enableCache, // Queue depends on Redis/cache
|
||||
},
|
||||
};
|
||||
|
||||
return createServiceContainer(containerConfig);
|
||||
// Deprecated - use the new modular structure
|
||||
const { createServiceContainerFromConfig: newCreateServiceContainerFromConfig } = require('./container/factory');
|
||||
return newCreateServiceContainerFromConfig(appConfig, options);
|
||||
}
|
||||
|
||||
|
|
|
|||
25
libs/core/di/src/config/schemas/index.ts
Normal file
25
libs/core/di/src/config/schemas/index.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { z } from 'zod';
|
||||
import { redisConfigSchema } from './redis.schema';
|
||||
import { mongodbConfigSchema } from './mongodb.schema';
|
||||
import { postgresConfigSchema } from './postgres.schema';
|
||||
import { questdbConfigSchema } from './questdb.schema';
|
||||
import { proxyConfigSchema, browserConfigSchema, queueConfigSchema } from './service.schema';
|
||||
|
||||
export const appConfigSchema = z.object({
|
||||
redis: redisConfigSchema,
|
||||
mongodb: mongodbConfigSchema,
|
||||
postgres: postgresConfigSchema,
|
||||
questdb: questdbConfigSchema.optional(),
|
||||
proxy: proxyConfigSchema.optional(),
|
||||
browser: browserConfigSchema.optional(),
|
||||
queue: queueConfigSchema.optional(),
|
||||
});
|
||||
|
||||
export type AppConfig = z.infer<typeof appConfigSchema>;
|
||||
|
||||
// Re-export individual schemas and types
|
||||
export * from './redis.schema';
|
||||
export * from './mongodb.schema';
|
||||
export * from './postgres.schema';
|
||||
export * from './questdb.schema';
|
||||
export * from './service.schema';
|
||||
9
libs/core/di/src/config/schemas/mongodb.schema.ts
Normal file
9
libs/core/di/src/config/schemas/mongodb.schema.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const mongodbConfigSchema = z.object({
|
||||
enabled: z.boolean().optional().default(true),
|
||||
uri: z.string(),
|
||||
database: z.string(),
|
||||
});
|
||||
|
||||
export type MongoDBConfig = z.infer<typeof mongodbConfigSchema>;
|
||||
12
libs/core/di/src/config/schemas/postgres.schema.ts
Normal file
12
libs/core/di/src/config/schemas/postgres.schema.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const postgresConfigSchema = z.object({
|
||||
enabled: z.boolean().optional().default(true),
|
||||
host: z.string().default('localhost'),
|
||||
port: z.number().default(5432),
|
||||
database: z.string(),
|
||||
user: z.string(),
|
||||
password: z.string(),
|
||||
});
|
||||
|
||||
export type PostgresConfig = z.infer<typeof postgresConfigSchema>;
|
||||
12
libs/core/di/src/config/schemas/questdb.schema.ts
Normal file
12
libs/core/di/src/config/schemas/questdb.schema.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const questdbConfigSchema = z.object({
|
||||
enabled: z.boolean().optional().default(true),
|
||||
host: z.string().default('localhost'),
|
||||
httpPort: z.number().optional().default(9000),
|
||||
pgPort: z.number().optional().default(8812),
|
||||
influxPort: z.number().optional().default(9009),
|
||||
database: z.string().optional().default('questdb'),
|
||||
});
|
||||
|
||||
export type QuestDBConfig = z.infer<typeof questdbConfigSchema>;
|
||||
12
libs/core/di/src/config/schemas/redis.schema.ts
Normal file
12
libs/core/di/src/config/schemas/redis.schema.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const redisConfigSchema = z.object({
|
||||
enabled: z.boolean().optional().default(true),
|
||||
host: z.string().default('localhost'),
|
||||
port: z.number().default(6379),
|
||||
password: z.string().optional(),
|
||||
username: z.string().optional(),
|
||||
db: z.number().optional().default(0),
|
||||
});
|
||||
|
||||
export type RedisConfig = z.infer<typeof redisConfigSchema>;
|
||||
19
libs/core/di/src/config/schemas/service.schema.ts
Normal file
19
libs/core/di/src/config/schemas/service.schema.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const proxyConfigSchema = z.object({
|
||||
cachePrefix: z.string().optional().default('proxy:'),
|
||||
ttl: z.number().optional().default(3600),
|
||||
});
|
||||
|
||||
export const browserConfigSchema = z.object({
|
||||
headless: z.boolean().optional().default(true),
|
||||
timeout: z.number().optional().default(30000),
|
||||
});
|
||||
|
||||
export const queueConfigSchema = z.object({
|
||||
enabled: z.boolean().optional().default(true),
|
||||
});
|
||||
|
||||
export type ProxyConfig = z.infer<typeof proxyConfigSchema>;
|
||||
export type BrowserConfig = z.infer<typeof browserConfigSchema>;
|
||||
export type QueueConfig = z.infer<typeof queueConfigSchema>;
|
||||
106
libs/core/di/src/container/README.md
Normal file
106
libs/core/di/src/container/README.md
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
# DI Container - Modular Structure
|
||||
|
||||
## Overview
|
||||
|
||||
The DI container has been refactored into a modular structure for better organization and maintainability.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
├── container/ # Core container logic
|
||||
│ ├── builder.ts # Fluent API for building containers
|
||||
│ ├── factory.ts # Factory functions (legacy compatibility)
|
||||
│ └── types.ts # Type definitions
|
||||
├── registrations/ # Service registration modules
|
||||
│ ├── core.ts # Core services (config, logger)
|
||||
│ ├── cache.ts # Cache services
|
||||
│ ├── database.ts # Database clients
|
||||
│ └── service.ts # Application services
|
||||
├── config/ # Configuration management
|
||||
│ └── schemas/ # Zod schemas for validation
|
||||
├── factories/ # Service factories
|
||||
│ └── cache.factory.ts # Cache factory utilities
|
||||
└── utils/ # Utilities
|
||||
└── lifecycle.ts # Service lifecycle management
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Using the Builder Pattern (Recommended)
|
||||
|
||||
```typescript
|
||||
import { ServiceContainerBuilder } from '@stock-bot/di';
|
||||
|
||||
// Create container with fluent API
|
||||
const container = await new ServiceContainerBuilder()
|
||||
.withConfig({
|
||||
redis: { host: 'localhost', port: 6379 },
|
||||
mongodb: { uri: 'mongodb://localhost', database: 'mydb' },
|
||||
postgres: { host: 'localhost', database: 'mydb', user: 'user', password: 'pass' }
|
||||
})
|
||||
.enableService('enableQueue', false) // Disable queue service
|
||||
.enableService('enableBrowser', false) // Disable browser service
|
||||
.build();
|
||||
|
||||
// Services are automatically initialized
|
||||
const cache = container.cradle.cache;
|
||||
const mongoClient = container.cradle.mongoClient;
|
||||
```
|
||||
|
||||
### Creating Namespaced Caches
|
||||
|
||||
```typescript
|
||||
import { CacheFactory } from '@stock-bot/di';
|
||||
|
||||
// Create a cache for a specific service
|
||||
const serviceCache = CacheFactory.createCacheForService(container, 'myservice');
|
||||
|
||||
// Create a cache for a handler
|
||||
const handlerCache = CacheFactory.createCacheForHandler(container, 'myhandler');
|
||||
|
||||
// Create a cache with custom prefix
|
||||
const customCache = CacheFactory.createCacheWithPrefix(container, 'custom');
|
||||
```
|
||||
|
||||
### Manual Service Lifecycle
|
||||
|
||||
```typescript
|
||||
import { ServiceContainerBuilder, ServiceLifecycleManager } from '@stock-bot/di';
|
||||
|
||||
// Create container without auto-initialization
|
||||
const container = await new ServiceContainerBuilder()
|
||||
.withConfig(config)
|
||||
.skipInitialization()
|
||||
.build();
|
||||
|
||||
// Manually initialize services
|
||||
const lifecycle = new ServiceLifecycleManager();
|
||||
await lifecycle.initializeServices(container);
|
||||
|
||||
// ... use services ...
|
||||
|
||||
// Manually shutdown services
|
||||
await lifecycle.shutdownServices(container);
|
||||
```
|
||||
|
||||
### Legacy API (Backward Compatible)
|
||||
|
||||
```typescript
|
||||
import { createServiceContainerFromConfig } from '@stock-bot/di';
|
||||
|
||||
// Old way still works
|
||||
const container = createServiceContainerFromConfig(appConfig, {
|
||||
enableQueue: true,
|
||||
enableCache: true
|
||||
});
|
||||
|
||||
// Manual initialization required with legacy API
|
||||
await initializeServices(container);
|
||||
```
|
||||
|
||||
## Migration Guide
|
||||
|
||||
1. Replace direct container creation with `ServiceContainerBuilder`
|
||||
2. Use `CacheFactory` instead of manually creating `NamespacedCache`
|
||||
3. Let the builder handle service initialization automatically
|
||||
4. Use typed configuration schemas for better validation
|
||||
171
libs/core/di/src/container/builder.ts
Normal file
171
libs/core/di/src/container/builder.ts
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
import { createContainer, InjectionMode, type AwilixContainer } from 'awilix';
|
||||
import type { AppConfig as StockBotAppConfig } from '@stock-bot/config';
|
||||
import { appConfigSchema, type AppConfig } from '../config/schemas';
|
||||
import {
|
||||
registerCoreServices,
|
||||
registerCacheServices,
|
||||
registerDatabaseServices,
|
||||
registerApplicationServices
|
||||
} from '../registrations';
|
||||
import { ServiceLifecycleManager } from '../utils/lifecycle';
|
||||
import type { ServiceDefinitions, ContainerBuildOptions } from './types';
|
||||
|
||||
export class ServiceContainerBuilder {
|
||||
private config: Partial<AppConfig> = {};
|
||||
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): this {
|
||||
this.config = this.transformStockBotConfig(config);
|
||||
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 {
|
||||
return {
|
||||
redis: {
|
||||
enabled: this.options.enableCache ?? true,
|
||||
host: config.redis?.host || 'localhost',
|
||||
port: config.redis?.port || 6379,
|
||||
password: config.redis?.password,
|
||||
username: config.redis?.username,
|
||||
db: config.redis?.db || 0,
|
||||
},
|
||||
mongodb: {
|
||||
enabled: this.options.enableMongoDB ?? true,
|
||||
uri: config.mongodb?.uri || '',
|
||||
database: config.mongodb?.database || '',
|
||||
},
|
||||
postgres: {
|
||||
enabled: this.options.enablePostgres ?? true,
|
||||
host: config.postgres?.host || 'localhost',
|
||||
port: config.postgres?.port || 5432,
|
||||
database: config.postgres?.database || '',
|
||||
user: config.postgres?.user || '',
|
||||
password: config.postgres?.password || '',
|
||||
},
|
||||
questdb: this.options.enableQuestDB ? config.questdb : undefined,
|
||||
proxy: this.options.enableProxy ? (config.proxy || { cachePrefix: 'proxy:', ttl: 3600 }) : undefined,
|
||||
browser: this.options.enableBrowser ? config.browser : undefined,
|
||||
queue: this.options.enableQueue ? { enabled: this.options.enableQueue } : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
private registerServices(container: AwilixContainer<ServiceDefinitions>, config: AppConfig): void {
|
||||
registerCoreServices(container, config);
|
||||
registerCacheServices(container, config);
|
||||
registerDatabaseServices(container, config);
|
||||
registerApplicationServices(container, config);
|
||||
|
||||
// Register service container aggregate
|
||||
container.register({
|
||||
serviceContainer: asFunction(({
|
||||
config, logger, cache, proxyManager, browser,
|
||||
queueManager, mongoClient, postgresClient, questdbClient
|
||||
}) => ({
|
||||
logger,
|
||||
cache,
|
||||
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: AppConfig | StockBotAppConfig): Partial<AppConfig> {
|
||||
// If it's already in the new format, return as is
|
||||
if ('redis' in config) {
|
||||
return config as AppConfig;
|
||||
}
|
||||
|
||||
// Transform from StockBotAppConfig format
|
||||
const stockBotConfig = config as StockBotAppConfig;
|
||||
return {
|
||||
redis: stockBotConfig.database?.dragonfly ? {
|
||||
enabled: true,
|
||||
host: stockBotConfig.database.dragonfly.host || 'localhost',
|
||||
port: stockBotConfig.database.dragonfly.port || 6379,
|
||||
password: stockBotConfig.database.dragonfly.password,
|
||||
db: stockBotConfig.database.dragonfly.db || 0,
|
||||
} : undefined,
|
||||
mongodb: stockBotConfig.database?.mongodb ? {
|
||||
enabled: true,
|
||||
uri: stockBotConfig.database.mongodb.uri ||
|
||||
`mongodb://${stockBotConfig.database.mongodb.user || ''}:${stockBotConfig.database.mongodb.password || ''}@${stockBotConfig.database.mongodb.host || 'localhost'}:${stockBotConfig.database.mongodb.port || 27017}/${stockBotConfig.database.mongodb.database || 'test'}?authSource=${stockBotConfig.database.mongodb.authSource || 'admin'}`,
|
||||
database: stockBotConfig.database.mongodb.database || 'test',
|
||||
} : undefined,
|
||||
postgres: stockBotConfig.database?.postgres ? {
|
||||
enabled: true,
|
||||
host: stockBotConfig.database.postgres.host || 'localhost',
|
||||
port: stockBotConfig.database.postgres.port || 5432,
|
||||
database: stockBotConfig.database.postgres.database || 'test',
|
||||
user: stockBotConfig.database.postgres.user || 'test',
|
||||
password: stockBotConfig.database.postgres.password || 'test',
|
||||
} : undefined,
|
||||
questdb: stockBotConfig.database?.questdb ? {
|
||||
enabled: true,
|
||||
host: stockBotConfig.database.questdb.host || 'localhost',
|
||||
httpPort: stockBotConfig.database.questdb.httpPort || 9000,
|
||||
pgPort: stockBotConfig.database.questdb.pgPort || 8812,
|
||||
influxPort: stockBotConfig.database.questdb.ilpPort || 9009,
|
||||
database: stockBotConfig.database.questdb.database || 'questdb',
|
||||
} : undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Add missing import
|
||||
import { asFunction } from 'awilix';
|
||||
99
libs/core/di/src/container/factory.ts
Normal file
99
libs/core/di/src/container/factory.ts
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import type { AwilixContainer } from 'awilix';
|
||||
import type { AppConfig as StockBotAppConfig } from '@stock-bot/config';
|
||||
import { ServiceContainerBuilder } from './builder';
|
||||
import type { ServiceDefinitions, ServiceContainerOptions } from './types';
|
||||
|
||||
/**
|
||||
* Creates a service container from raw configuration
|
||||
* @deprecated Use ServiceContainerBuilder instead
|
||||
*/
|
||||
export function createServiceContainer(rawConfig: unknown): AwilixContainer<ServiceDefinitions> {
|
||||
// For backward compatibility, we need to create the container synchronously
|
||||
// This means we'll use the original implementation pattern
|
||||
const { createContainer, InjectionMode, asValue, asFunction, asClass } = require('awilix');
|
||||
const { appConfigSchema } = require('../config/schemas');
|
||||
const config = appConfigSchema.parse(rawConfig);
|
||||
|
||||
const container = createContainer({
|
||||
injectionMode: InjectionMode.PROXY,
|
||||
strict: true,
|
||||
});
|
||||
|
||||
// Register all services synchronously
|
||||
const {
|
||||
registerCoreServices,
|
||||
registerCacheServices,
|
||||
registerDatabaseServices,
|
||||
registerApplicationServices
|
||||
} = require('../registrations');
|
||||
|
||||
registerCoreServices(container, config);
|
||||
registerCacheServices(container, config);
|
||||
registerDatabaseServices(container, config);
|
||||
registerApplicationServices(container, config);
|
||||
|
||||
// Register service container aggregate
|
||||
container.register({
|
||||
serviceContainer: asFunction((cradle: ServiceDefinitions) => ({
|
||||
logger: cradle.logger,
|
||||
cache: cradle.cache,
|
||||
proxy: cradle.proxyManager, // Map proxyManager to proxy
|
||||
browser: cradle.browser,
|
||||
queue: cradle.queueManager, // Map queueManager to queue
|
||||
mongodb: cradle.mongoClient, // Map mongoClient to mongodb
|
||||
postgres: cradle.postgresClient, // Map postgresClient to postgres
|
||||
questdb: cradle.questdbClient, // Map questdbClient to questdb
|
||||
})).singleton(),
|
||||
});
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a service container from StockBotAppConfig
|
||||
* @deprecated Use ServiceContainerBuilder instead
|
||||
*/
|
||||
export function createServiceContainerFromConfig(
|
||||
appConfig: StockBotAppConfig,
|
||||
options: ServiceContainerOptions = {}
|
||||
): AwilixContainer<ServiceDefinitions> {
|
||||
const builder = new ServiceContainerBuilder();
|
||||
return builder
|
||||
.withConfig(appConfig)
|
||||
.withOptions({
|
||||
...options,
|
||||
skipInitialization: true, // Legacy behavior
|
||||
})
|
||||
.build()
|
||||
.then(container => container)
|
||||
.catch(error => {
|
||||
throw error;
|
||||
}) as any; // Sync interface for backward compatibility
|
||||
}
|
||||
|
||||
/**
|
||||
* Modern async factory for creating service containers
|
||||
*/
|
||||
export async function createServiceContainerAsync(
|
||||
config: StockBotAppConfig,
|
||||
options: ServiceContainerOptions = {}
|
||||
): Promise<AwilixContainer<ServiceDefinitions>> {
|
||||
const builder = new ServiceContainerBuilder();
|
||||
return builder
|
||||
.withConfig(config)
|
||||
.withOptions(options)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize services in an existing container
|
||||
* @deprecated Handled automatically by ServiceContainerBuilder
|
||||
*/
|
||||
export async function initializeServices(
|
||||
container: AwilixContainer<ServiceDefinitions>
|
||||
): Promise<void> {
|
||||
const { ServiceLifecycleManager } = await import('../utils/lifecycle');
|
||||
const lifecycleManager = new ServiceLifecycleManager();
|
||||
await lifecycleManager.initializeServices(container);
|
||||
}
|
||||
47
libs/core/di/src/container/types.ts
Normal file
47
libs/core/di/src/container/types.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import type { IServiceContainer } from '@stock-bot/handlers';
|
||||
import type { Logger } from '@stock-bot/logger';
|
||||
import type { AppConfig } from '../config/schemas';
|
||||
import type { CacheProvider } from '@stock-bot/cache';
|
||||
import type { ProxyManager } from '@stock-bot/proxy';
|
||||
import type { Browser } from '@stock-bot/browser';
|
||||
import type { QueueManager } from '@stock-bot/queue';
|
||||
import type { MongoDBClient } from '@stock-bot/mongodb';
|
||||
import type { PostgreSQLClient } from '@stock-bot/postgres';
|
||||
import type { QuestDBClient } from '@stock-bot/questdb';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export type ServiceCradle = ServiceDefinitions;
|
||||
|
||||
export interface ServiceContainerOptions {
|
||||
enableQuestDB?: boolean;
|
||||
enableMongoDB?: boolean;
|
||||
enablePostgres?: boolean;
|
||||
enableCache?: boolean;
|
||||
enableQueue?: boolean;
|
||||
enableBrowser?: boolean;
|
||||
enableProxy?: boolean;
|
||||
}
|
||||
|
||||
export interface ContainerBuildOptions extends ServiceContainerOptions {
|
||||
skipInitialization?: boolean;
|
||||
initializationTimeout?: number;
|
||||
}
|
||||
44
libs/core/di/src/factories/cache.factory.ts
Normal file
44
libs/core/di/src/factories/cache.factory.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import type { AwilixContainer } from 'awilix';
|
||||
import { NamespacedCache, type CacheProvider } from '@stock-bot/cache';
|
||||
import type { ServiceDefinitions } from '../container/types';
|
||||
|
||||
export class CacheFactory {
|
||||
static createNamespacedCache(
|
||||
baseCache: CacheProvider,
|
||||
namespace: string
|
||||
): NamespacedCache {
|
||||
return new NamespacedCache(baseCache, namespace);
|
||||
}
|
||||
|
||||
static createCacheForService(
|
||||
container: AwilixContainer<ServiceDefinitions>,
|
||||
serviceName: string
|
||||
): CacheProvider | null {
|
||||
const baseCache = container.cradle.cache;
|
||||
if (!baseCache) return null;
|
||||
|
||||
return this.createNamespacedCache(baseCache, serviceName);
|
||||
}
|
||||
|
||||
static createCacheForHandler(
|
||||
container: AwilixContainer<ServiceDefinitions>,
|
||||
handlerName: string
|
||||
): CacheProvider | null {
|
||||
const baseCache = container.cradle.cache;
|
||||
if (!baseCache) return null;
|
||||
|
||||
return this.createNamespacedCache(baseCache, `handler:${handlerName}`);
|
||||
}
|
||||
|
||||
static createCacheWithPrefix(
|
||||
container: AwilixContainer<ServiceDefinitions>,
|
||||
prefix: string
|
||||
): CacheProvider | null {
|
||||
const baseCache = container.cradle.cache;
|
||||
if (!baseCache) return null;
|
||||
|
||||
// Remove 'cache:' prefix if already included
|
||||
const cleanPrefix = prefix.replace(/^cache:/, '');
|
||||
return this.createNamespacedCache(baseCache, cleanPrefix);
|
||||
}
|
||||
}
|
||||
1
libs/core/di/src/factories/index.ts
Normal file
1
libs/core/di/src/factories/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { CacheFactory } from './cache.factory';
|
||||
|
|
@ -3,7 +3,7 @@ export * from './operation-context';
|
|||
export * from './pool-size-calculator';
|
||||
export * from './types';
|
||||
|
||||
// Awilix container exports
|
||||
// Legacy exports for backward compatibility
|
||||
export {
|
||||
createServiceContainer,
|
||||
createServiceContainerFromConfig,
|
||||
|
|
@ -14,6 +14,24 @@ export {
|
|||
type ServiceContainerOptions,
|
||||
} from './awilix-container';
|
||||
|
||||
// New modular structure exports
|
||||
export * from './container/types';
|
||||
export { ServiceContainerBuilder } from './container/builder';
|
||||
export {
|
||||
createServiceContainerAsync,
|
||||
createServiceContainer as createServiceContainerNew,
|
||||
createServiceContainerFromConfig as createServiceContainerFromConfigNew
|
||||
} from './container/factory';
|
||||
|
||||
// Configuration exports
|
||||
export * from './config/schemas';
|
||||
|
||||
// Factory exports
|
||||
export * from './factories';
|
||||
|
||||
// Utility exports
|
||||
export { ServiceLifecycleManager } from './utils/lifecycle';
|
||||
|
||||
// Service application framework
|
||||
export {
|
||||
ServiceApplication,
|
||||
|
|
|
|||
27
libs/core/di/src/registrations/cache.registration.ts
Normal file
27
libs/core/di/src/registrations/cache.registration.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { asClass, asFunction, asValue, type AwilixContainer } from 'awilix';
|
||||
import { createCache, type CacheProvider } from '@stock-bot/cache';
|
||||
import type { AppConfig } from '../config/schemas';
|
||||
import type { ServiceDefinitions } from '../container/types';
|
||||
|
||||
export function registerCacheServices(
|
||||
container: AwilixContainer<ServiceDefinitions>,
|
||||
config: AppConfig
|
||||
): void {
|
||||
if (config.redis.enabled) {
|
||||
container.register({
|
||||
cache: asFunction(() => {
|
||||
return createCache({
|
||||
redisConfig: {
|
||||
host: config.redis.host,
|
||||
port: config.redis.port,
|
||||
password: config.redis.password,
|
||||
},
|
||||
});
|
||||
}).singleton(),
|
||||
});
|
||||
} else {
|
||||
container.register({
|
||||
cache: asValue(null),
|
||||
});
|
||||
}
|
||||
}
|
||||
14
libs/core/di/src/registrations/core.registration.ts
Normal file
14
libs/core/di/src/registrations/core.registration.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { asValue, type AwilixContainer } from 'awilix';
|
||||
import { getLogger, type Logger } from '@stock-bot/logger';
|
||||
import type { AppConfig } from '../config/schemas';
|
||||
import type { ServiceDefinitions } from '../container/types';
|
||||
|
||||
export function registerCoreServices(
|
||||
container: AwilixContainer<ServiceDefinitions>,
|
||||
config: AppConfig
|
||||
): void {
|
||||
container.register({
|
||||
config: asValue(config),
|
||||
logger: asValue(getLogger('di-container')),
|
||||
});
|
||||
}
|
||||
79
libs/core/di/src/registrations/database.registration.ts
Normal file
79
libs/core/di/src/registrations/database.registration.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import { MongoDBClient } from '@stock-bot/mongodb';
|
||||
import { PostgreSQLClient } from '@stock-bot/postgres';
|
||||
import { QuestDBClient } from '@stock-bot/questdb';
|
||||
import { asFunction, asValue, type AwilixContainer } from 'awilix';
|
||||
import type { AppConfig } from '../config/schemas';
|
||||
import type { ServiceDefinitions } from '../container/types';
|
||||
|
||||
export function registerDatabaseServices(
|
||||
container: AwilixContainer<ServiceDefinitions>,
|
||||
config: AppConfig
|
||||
): void {
|
||||
// MongoDB
|
||||
if (config.mongodb.enabled) {
|
||||
container.register({
|
||||
mongoClient: asFunction(({ logger }) => {
|
||||
// Parse MongoDB URI to extract components
|
||||
const uriMatch = config.mongodb.uri.match(/mongodb:\/\/(?:([^:]+):([^@]+)@)?([^:\/]+):(\d+)\/([^?]+)(?:\?authSource=(.+))?/);
|
||||
const mongoConfig = {
|
||||
host: uriMatch?.[3] || 'localhost',
|
||||
port: parseInt(uriMatch?.[4] || '27017'),
|
||||
database: config.mongodb.database,
|
||||
username: uriMatch?.[1],
|
||||
password: uriMatch?.[2],
|
||||
authSource: uriMatch?.[6] || 'admin',
|
||||
uri: config.mongodb.uri,
|
||||
};
|
||||
return new MongoDBClient(mongoConfig, logger);
|
||||
}).singleton(),
|
||||
});
|
||||
} else {
|
||||
container.register({
|
||||
mongoClient: asValue(null),
|
||||
});
|
||||
}
|
||||
|
||||
// PostgreSQL
|
||||
if (config.postgres.enabled) {
|
||||
container.register({
|
||||
postgresClient: asFunction(({ logger }) => {
|
||||
return new PostgreSQLClient(
|
||||
{
|
||||
host: config.postgres.host,
|
||||
port: config.postgres.port,
|
||||
database: config.postgres.database,
|
||||
username: config.postgres.user,
|
||||
password: config.postgres.password,
|
||||
},
|
||||
logger
|
||||
);
|
||||
}).singleton(),
|
||||
});
|
||||
} else {
|
||||
container.register({
|
||||
postgresClient: asValue(null),
|
||||
});
|
||||
}
|
||||
|
||||
// QuestDB
|
||||
if (config.questdb?.enabled) {
|
||||
container.register({
|
||||
questdbClient: asFunction(({ logger }) => {
|
||||
return new QuestDBClient(
|
||||
{
|
||||
host: config.questdb!.host,
|
||||
httpPort: config.questdb!.httpPort,
|
||||
pgPort: config.questdb!.pgPort,
|
||||
influxPort: config.questdb!.influxPort,
|
||||
database: config.questdb!.database,
|
||||
},
|
||||
logger
|
||||
);
|
||||
}).singleton(),
|
||||
});
|
||||
} else {
|
||||
container.register({
|
||||
questdbClient: asValue(null),
|
||||
});
|
||||
}
|
||||
}
|
||||
4
libs/core/di/src/registrations/index.ts
Normal file
4
libs/core/di/src/registrations/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export { registerCoreServices } from './core.registration';
|
||||
export { registerCacheServices } from './cache.registration';
|
||||
export { registerDatabaseServices } from './database.registration';
|
||||
export { registerApplicationServices } from './service.registration';
|
||||
81
libs/core/di/src/registrations/service.registration.ts
Normal file
81
libs/core/di/src/registrations/service.registration.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { asClass, asFunction, asValue, type AwilixContainer } from 'awilix';
|
||||
import { Browser } from '@stock-bot/browser';
|
||||
import { ProxyManager } from '@stock-bot/proxy';
|
||||
import { NamespacedCache } from '@stock-bot/cache';
|
||||
import type { QueueManager } from '@stock-bot/queue';
|
||||
import type { AppConfig } from '../config/schemas';
|
||||
import type { ServiceDefinitions } from '../container/types';
|
||||
|
||||
export function registerApplicationServices(
|
||||
container: AwilixContainer<ServiceDefinitions>,
|
||||
config: AppConfig
|
||||
): void {
|
||||
// Browser
|
||||
if (config.browser) {
|
||||
container.register({
|
||||
browser: asClass(Browser)
|
||||
.singleton()
|
||||
.inject(() => ({
|
||||
options: {
|
||||
headless: config.browser!.headless,
|
||||
timeout: config.browser!.timeout,
|
||||
},
|
||||
})),
|
||||
});
|
||||
} else {
|
||||
container.register({
|
||||
browser: asValue(null as any), // Required field
|
||||
});
|
||||
}
|
||||
|
||||
// Proxy Manager
|
||||
if (config.proxy && config.redis.enabled) {
|
||||
container.register({
|
||||
proxyManager: asFunction(({ cache, logger }) => {
|
||||
if (!cache) return null;
|
||||
const proxyCache = new NamespacedCache(cache, 'proxy');
|
||||
return new ProxyManager(proxyCache, logger);
|
||||
}).singleton(),
|
||||
});
|
||||
} else {
|
||||
container.register({
|
||||
proxyManager: asValue(null),
|
||||
});
|
||||
}
|
||||
|
||||
// Queue Manager
|
||||
if (config.queue?.enabled && config.redis.enabled) {
|
||||
container.register({
|
||||
queueManager: asFunction(({ logger }) => {
|
||||
const { QueueManager } = require('@stock-bot/queue');
|
||||
const queueConfig = {
|
||||
redis: {
|
||||
host: config.redis.host,
|
||||
port: config.redis.port,
|
||||
password: config.redis.password,
|
||||
db: config.redis.db,
|
||||
},
|
||||
defaultQueueOptions: {
|
||||
workers: 1,
|
||||
concurrency: 1,
|
||||
defaultJobOptions: {
|
||||
removeOnComplete: 100,
|
||||
removeOnFail: 50,
|
||||
attempts: 3,
|
||||
backoff: {
|
||||
type: 'exponential',
|
||||
delay: 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
enableScheduledJobs: true,
|
||||
};
|
||||
return new QueueManager(queueConfig, logger);
|
||||
}).singleton(),
|
||||
});
|
||||
} else {
|
||||
container.register({
|
||||
queueManager: asValue(null),
|
||||
});
|
||||
}
|
||||
}
|
||||
96
libs/core/di/src/utils/lifecycle.ts
Normal file
96
libs/core/di/src/utils/lifecycle.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import type { AwilixContainer } from 'awilix';
|
||||
import type { ServiceDefinitions } from '../container/types';
|
||||
|
||||
interface ServiceWithLifecycle {
|
||||
connect?: () => Promise<void>;
|
||||
disconnect?: () => Promise<void>;
|
||||
close?: () => Promise<void>;
|
||||
initialize?: () => Promise<void>;
|
||||
shutdown?: () => Promise<void>;
|
||||
}
|
||||
|
||||
export class ServiceLifecycleManager {
|
||||
private readonly services = [
|
||||
{ name: 'cache', key: 'cache' as const },
|
||||
{ name: 'mongoClient', key: 'mongoClient' as const },
|
||||
{ name: 'postgresClient', key: 'postgresClient' as const },
|
||||
{ name: 'questdbClient', key: 'questdbClient' as const },
|
||||
{ name: 'queueManager', key: 'queueManager' as const },
|
||||
];
|
||||
|
||||
async initializeServices(
|
||||
container: AwilixContainer<ServiceDefinitions>,
|
||||
timeout = 30000
|
||||
): Promise<void> {
|
||||
const initPromises: Promise<void>[] = [];
|
||||
|
||||
for (const { name, key } of this.services) {
|
||||
const service = container.cradle[key] as ServiceWithLifecycle | null;
|
||||
|
||||
if (service) {
|
||||
const initPromise = this.initializeService(name, service);
|
||||
initPromises.push(
|
||||
Promise.race([
|
||||
initPromise,
|
||||
this.createTimeoutPromise(timeout, `${name} initialization timed out after ${timeout}ms`),
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(initPromises);
|
||||
console.log('✅ All services initialized successfully');
|
||||
}
|
||||
|
||||
async shutdownServices(container: AwilixContainer<ServiceDefinitions>): Promise<void> {
|
||||
const shutdownPromises: Promise<void>[] = [];
|
||||
|
||||
// Shutdown in reverse order
|
||||
for (const { name, key } of [...this.services].reverse()) {
|
||||
const service = container.cradle[key] as ServiceWithLifecycle | null;
|
||||
|
||||
if (service) {
|
||||
shutdownPromises.push(this.shutdownService(name, service));
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.allSettled(shutdownPromises);
|
||||
console.log('✅ All services shut down');
|
||||
}
|
||||
|
||||
private async initializeService(name: string, service: ServiceWithLifecycle): Promise<void> {
|
||||
try {
|
||||
if (typeof service.connect === 'function') {
|
||||
await service.connect();
|
||||
console.log(`✅ ${name} connected`);
|
||||
} else if (typeof service.initialize === 'function') {
|
||||
await service.initialize();
|
||||
console.log(`✅ ${name} initialized`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to initialize ${name}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async shutdownService(name: string, service: ServiceWithLifecycle): Promise<void> {
|
||||
try {
|
||||
if (typeof service.disconnect === 'function') {
|
||||
await service.disconnect();
|
||||
} else if (typeof service.close === 'function') {
|
||||
await service.close();
|
||||
} else if (typeof service.shutdown === 'function') {
|
||||
await service.shutdown();
|
||||
}
|
||||
console.log(`✅ ${name} shut down`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Error shutting down ${name}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
private createTimeoutPromise(timeout: number, message: string): Promise<never> {
|
||||
return new Promise((_, reject) => {
|
||||
setTimeout(() => reject(new Error(message)), timeout);
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue