/** * OperationContext - Unified context for handler operations * * Provides streamlined access to: * - Child loggers with hierarchical context * - Database clients (MongoDB, PostgreSQL) * - Contextual cache with automatic key prefixing * - Shared resource management */ import { createCache, type CacheProvider } from '@stock-bot/cache'; import { getLogger, type Logger } from '@stock-bot/logger'; import { getDatabaseConfig } from '@stock-bot/config'; import type { ServiceResolver } from './service-container'; import type { MongoDBClient } from '@stock-bot/mongodb'; import type { PostgreSQLClient } from '@stock-bot/postgres'; export interface OperationContextOptions { handlerName: string; operationName: string; parentLogger?: Logger; container?: ServiceResolver; } export class OperationContext { public readonly logger: Logger; private readonly container?: ServiceResolver; private _mongodb?: MongoDBClient; private _postgres?: PostgreSQLClient; private _cache?: CacheProvider; private _queue?: any; // Type will be QueueManager but we avoid import for circular deps private static sharedCache: CacheProvider | null = null; private static parentLoggers = new Map(); private static databaseConfig: any = null; constructor( public readonly handlerName: string, public readonly operationName: string, parentLoggerOrOptions?: Logger | OperationContextOptions ) { // Handle both old and new constructor signatures if (parentLoggerOrOptions && 'container' in parentLoggerOrOptions) { const options = parentLoggerOrOptions; this.container = options.container; const parent = options.parentLogger || this.getOrCreateParentLogger(); this.logger = parent.child(operationName, { handler: handlerName, operation: operationName }); } else { // Legacy support const parentLogger = parentLoggerOrOptions as Logger | undefined; const parent = parentLogger || this.getOrCreateParentLogger(); this.logger = parent.child(operationName, { handler: handlerName, operation: operationName }); } } // Lazy load MongoDB client get mongodb(): MongoDBClient { if (!this._mongodb) { if (this.container) { try { this._mongodb = this.container.resolve('mongodb'); } catch (error) { this.logger.warn('Failed to resolve MongoDB from container, falling back to singleton', { error }); this._mongodb = this.getLegacyDatabaseClient('mongodb') as MongoDBClient; } } else { this._mongodb = this.getLegacyDatabaseClient('mongodb') as MongoDBClient; } } return this._mongodb!; } // Lazy load PostgreSQL client get postgres(): PostgreSQLClient { if (!this._postgres) { if (this.container) { try { this._postgres = this.container.resolve('postgres'); } catch (error) { this.logger.warn('Failed to resolve PostgreSQL from container, falling back to singleton', { error }); this._postgres = this.getLegacyDatabaseClient('postgres') as PostgreSQLClient; } } else { this._postgres = this.getLegacyDatabaseClient('postgres') as PostgreSQLClient; } } return this._postgres!; } // Lazy load QueueManager get queue(): any { if (!this._queue) { if (this.container) { try { this._queue = this.container.resolve('queue'); } catch (error) { this.logger.warn('Failed to resolve QueueManager from container, falling back to singleton', { error }); this._queue = this.getLegacyQueueManager(); } } else { this._queue = this.getLegacyQueueManager(); } } return this._queue!; } // Legacy method for QueueManager private getLegacyQueueManager(): any { try { // Dynamic import to avoid TypeScript issues during build const { QueueManager } = require('@stock-bot/queue'); return QueueManager.getInstance(); } catch (error) { this.logger.warn('QueueManager not initialized, queue operations may fail', { error }); throw new Error('QueueManager not available'); } } // Legacy method for backward compatibility private getLegacyDatabaseClient(type: 'mongodb' | 'postgres'): any { try { if (type === 'mongodb') { // Dynamic import to avoid TypeScript issues during build const { getMongoDBClient } = require('@stock-bot/mongodb'); return getMongoDBClient(); } else { // Dynamic import to avoid TypeScript issues during build const { getPostgreSQLClient } = require('@stock-bot/postgres'); return getPostgreSQLClient(); } } catch (error) { this.logger.warn(`${type} client not initialized, operations may fail`, { error }); return null; } } private getOrCreateParentLogger(): Logger { const parentKey = `${this.handlerName}-handler`; if (!OperationContext.parentLoggers.has(parentKey)) { const parentLogger = getLogger(parentKey); OperationContext.parentLoggers.set(parentKey, parentLogger); } return OperationContext.parentLoggers.get(parentKey)!; } /** * Get contextual cache with automatic key prefixing * Keys are automatically prefixed as: "operations:handlerName:operationName:key" */ get cache(): CacheProvider { if (!this._cache) { if (this.container) { try { const baseCache = this.container.resolve('cache'); this._cache = this.createContextualCache(baseCache); } catch (error) { this.logger.warn('Failed to resolve cache from container, using shared cache', { error }); this._cache = this.getOrCreateSharedCache(); } } else { this._cache = this.getOrCreateSharedCache(); } } return this._cache!; } private getOrCreateSharedCache(): CacheProvider { if (!OperationContext.sharedCache) { // Get Redis configuration from database config if (!OperationContext.databaseConfig) { OperationContext.databaseConfig = getDatabaseConfig(); } const redisConfig = OperationContext.databaseConfig.dragonfly || { host: 'localhost', port: 6379, db: 1 }; OperationContext.sharedCache = createCache({ keyPrefix: 'operations:', shared: true, // Use singleton Redis connection enableMetrics: true, ttl: 3600, // Default 1 hour TTL redisConfig }); } return this.createContextualCache(OperationContext.sharedCache); } private createContextualCache(baseCache: CacheProvider): CacheProvider { const contextPrefix = `${this.handlerName}:${this.operationName}:`; // Return a proxy that automatically prefixes keys with context return { async get(key: string): Promise { return baseCache.get(`${contextPrefix}${key}`); }, async set(key: string, value: T, options?: any): Promise { return baseCache.set(`${contextPrefix}${key}`, value, options); }, async del(key: string): Promise { return baseCache.del(`${contextPrefix}${key}`); }, async exists(key: string): Promise { return baseCache.exists(`${contextPrefix}${key}`); }, async clear(): Promise { // Not implemented for contextual cache - use del() for specific keys throw new Error('clear() not implemented for contextual cache - use del() for specific keys'); }, async keys(pattern: string): Promise { const fullPattern = `${contextPrefix}${pattern}`; return baseCache.keys(fullPattern); }, getStats() { return baseCache.getStats(); }, async health(): Promise { return baseCache.health(); }, async waitForReady(timeout?: number): Promise { return baseCache.waitForReady(timeout); }, isReady(): boolean { return baseCache.isReady(); } } as CacheProvider; } /** * Factory method to create OperationContext */ static create(handlerName: string, operationName: string, parentLoggerOrOptions?: Logger | OperationContextOptions): OperationContext { if (parentLoggerOrOptions && 'container' in parentLoggerOrOptions) { return new OperationContext(handlerName, operationName, { ...parentLoggerOrOptions, handlerName, operationName }); } return new OperationContext(handlerName, operationName, parentLoggerOrOptions as Logger | undefined); } /** * Get cache key prefix for this operation context */ getCacheKeyPrefix(): string { return `operations:${this.handlerName}:${this.operationName}:`; } /** * Create a child context for sub-operations */ createChild(subOperationName: string): OperationContext { if (this.container) { return new OperationContext( this.handlerName, `${this.operationName}:${subOperationName}`, { handlerName: this.handlerName, operationName: `${this.operationName}:${subOperationName}`, parentLogger: this.logger, container: this.container } ); } return new OperationContext( this.handlerName, `${this.operationName}:${subOperationName}`, this.logger ); } /** * Dispose of resources if using container-based connections * This is a no-op for legacy singleton connections */ async dispose(): Promise { // If using container, it will handle cleanup // For singleton connections, they persist this.logger.debug('OperationContext disposed', { handler: this.handlerName, operation: this.operationName, hasContainer: !!this.container }); } } export default OperationContext;