modern decodators

This commit is contained in:
Boki 2025-06-21 21:24:09 -04:00
parent 8405f44bd9
commit 931f212ec7
6 changed files with 262 additions and 150 deletions

View file

@ -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();
}

View file

@ -1,32 +1,17 @@
import type { IDataIngestionServices } from '@stock-bot/di';
import { import {
BaseHandler, BaseHandler,
Handler, Handler,
Operation, Operation,
QueueSchedule, QueueSchedule,
type ExecutionContext, type ExecutionContext
type HandlerConfigWithSchedule
} from '@stock-bot/handlers'; } 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'; import type { SymbolSpiderJob } from './shared/types';
@Handler('qm') @Handler('qm')
export class QMHandler extends BaseHandler { export class QMHandler extends BaseHandler {
constructor(services: IDataIngestionServices) { constructor(services: IDataIngestionServices) {
super(services); super(services); // Handler name read from @Handler decorator
}
async execute(operation: string, input: unknown, context: ExecutionContext): Promise<unknown> {
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}`);
}
} }
@Operation('create-sessions') @Operation('create-sessions')
@ -36,26 +21,44 @@ export class QMHandler extends BaseHandler {
description: 'Create and maintain QM sessions' description: 'Create and maintain QM sessions'
}) })
async createSessions(input: unknown, context: ExecutionContext): Promise<unknown> { async createSessions(input: unknown, context: ExecutionContext): Promise<unknown> {
// Direct access to typed dependencies this.logger.info('Creating QM sessions with new DI pattern...');
try {
// Check existing sessions in MongoDB
const sessionsCollection = this.mongodb.collection('qm_sessions'); const sessionsCollection = this.mongodb.collection('qm_sessions');
// Get existing sessions
const existingSessions = await sessionsCollection.find({}).toArray(); const existingSessions = await sessionsCollection.find({}).toArray();
this.logger.info('Found existing QM sessions', { count: existingSessions.length });
// Cache session count for monitoring 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('qm-sessions-count', existingSessions.length, 3600);
await this.cache.set('last-session-check', new Date().toISOString(), 1800);
return { success: true, existingCount: existingSessions.length }; // 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') @Operation('search-symbols')
async searchSymbols(input: unknown, context: ExecutionContext): Promise<unknown> { async searchSymbols(_input: unknown, _context: ExecutionContext): Promise<unknown> {
// Direct access to typed dependencies this.logger.info('Searching QM symbols with new DI pattern...');
try {
// Check existing symbols in MongoDB
const symbolsCollection = this.mongodb.collection('qm_symbols'); const symbolsCollection = this.mongodb.collection('qm_symbols');
// Get symbols from database
const symbols = await symbolsCollection.find({}).limit(100).toArray(); const symbols = await symbolsCollection.find({}).limit(100).toArray();
this.logger.info('QM symbol search completed', { count: symbols.length }); this.logger.info('QM symbol search completed', { count: symbols.length });
if (symbols && symbols.length > 0) { if (symbols && symbols.length > 0) {
@ -69,12 +72,19 @@ export class QMHandler extends BaseHandler {
symbols: symbols.slice(0, 10), // Return first 10 symbols as sample symbols: symbols.slice(0, 10), // Return first 10 symbols as sample
}; };
} else { } else {
// No symbols found - this is expected initially
this.logger.info('No QM symbols found in database yet');
return { return {
success: false, success: true,
message: 'No symbols found', message: 'No symbols found yet - database is empty',
count: 0, count: 0,
}; };
} }
} catch (error) {
this.logger.error('Failed to search QM symbols', { error });
throw error;
}
} }
@Operation('spider-symbol-search') @Operation('spider-symbol-search')
@ -108,57 +118,19 @@ export class QMHandler extends BaseHandler {
spiderJobId spiderJobId
}; };
} }
}
// Initialize and register the QM provider with new DI pattern /**
export function initializeQMProviderNew(services: IDataIngestionServices) { * Provide payloads for scheduled jobs
// Create handler instance with new DI */
const handler = new QMHandler(services); protected getScheduledJobPayload(operation: string): any {
if (operation === 'spiderSymbolSearch') {
// Register with legacy format for backward compatibility return {
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, prefix: null,
depth: 1, depth: 1,
source: 'qm', source: 'qm',
maxDepth: 4 maxDepth: 4
},
cronPattern: '0 0 * * 0', // Every Sunday at midnight
priority: 10,
immediately: true,
description: 'Comprehensive symbol search using QM API',
},
],
}; };
}
handlerRegistry.registerWithSchedule(qmProviderConfig); return undefined;
handler.logger.debug('QM provider registered successfully with new DI pattern'); }
} }

View file

@ -9,19 +9,19 @@ import { Hono } from 'hono';
import { cors } from 'hono/cors'; import { cors } from 'hono/cors';
// Library imports // Library imports
import { getLogger, setLoggerConfig, shutdownLoggers } from '@stock-bot/logger';
import { Shutdown } from '@stock-bot/shutdown';
import { ProxyManager } from '@stock-bot/utils';
import { import {
createDataIngestionServices, createDataIngestionServices,
disposeDataIngestionServices, disposeDataIngestionServices,
type IDataIngestionServices type IDataIngestionServices
} from '@stock-bot/di'; } from '@stock-bot/di';
import { getLogger, setLoggerConfig, shutdownLoggers } from '@stock-bot/logger';
import { Shutdown } from '@stock-bot/shutdown';
import { handlerRegistry } from '@stock-bot/types'; import { handlerRegistry } from '@stock-bot/types';
import { ProxyManager } from '@stock-bot/utils';
// Local imports // Local imports
import { createRoutes } from './routes/create-routes'; import { createRoutes } from './routes/create-routes';
import { initializeQMProviderNew } from './handlers/qm/qm.handler'; import { initializeAllHandlers } from './handlers';
const config = initializeServiceConfig(); const config = initializeServiceConfig();
console.log('Data Service Configuration:', JSON.stringify(config, null, 2)); console.log('Data Service Configuration:', JSON.stringify(config, null, 2));
@ -84,19 +84,14 @@ async function initializeServices() {
// Initialize handlers with new DI pattern // Initialize handlers with new DI pattern
logger.debug('Initializing data handlers with new DI pattern...'); logger.debug('Initializing data handlers with new DI pattern...');
// Initialize QM handler with new pattern // Auto-register all handlers
initializeQMProviderNew(services); initializeAllHandlers(services);
// TODO: Convert other handlers to new pattern
// initializeWebShareProviderNew(services);
// initializeIBProviderNew(services);
// initializeProxyProviderNew(services);
logger.info('Data handlers initialized with new DI pattern'); logger.info('Data handlers initialized with new DI pattern');
// Create scheduled jobs from registered handlers // Create scheduled jobs from registered handlers
logger.debug('Creating scheduled jobs from registered handlers...'); logger.debug('Creating scheduled jobs from registered handlers...');
const allHandlers = handlerRegistry.getAllHandlers(); const allHandlers = handlerRegistry.getAllHandlersWithSchedule();
let totalScheduledJobs = 0; let totalScheduledJobs = 0;
for (const [handlerName, config] of allHandlers) { for (const [handlerName, config] of allHandlers) {

View file

@ -1,6 +1,7 @@
import { getLogger } from '@stock-bot/logger'; import { getLogger } from '@stock-bot/logger';
import type { IDataIngestionServices, IExecutionContext } from '@stock-bot/di'; import type { IDataIngestionServices, IExecutionContext } from '@stock-bot/di';
import type { IHandler, ExecutionContext } from '../types/types'; 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 * 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 { export abstract class BaseHandler implements IHandler {
protected readonly logger; protected readonly logger;
private handlerName: string;
constructor(protected readonly services: IDataIngestionServices) { constructor(protected readonly services: IDataIngestionServices, handlerName?: string) {
this.logger = getLogger(this.constructor.name); 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 // Convenience getters for common services
@ -20,18 +25,35 @@ export abstract class BaseHandler implements IHandler {
protected get queue() { return this.services.queue; } 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) * Works with queue (events commented for future)
*/ */
abstract execute(operation: string, input: unknown, context: ExecutionContext): Promise<unknown>; async execute(operation: string, input: unknown, context: ExecutionContext): Promise<unknown> {
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 * Queue helper methods - now type-safe and direct
*/ */
protected async scheduleOperation(operation: string, payload: unknown, delay?: number): Promise<void> { protected async scheduleOperation(operation: string, payload: unknown, delay?: number): Promise<void> {
const queue = this.services.queue.getQueue(this.constructor.name.toLowerCase()); const queue = this.services.queue.getQueue(this.handlerName);
const jobData = { const jobData = {
handler: this.constructor.name.toLowerCase(), handler: this.handlerName,
operation, operation,
payload payload
}; };
@ -58,6 +80,64 @@ export abstract class BaseHandler implements IHandler {
// await eventBus.publish(eventName, payload); // 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<string, any> = {};
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 * Lifecycle hooks - can be overridden by subclasses
*/ */
@ -67,6 +147,7 @@ export abstract class BaseHandler implements IHandler {
async onDispose?(): Promise<void>; async onDispose?(): Promise<void>;
} }
/** /**
* Specialized handler for operations that have scheduled jobs * Specialized handler for operations that have scheduled jobs
*/ */

View file

@ -1,15 +1,20 @@
// Simple decorators for handler registration // Modern TC39 Stage 3 decorators for handler registration
// These are placeholders for now - can be enhanced with reflection later
/** /**
* Handler decorator - marks a class as a handler * Handler decorator - marks a class as a handler
* @param name Handler name for registration * @param name Handler name for registration
*/ */
export function Handler(name: string) { export function Handler(name: string) {
return function <T extends { new (...args: any[]): {} }>(constructor: T) { return function <T extends { new (...args: any[]): {} }>(
// Store handler name on the constructor for future use target: T,
(constructor as any).__handlerName = name; context: ClassDecoratorContext
return constructor; ) {
// 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 * @param name Operation name
*/ */
export function Operation(name: string) { export function Operation(name: string) {
return function (target: any, propertyName: string, descriptor?: PropertyDescriptor) { return function (
// Store operation metadata for future use _target: Function,
if (!target.constructor.__operations) { context: ClassMethodDecoratorContext
target.constructor.__operations = []; ) {
} const methodName = String(context.name);
target.constructor.__operations.push({
name, console.log('Operation decorator applied', {
method: propertyName, 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; description?: string;
} }
) { ) {
return function (target: any, propertyName: string, descriptor?: PropertyDescriptor) { return function (
// Store schedule metadata for future use _target: Function,
if (!target.constructor.__schedules) { context: ClassMethodDecoratorContext
target.constructor.__schedules = []; ) {
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 = [];
} }
target.constructor.__schedules.push({ constructor.__schedules.push({
operation: propertyName, operation: methodName,
cronPattern, cronPattern,
...options, ...options,
}); });
return descriptor; });
// Don't return anything - just modify metadata
}; };
} }

View file

@ -5,7 +5,12 @@
// Override root settings for application builds // Override root settings for application builds
"composite": true, "composite": true,
"incremental": true, "incremental": true,
"types": ["bun-types"] "types": ["bun-types"],
// Modern TC39 decorators configuration
"experimentalDecorators": false,
"emitDecoratorMetadata": false,
"useDefineForClassFields": true
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"] "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]