libs fully refactored

This commit is contained in:
Boki 2025-06-21 19:00:10 -04:00
parent 63baeaec70
commit 1b34da9a69
10 changed files with 181 additions and 21 deletions

View file

@ -0,0 +1,203 @@
import { getLogger, type Logger } from '@stock-bot/logger';
import type {
ConnectionFactory as IConnectionFactory,
ConnectionPool,
ConnectionFactoryConfig,
MongoDBPoolConfig,
PostgreSQLPoolConfig,
CachePoolConfig,
QueuePoolConfig,
PoolMetrics,
} from './types';
export class ConnectionFactory implements IConnectionFactory {
private readonly logger: Logger;
private readonly pools: Map<string, ConnectionPool<any>> = new Map();
private readonly config: ConnectionFactoryConfig;
constructor(config: ConnectionFactoryConfig) {
this.config = config;
this.logger = getLogger(`connection-factory:${config.service}`);
// Note: config is stored for future use and used in logger name
}
async createMongoDB(poolConfig: MongoDBPoolConfig): Promise<ConnectionPool<any>> {
const key = `mongodb:${poolConfig.name}`;
if (this.pools.has(key)) {
this.logger.debug('Reusing existing MongoDB pool', { name: poolConfig.name });
return this.pools.get(key)!;
}
this.logger.info('Creating MongoDB connection pool', {
name: poolConfig.name,
poolSize: poolConfig.poolSize,
});
try {
// Dynamic import to avoid circular dependency
const { createMongoDBClient } = await import('@stock-bot/mongodb');
const events = {
onConnect: () => {
this.logger.debug('MongoDB connected', { pool: poolConfig.name });
},
onDisconnect: () => {
this.logger.debug('MongoDB disconnected', { pool: poolConfig.name });
},
onError: (error: any) => {
this.logger.error('MongoDB error', { pool: poolConfig.name, error });
},
};
const client = createMongoDBClient(poolConfig.config as any, events);
await client.connect();
if (poolConfig.minConnections) {
await client.warmupPool();
}
const pool: ConnectionPool<any> = {
name: poolConfig.name,
client,
metrics: client.getPoolMetrics(),
health: async () => {
try {
await client.getDatabase().admin().ping();
return true;
} catch {
return false;
}
},
dispose: async () => {
await client.disconnect();
this.pools.delete(key);
},
};
this.pools.set(key, pool);
return pool;
} catch (error) {
this.logger.error('Failed to create MongoDB pool', { name: poolConfig.name, error });
throw error;
}
}
async createPostgreSQL(poolConfig: PostgreSQLPoolConfig): Promise<ConnectionPool<any>> {
const key = `postgres:${poolConfig.name}`;
if (this.pools.has(key)) {
this.logger.debug('Reusing existing PostgreSQL pool', { name: poolConfig.name });
return this.pools.get(key)!;
}
this.logger.info('Creating PostgreSQL connection pool', {
name: poolConfig.name,
poolSize: poolConfig.poolSize,
});
try {
// Dynamic import to avoid circular dependency
const { createPostgreSQLClient } = await import('@stock-bot/postgres');
// Events will be handled by the client internally
const client = createPostgreSQLClient(poolConfig.config as any);
await client.connect();
if (poolConfig.minConnections) {
await client.warmupPool();
}
const pool: ConnectionPool<any> = {
name: poolConfig.name,
client,
metrics: client.getPoolMetrics(),
health: async () => client.connected,
dispose: async () => {
await client.disconnect();
this.pools.delete(key);
},
};
this.pools.set(key, pool);
return pool;
} catch (error) {
this.logger.error('Failed to create PostgreSQL pool', { name: poolConfig.name, error });
throw error;
}
}
createCache(poolConfig: CachePoolConfig): ConnectionPool<any> {
const key = `cache:${poolConfig.name}`;
if (this.pools.has(key)) {
this.logger.debug('Reusing existing cache pool', { name: poolConfig.name });
return this.pools.get(key)!;
}
this.logger.info('Creating cache connection pool', {
name: poolConfig.name,
});
try {
// TODO: Implement cache creation with dynamic import
throw new Error('Cache creation temporarily disabled');
} catch (error) {
this.logger.error('Failed to create cache pool', { name: poolConfig.name, error });
throw error;
}
}
createQueue(poolConfig: QueuePoolConfig): ConnectionPool<any> {
const key = `queue:${poolConfig.name}`;
if (this.pools.has(key)) {
this.logger.debug('Reusing existing queue manager', { name: poolConfig.name });
return this.pools.get(key)!;
}
this.logger.info('Creating queue manager', {
name: poolConfig.name,
});
try {
// TODO: Implement queue creation with dynamic import
throw new Error('Queue creation temporarily disabled');
} catch (error) {
this.logger.error('Failed to create queue manager', { name: poolConfig.name, error });
throw error;
}
}
getPool(type: 'mongodb' | 'postgres' | 'cache' | 'queue', name: string): ConnectionPool<any> | undefined {
const key = `${type}:${name}`;
return this.pools.get(key);
}
listPools(): Array<{ type: string; name: string; metrics: PoolMetrics }> {
const result: Array<{ type: string; name: string; metrics: PoolMetrics }> = [];
for (const [key, pool] of this.pools) {
const [type] = key.split(':');
result.push({
type: type || 'unknown',
name: pool.name,
metrics: pool.metrics,
});
}
return result;
}
async disposeAll(): Promise<void> {
this.logger.info('Disposing all connection pools', { service: this.config.service });
const disposePromises = Array.from(this.pools.values()).map(pool => pool.dispose());
await Promise.all(disposePromises);
this.pools.clear();
}
}

View file

@ -0,0 +1,6 @@
// Export all dependency injection components
export * from './service-container';
export { ConnectionFactory } from './connection-factory';
export * from './operation-context';
export * from './pool-size-calculator';
export * from './types';

View file

@ -0,0 +1,61 @@
/**
* OperationContext - Unified context for handler operations
*
* TEMPORARILY DISABLED to avoid circular dependencies during library build
* Will be re-enabled once all core libraries are built
*/
import { getLogger, type Logger } from '@stock-bot/logger';
import type { ServiceResolver } from './service-container';
export interface OperationContextOptions {
handlerName: string;
operationName: string;
parentLogger?: Logger;
container?: ServiceResolver;
}
export class OperationContext {
public readonly logger: Logger;
private readonly container?: ServiceResolver;
constructor(options: OperationContextOptions) {
this.container = options.container;
this.logger = options.parentLogger || getLogger(`${options.handlerName}:${options.operationName}`);
}
/**
* Creates a new OperationContext with automatic resource management
* TEMPORARILY SIMPLIFIED - full implementation will be restored after build fixes
*/
static create(
handlerName: string,
operationName: string,
options: { container?: ServiceResolver; parentLogger?: Logger } = {}
): OperationContext {
return new OperationContext({
handlerName,
operationName,
...options,
});
}
/**
* Cleanup method - simplified for now
*/
async dispose(): Promise<void> {
// Cleanup will be implemented when dependencies are resolved
}
/**
* Create child context - simplified for now
*/
createChild(operationName: string): OperationContext {
return new OperationContext({
handlerName: 'child',
operationName,
parentLogger: this.logger,
container: this.container,
});
}
}

View file

@ -0,0 +1,80 @@
import type { ConnectionPoolConfig } from './types';
export interface PoolSizeRecommendation {
min: number;
max: number;
idle: number;
}
export class PoolSizeCalculator {
private static readonly DEFAULT_SIZES: Record<string, PoolSizeRecommendation> = {
// Service-level defaults
'data-ingestion': { min: 5, max: 50, idle: 10 },
'data-pipeline': { min: 3, max: 30, idle: 5 },
'processing-service': { min: 2, max: 20, idle: 3 },
'web-api': { min: 2, max: 10, idle: 2 },
'portfolio-service': { min: 2, max: 15, idle: 3 },
'strategy-service': { min: 3, max: 25, idle: 5 },
'execution-service': { min: 2, max: 10, idle: 2 },
// Handler-level defaults
'batch-import': { min: 10, max: 100, idle: 20 },
'real-time': { min: 2, max: 10, idle: 3 },
'analytics': { min: 5, max: 30, idle: 10 },
'reporting': { min: 3, max: 20, idle: 5 },
};
static calculate(
serviceName: string,
handlerName?: string,
customConfig?: Partial<ConnectionPoolConfig>
): PoolSizeRecommendation {
// Check for custom configuration first
if (customConfig?.minConnections && customConfig?.maxConnections) {
return {
min: customConfig.minConnections,
max: customConfig.maxConnections,
idle: Math.floor((customConfig.minConnections + customConfig.maxConnections) / 4),
};
}
// Try handler-specific sizes first, then service-level
const key = handlerName || serviceName;
const recommendation = this.DEFAULT_SIZES[key] || this.DEFAULT_SIZES[serviceName];
if (recommendation) {
return { ...recommendation };
}
// Fall back to generic defaults
return {
min: 2,
max: 10,
idle: 3,
};
}
static getOptimalPoolSize(
expectedConcurrency: number,
averageQueryTimeMs: number,
targetLatencyMs: number
): number {
// Little's Law: L = λ * W
// L = number of connections needed
// λ = arrival rate (requests per second)
// W = average time in system (seconds)
const requestsPerSecond = expectedConcurrency;
const averageTimeInSystem = averageQueryTimeMs / 1000;
const minConnections = Math.ceil(requestsPerSecond * averageTimeInSystem);
// Add buffer for burst traffic (20% overhead)
const recommendedSize = Math.ceil(minConnections * 1.2);
// Ensure we meet target latency
const latencyBasedSize = Math.ceil(expectedConcurrency * (averageQueryTimeMs / targetLatencyMs));
return Math.max(recommendedSize, latencyBasedSize, 2); // Minimum 2 connections
}
}

View file

@ -0,0 +1,234 @@
import { getLogger, type Logger } from '@stock-bot/logger';
import type { ConnectionFactory } from './connection-factory';
export interface ServiceRegistration<T = any> {
name: string;
factory: () => T | Promise<T>;
singleton?: boolean;
dispose?: (instance: T) => Promise<void>;
}
export interface ServiceResolver {
resolve<T>(name: string, options?: any): T;
resolveAsync<T>(name: string, options?: any): Promise<T>;
}
export class ServiceContainer implements ServiceResolver {
private readonly logger: Logger;
private readonly registrations = new Map<string, ServiceRegistration>();
private readonly instances = new Map<string, any>();
private readonly scopedInstances = new Map<string, any>();
private readonly parent?: ServiceContainer;
constructor(name: string, parent?: ServiceContainer) {
this.logger = getLogger(`service-container:${name}`);
this.parent = parent;
}
register<T>(registration: ServiceRegistration<T>): void {
this.registrations.set(registration.name, registration);
this.logger.debug('Service registered', { name: registration.name, singleton: registration.singleton });
}
resolve<T>(name: string, _options?: any): T {
// Check scoped instances first
if (this.scopedInstances.has(name)) {
return this.scopedInstances.get(name);
}
// Check singleton instances
if (this.instances.has(name)) {
return this.instances.get(name);
}
// Get registration from this container or parent
const registration = this.getRegistration(name);
if (!registration) {
throw new Error(`Service ${name} not registered`);
}
// Create instance synchronously
const instance = registration.factory();
// Check if factory returned a promise
if (instance instanceof Promise) {
throw new Error(`Service ${name} is async. Use resolveAsync() instead.`);
}
// Store based on singleton flag
if (registration.singleton) {
this.instances.set(name, instance);
} else {
this.scopedInstances.set(name, instance);
}
return instance as T;
}
async resolveAsync<T>(name: string, _options?: any): Promise<T> {
// Check scoped instances first
if (this.scopedInstances.has(name)) {
return this.scopedInstances.get(name);
}
// Check singleton instances
if (this.instances.has(name)) {
return this.instances.get(name);
}
// Get registration from this container or parent
const registration = this.getRegistration(name);
if (!registration) {
throw new Error(`Service ${name} not registered`);
}
// Create instance
const instance = await Promise.resolve(registration.factory());
// Store based on singleton flag
if (registration.singleton) {
this.instances.set(name, instance);
} else {
this.scopedInstances.set(name, instance);
}
return instance as T;
}
createScope(): ServiceContainer {
return new ServiceContainer('scoped', this);
}
async dispose(): Promise<void> {
// Dispose scoped instances
for (const [name, instance] of this.scopedInstances.entries()) {
const registration = this.getRegistration(name);
if (registration?.dispose) {
await registration.dispose(instance);
}
}
this.scopedInstances.clear();
// Only dispose singletons if this is the root container
if (!this.parent) {
for (const [name, instance] of this.instances.entries()) {
const registration = this.registrations.get(name);
if (registration?.dispose) {
await registration.dispose(instance);
}
}
this.instances.clear();
}
}
private getRegistration(name: string): ServiceRegistration | undefined {
return this.registrations.get(name) || this.parent?.getRegistration(name);
}
}
// Enhanced service container factory with infrastructure services
export function createServiceContainer(
serviceName: string,
connectionFactory: ConnectionFactory,
config?: any
): ServiceContainer {
const container = new ServiceContainer(serviceName);
// Register configuration if provided
if (config) {
container.register({
name: 'config',
factory: () => config,
singleton: true,
});
}
// Register connection factories
container.register({
name: 'mongodb',
factory: async () => {
const pool = await connectionFactory.createMongoDB({
name: 'default',
config: {} as any, // Config injected by factory
});
return pool.client;
},
singleton: true,
});
container.register({
name: 'postgres',
factory: async () => {
const pool = await connectionFactory.createPostgreSQL({
name: 'default',
config: {} as any, // Config injected by factory
});
return pool.client;
},
singleton: true,
});
container.register({
name: 'cache',
factory: () => {
const pool = connectionFactory.createCache({
name: 'default',
config: {} as any, // Config injected by factory
});
return pool.client;
},
singleton: true,
});
container.register({
name: 'queue',
factory: () => {
const pool = connectionFactory.createQueue({
name: 'default',
config: {} as any, // Config injected by factory
});
return pool.client;
},
singleton: true,
});
// Optional services - comment out for now to avoid circular dependencies
// These can be registered manually by apps that need them
// // Register ProxyManager
// container.register({
// name: 'proxyManager',
// factory: async () => {
// const { ProxyManager } = await import('@stock-bot/utils');
// await ProxyManager.initialize();
// return ProxyManager.getInstance();
// },
// singleton: true,
// });
// // Register Browser service
// container.register({
// name: 'browser',
// factory: async () => {
// const { Browser } = await import('@stock-bot/browser');
// return Browser;
// },
// singleton: true,
// });
// // Register HttpClient with default configuration
// container.register({
// name: 'httpClient',
// factory: async () => {
// const { createHttpClient } = await import('@stock-bot/http');
// return createHttpClient({
// timeout: 30000,
// retries: 3,
// userAgent: 'stock-bot/1.0',
// });
// },
// singleton: true,
// });
return container;
}

68
libs/core/di/src/types.ts Normal file
View file

@ -0,0 +1,68 @@
// Generic types to avoid circular dependencies
export interface GenericClientConfig {
[key: string]: any;
}
export interface ConnectionPoolConfig {
name: string;
poolSize?: number;
minConnections?: number;
maxConnections?: number;
idleTimeoutMillis?: number;
connectionTimeoutMillis?: number;
enableMetrics?: boolean;
}
export interface MongoDBPoolConfig extends ConnectionPoolConfig {
config: GenericClientConfig;
}
export interface PostgreSQLPoolConfig extends ConnectionPoolConfig {
config: GenericClientConfig;
}
export interface CachePoolConfig extends ConnectionPoolConfig {
config: GenericClientConfig;
}
export interface QueuePoolConfig extends ConnectionPoolConfig {
config: GenericClientConfig;
}
export interface ConnectionFactoryConfig {
service: string;
environment: 'development' | 'production' | 'test';
pools?: {
mongodb?: Partial<MongoDBPoolConfig>;
postgres?: Partial<PostgreSQLPoolConfig>;
cache?: Partial<CachePoolConfig>;
queue?: Partial<QueuePoolConfig>;
};
}
export interface ConnectionPool<T> {
name: string;
client: T;
metrics: PoolMetrics;
health(): Promise<boolean>;
dispose(): Promise<void>;
}
export interface PoolMetrics {
created: Date;
totalConnections: number;
activeConnections: number;
idleConnections: number;
waitingRequests: number;
errors: number;
}
export interface ConnectionFactory {
createMongoDB(config: MongoDBPoolConfig): Promise<ConnectionPool<any>>;
createPostgreSQL(config: PostgreSQLPoolConfig): Promise<ConnectionPool<any>>;
createCache(config: CachePoolConfig): ConnectionPool<any>;
createQueue(config: QueuePoolConfig): ConnectionPool<any>;
getPool(type: 'mongodb' | 'postgres' | 'cache' | 'queue', name: string): ConnectionPool<any> | undefined;
listPools(): Array<{ type: string; name: string; metrics: PoolMetrics }>;
disposeAll(): Promise<void>;
}