307 lines
No EOL
9.9 KiB
TypeScript
307 lines
No EOL
9.9 KiB
TypeScript
/**
|
|
* 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<string, Logger>();
|
|
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<MongoDBClient>('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<PostgreSQLClient>('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<CacheProvider>('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<T>(key: string): Promise<T | null> {
|
|
return baseCache.get(`${contextPrefix}${key}`);
|
|
},
|
|
|
|
async set<T>(key: string, value: T, options?: any): Promise<T | null> {
|
|
return baseCache.set(`${contextPrefix}${key}`, value, options);
|
|
},
|
|
|
|
async del(key: string): Promise<void> {
|
|
return baseCache.del(`${contextPrefix}${key}`);
|
|
},
|
|
|
|
async exists(key: string): Promise<boolean> {
|
|
return baseCache.exists(`${contextPrefix}${key}`);
|
|
},
|
|
|
|
async clear(): Promise<void> {
|
|
// 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<string[]> {
|
|
const fullPattern = `${contextPrefix}${pattern}`;
|
|
return baseCache.keys(fullPattern);
|
|
},
|
|
|
|
getStats() {
|
|
return baseCache.getStats();
|
|
},
|
|
|
|
async health(): Promise<boolean> {
|
|
return baseCache.health();
|
|
},
|
|
|
|
async waitForReady(timeout?: number): Promise<void> {
|
|
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<void> {
|
|
// 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; |