refactored di into more composable parts
This commit is contained in:
parent
177fe30586
commit
26ebc77fe6
22 changed files with 908 additions and 281 deletions
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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue