diff --git a/apps/data-ingestion/src/handlers/index.ts b/apps/data-ingestion/src/handlers/index.ts new file mode 100644 index 0000000..3e4a83d --- /dev/null +++ b/apps/data-ingestion/src/handlers/index.ts @@ -0,0 +1,23 @@ +/** + * Handler auto-registration + * Import all handlers here to trigger auto-registration + */ + +import type { IDataIngestionServices } from '@stock-bot/di'; +import { QMHandler } from './qm/qm.handler'; + +/** + * Initialize and register all handlers + */ +export function initializeAllHandlers(services: IDataIngestionServices): void { + // QM Handler + const qmHandler = new QMHandler(services); + qmHandler.register(); + + // TODO: Add other handlers here as they're converted + // const webShareHandler = new WebShareHandler(services); + // webShareHandler.register(); + + // const ibHandler = new IBHandler(services); + // ibHandler.register(); +} \ No newline at end of file diff --git a/apps/data-ingestion/src/handlers/qm/qm.handler.ts b/apps/data-ingestion/src/handlers/qm/qm.handler.ts index f38b310..6716dc9 100644 --- a/apps/data-ingestion/src/handlers/qm/qm.handler.ts +++ b/apps/data-ingestion/src/handlers/qm/qm.handler.ts @@ -1,32 +1,17 @@ +import type { IDataIngestionServices } from '@stock-bot/di'; import { BaseHandler, Handler, Operation, QueueSchedule, - type ExecutionContext, - type HandlerConfigWithSchedule + type ExecutionContext } from '@stock-bot/handlers'; -import { handlerRegistry, createJobHandler } from '@stock-bot/types'; -import type { IDataIngestionServices, IExecutionContext } from '@stock-bot/di'; import type { SymbolSpiderJob } from './shared/types'; @Handler('qm') export class QMHandler extends BaseHandler { constructor(services: IDataIngestionServices) { - super(services); - } - - async execute(operation: string, input: unknown, context: ExecutionContext): Promise { - switch (operation) { - case 'create-sessions': - return await this.createSessions(input, context); - case 'search-symbols': - return await this.searchSymbols(input, context); - case 'spider-symbol-search': - return await this.spiderSymbolSearch(input as SymbolSpiderJob, context); - default: - throw new Error(`Unknown operation: ${operation}`); - } + super(services); // Handler name read from @Handler decorator } @Operation('create-sessions') @@ -36,44 +21,69 @@ export class QMHandler extends BaseHandler { description: 'Create and maintain QM sessions' }) async createSessions(input: unknown, context: ExecutionContext): Promise { - // Direct access to typed dependencies - const sessionsCollection = this.mongodb.collection('qm_sessions'); + this.logger.info('Creating QM sessions with new DI pattern...'); - // Get existing sessions - const existingSessions = await sessionsCollection.find({}).toArray(); - this.logger.info('Found existing QM sessions', { count: existingSessions.length }); - - // Cache session count for monitoring - await this.cache.set('qm-sessions-count', existingSessions.length, 3600); - - return { success: true, existingCount: existingSessions.length }; + try { + // Check existing sessions in MongoDB + const sessionsCollection = this.mongodb.collection('qm_sessions'); + const existingSessions = await sessionsCollection.find({}).toArray(); + + this.logger.info('Current QM sessions', { + existing: existingSessions.length, + action: 'creating_new_sessions' + }); + + // Cache session stats for monitoring + await this.cache.set('qm-sessions-count', existingSessions.length, 3600); + await this.cache.set('last-session-check', new Date().toISOString(), 1800); + + // For now, just return the current state + // TODO: Implement actual session creation logic using new DI pattern + return { + success: true, + existingSessions: existingSessions.length, + message: 'QM session check completed' + }; + + } catch (error) { + this.logger.error('Failed to create QM sessions', { error }); + throw error; + } } @Operation('search-symbols') - async searchSymbols(input: unknown, context: ExecutionContext): Promise { - // Direct access to typed dependencies - const symbolsCollection = this.mongodb.collection('qm_symbols'); - - // Get symbols from database - const symbols = await symbolsCollection.find({}).limit(100).toArray(); - this.logger.info('QM symbol search completed', { count: symbols.length }); - - if (symbols && symbols.length > 0) { - // Cache result for performance - await this.cache.set('qm-symbols-sample', symbols.slice(0, 10), 1800); + async searchSymbols(_input: unknown, _context: ExecutionContext): Promise { + this.logger.info('Searching QM symbols with new DI pattern...'); + try { + // Check existing symbols in MongoDB + const symbolsCollection = this.mongodb.collection('qm_symbols'); + const symbols = await symbolsCollection.find({}).limit(100).toArray(); - return { - success: true, - message: 'QM symbol search completed successfully', - count: symbols.length, - symbols: symbols.slice(0, 10), // Return first 10 symbols as sample - }; - } else { - return { - success: false, - message: 'No symbols found', - count: 0, - }; + this.logger.info('QM symbol search completed', { count: symbols.length }); + + if (symbols && symbols.length > 0) { + // Cache result for performance + await this.cache.set('qm-symbols-sample', symbols.slice(0, 10), 1800); + + return { + success: true, + message: 'QM symbol search completed successfully', + count: symbols.length, + symbols: symbols.slice(0, 10), // Return first 10 symbols as sample + }; + } else { + // No symbols found - this is expected initially + this.logger.info('No QM symbols found in database yet'); + return { + success: true, + message: 'No symbols found yet - database is empty', + count: 0, + }; + } + + } catch (error) { + this.logger.error('Failed to search QM symbols', { error }); + throw error; } } @@ -108,57 +118,19 @@ export class QMHandler extends BaseHandler { spiderJobId }; } + + /** + * Provide payloads for scheduled jobs + */ + protected getScheduledJobPayload(operation: string): any { + if (operation === 'spiderSymbolSearch') { + return { + prefix: null, + depth: 1, + source: 'qm', + maxDepth: 4 + }; + } + return undefined; + } } - -// Initialize and register the QM provider with new DI pattern -export function initializeQMProviderNew(services: IDataIngestionServices) { - // Create handler instance with new DI - const handler = new QMHandler(services); - - // Register with legacy format for backward compatibility - const qmProviderConfig: HandlerConfigWithSchedule = { - name: 'qm', - operations: { - 'create-sessions': createJobHandler(async (payload) => { - const context = handler.createExecutionContext('queue', { source: 'queue', timestamp: Date.now() }); - return await handler.execute('create-sessions', payload, context); - }), - 'search-symbols': createJobHandler(async (payload) => { - const context = handler.createExecutionContext('queue', { source: 'queue', timestamp: Date.now() }); - return await handler.execute('search-symbols', payload, context); - }), - 'spider-symbol-search': createJobHandler(async (payload: SymbolSpiderJob) => { - const context = handler.createExecutionContext('queue', { source: 'queue', timestamp: Date.now() }); - return await handler.execute('spider-symbol-search', payload, context); - }), - }, - - scheduledJobs: [ - { - type: 'session-management', - operation: 'create-sessions', - cronPattern: '0 */15 * * *', // Every 15 minutes - priority: 7, - immediately: true, - description: 'Create and maintain QM sessions', - }, - { - type: 'qm-maintnance', - operation: 'spider-symbol-search', - payload: { - prefix: null, - depth: 1, - source: 'qm', - maxDepth: 4 - }, - cronPattern: '0 0 * * 0', // Every Sunday at midnight - priority: 10, - immediately: true, - description: 'Comprehensive symbol search using QM API', - }, - ], - }; - - handlerRegistry.registerWithSchedule(qmProviderConfig); - handler.logger.debug('QM provider registered successfully with new DI pattern'); -} \ No newline at end of file diff --git a/apps/data-ingestion/src/index.ts b/apps/data-ingestion/src/index.ts index a69a8c3..8ec3c15 100644 --- a/apps/data-ingestion/src/index.ts +++ b/apps/data-ingestion/src/index.ts @@ -9,19 +9,19 @@ import { Hono } from 'hono'; import { cors } from 'hono/cors'; // Library imports +import { + createDataIngestionServices, + disposeDataIngestionServices, + type IDataIngestionServices +} from '@stock-bot/di'; import { getLogger, setLoggerConfig, shutdownLoggers } from '@stock-bot/logger'; import { Shutdown } from '@stock-bot/shutdown'; -import { ProxyManager } from '@stock-bot/utils'; -import { - createDataIngestionServices, - disposeDataIngestionServices, - type IDataIngestionServices -} from '@stock-bot/di'; import { handlerRegistry } from '@stock-bot/types'; +import { ProxyManager } from '@stock-bot/utils'; // Local imports import { createRoutes } from './routes/create-routes'; -import { initializeQMProviderNew } from './handlers/qm/qm.handler'; +import { initializeAllHandlers } from './handlers'; const config = initializeServiceConfig(); console.log('Data Service Configuration:', JSON.stringify(config, null, 2)); @@ -84,19 +84,14 @@ async function initializeServices() { // Initialize handlers with new DI pattern logger.debug('Initializing data handlers with new DI pattern...'); - // Initialize QM handler with new pattern - initializeQMProviderNew(services); - - // TODO: Convert other handlers to new pattern - // initializeWebShareProviderNew(services); - // initializeIBProviderNew(services); - // initializeProxyProviderNew(services); + // Auto-register all handlers + initializeAllHandlers(services); logger.info('Data handlers initialized with new DI pattern'); // Create scheduled jobs from registered handlers logger.debug('Creating scheduled jobs from registered handlers...'); - const allHandlers = handlerRegistry.getAllHandlers(); + const allHandlers = handlerRegistry.getAllHandlersWithSchedule(); let totalScheduledJobs = 0; for (const [handlerName, config] of allHandlers) { diff --git a/libs/core/handlers/src/base/BaseHandler.ts b/libs/core/handlers/src/base/BaseHandler.ts index 50f7f3f..779e195 100644 --- a/libs/core/handlers/src/base/BaseHandler.ts +++ b/libs/core/handlers/src/base/BaseHandler.ts @@ -1,6 +1,7 @@ import { getLogger } from '@stock-bot/logger'; import type { IDataIngestionServices, IExecutionContext } from '@stock-bot/di'; import type { IHandler, ExecutionContext } from '../types/types'; +import { handlerRegistry, createJobHandler, type HandlerConfigWithSchedule } from '@stock-bot/types'; /** * Abstract base class for all handlers with improved DI @@ -8,9 +9,13 @@ import type { IHandler, ExecutionContext } from '../types/types'; */ export abstract class BaseHandler implements IHandler { protected readonly logger; + private handlerName: string; - constructor(protected readonly services: IDataIngestionServices) { + constructor(protected readonly services: IDataIngestionServices, handlerName?: string) { this.logger = getLogger(this.constructor.name); + // Read handler name from decorator first, then fallback to parameter or class name + const constructor = this.constructor as any; + this.handlerName = constructor.__handlerName || handlerName || this.constructor.name.toLowerCase(); } // Convenience getters for common services @@ -20,18 +25,35 @@ export abstract class BaseHandler implements IHandler { protected get queue() { return this.services.queue; } /** - * Main execution method - must be implemented by subclasses + * Main execution method - automatically routes to decorated methods * Works with queue (events commented for future) */ - abstract execute(operation: string, input: unknown, context: ExecutionContext): Promise; + async execute(operation: string, input: unknown, context: ExecutionContext): Promise { + const constructor = this.constructor as any; + const operations = constructor.__operations || []; + + // Find the operation metadata + const operationMeta = operations.find((op: any) => op.name === operation); + if (!operationMeta) { + throw new Error(`Unknown operation: ${operation}`); + } + + // Get the method from the instance and call it + const method = (this as any)[operationMeta.method]; + if (typeof method !== 'function') { + throw new Error(`Operation method '${operationMeta.method}' not found on handler`); + } + + return await method.call(this, input, context); + } /** * Queue helper methods - now type-safe and direct */ protected async scheduleOperation(operation: string, payload: unknown, delay?: number): Promise { - const queue = this.services.queue.getQueue(this.constructor.name.toLowerCase()); + const queue = this.services.queue.getQueue(this.handlerName); const jobData = { - handler: this.constructor.name.toLowerCase(), + handler: this.handlerName, operation, payload }; @@ -58,6 +80,64 @@ export abstract class BaseHandler implements IHandler { // await eventBus.publish(eventName, payload); // } + /** + * Register this handler using decorator metadata + * Automatically reads @Handler, @Operation, and @QueueSchedule decorators + */ + register(): void { + const constructor = this.constructor as any; + const handlerName = constructor.__handlerName || this.handlerName; + const operations = constructor.__operations || []; + const schedules = constructor.__schedules || []; + + // Create operation handlers from decorator metadata + const operationHandlers: Record = {}; + for (const op of operations) { + operationHandlers[op.name] = createJobHandler(async (payload) => { + const context: ExecutionContext = { + type: 'queue', + metadata: { source: 'queue', timestamp: Date.now() } + }; + return await this.execute(op.name, payload, context); + }); + } + + // Create scheduled jobs from decorator metadata + const scheduledJobs = schedules.map((schedule: any) => { + // Find the operation name from the method name + const operation = operations.find((op: any) => op.method === schedule.operation); + return { + type: `${handlerName}-${schedule.operation}`, + operation: operation?.name || schedule.operation, + cronPattern: schedule.cronPattern, + priority: schedule.priority || 5, + immediately: schedule.immediately || false, + description: schedule.description || `${handlerName} ${schedule.operation}`, + payload: this.getScheduledJobPayload?.(schedule.operation), + }; + }); + + const config: HandlerConfigWithSchedule = { + name: handlerName, + operations: operationHandlers, + scheduledJobs, + }; + + handlerRegistry.registerWithSchedule(config); + this.logger.debug('Handler registered using decorator metadata', { + handlerName, + operations: operations.length, + schedules: schedules.length + }); + } + + /** + * Override this method to provide payloads for scheduled jobs + * @param operation The operation name that needs a payload + * @returns The payload for the scheduled job, or undefined + */ + protected getScheduledJobPayload?(operation: string): any; + /** * Lifecycle hooks - can be overridden by subclasses */ @@ -67,6 +147,7 @@ export abstract class BaseHandler implements IHandler { async onDispose?(): Promise; } + /** * Specialized handler for operations that have scheduled jobs */ diff --git a/libs/core/handlers/src/decorators/decorators.ts b/libs/core/handlers/src/decorators/decorators.ts index c9f17cc..95302bc 100644 --- a/libs/core/handlers/src/decorators/decorators.ts +++ b/libs/core/handlers/src/decorators/decorators.ts @@ -1,15 +1,20 @@ -// Simple decorators for handler registration -// These are placeholders for now - can be enhanced with reflection later +// Modern TC39 Stage 3 decorators for handler registration /** * Handler decorator - marks a class as a handler * @param name Handler name for registration */ export function Handler(name: string) { - return function (constructor: T) { - // Store handler name on the constructor for future use - (constructor as any).__handlerName = name; - return constructor; + return function ( + target: T, + context: ClassDecoratorContext + ) { + // Store handler name on the constructor + (target as any).__handlerName = name; + (target as any).__needsAutoRegistration = true; + + console.log('Handler decorator applied', { name, className: context.name }); + return target; }; } @@ -18,16 +23,38 @@ export function Handler(name: string) { * @param name Operation name */ export function Operation(name: string) { - return function (target: any, propertyName: string, descriptor?: PropertyDescriptor) { - // Store operation metadata for future use - if (!target.constructor.__operations) { - target.constructor.__operations = []; - } - target.constructor.__operations.push({ - name, - method: propertyName, + return function ( + _target: Function, + context: ClassMethodDecoratorContext + ) { + const methodName = String(context.name); + + console.log('Operation decorator applied', { + operationName: name, + methodName, + contextName: context.name, + contextKind: context.kind }); - return descriptor; + + // Use context.addInitializer to run code when the class is constructed + context.addInitializer(function(this: any) { + const constructor = this.constructor as any; + if (!constructor.__operations) { + constructor.__operations = []; + } + constructor.__operations.push({ + name, + method: methodName, + }); + + console.log('Operation registered via initializer', { + name, + methodName, + className: constructor.name + }); + }); + + // Don't return anything - just modify metadata }; } @@ -44,17 +71,26 @@ export function QueueSchedule( description?: string; } ) { - return function (target: any, propertyName: string, descriptor?: PropertyDescriptor) { - // Store schedule metadata for future use - if (!target.constructor.__schedules) { - target.constructor.__schedules = []; - } - target.constructor.__schedules.push({ - operation: propertyName, - cronPattern, - ...options, + return function ( + _target: Function, + context: ClassMethodDecoratorContext + ) { + const methodName = String(context.name); + + // Use context.addInitializer to run code when the class is constructed + context.addInitializer(function(this: any) { + const constructor = this.constructor as any; + if (!constructor.__schedules) { + constructor.__schedules = []; + } + constructor.__schedules.push({ + operation: methodName, + cronPattern, + ...options, + }); }); - return descriptor; + + // Don't return anything - just modify metadata }; } diff --git a/tsconfig.app.json b/tsconfig.app.json index 42cc06a..198aa42 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -5,7 +5,12 @@ // Override root settings for application builds "composite": true, "incremental": true, - "types": ["bun-types"] + "types": ["bun-types"], + // Modern TC39 decorators configuration + "experimentalDecorators": false, + "emitDecoratorMetadata": false, + "useDefineForClassFields": true + }, "include": ["src/**/*"], "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]