190 lines
No EOL
6.7 KiB
TypeScript
190 lines
No EOL
6.7 KiB
TypeScript
import { getLogger } from '@stock-bot/logger';
|
|
import type { IHandler, ExecutionContext } from '../types/types';
|
|
import type { IServiceContainer } from '../types/service-container';
|
|
import { handlerRegistry, createJobHandler, type HandlerConfigWithSchedule } from '@stock-bot/types';
|
|
|
|
/**
|
|
* Abstract base class for all handlers with improved DI
|
|
* Provides common functionality and structure for queue/event operations
|
|
*/
|
|
export abstract class BaseHandler implements IHandler {
|
|
protected readonly logger;
|
|
private handlerName: string;
|
|
|
|
constructor(protected readonly services: IServiceContainer, 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
|
|
protected get mongodb() { return this.services.mongodb; }
|
|
protected get postgres() { return this.services.postgres; }
|
|
protected get questdb() { return this.services.questdb; }
|
|
protected get cache() { return this.services.cache; }
|
|
protected get queue() { return this.services.queue; }
|
|
protected get http() { return this.services.http; }
|
|
|
|
/**
|
|
* Main execution method - automatically routes to decorated methods
|
|
* Works with queue (events commented for future)
|
|
*/
|
|
async execute(operation: string, input: unknown, context: ExecutionContext): Promise<unknown> {
|
|
const constructor = this.constructor as any;
|
|
const operations = constructor.__operations || [];
|
|
|
|
// Debug logging
|
|
this.logger.debug('Handler execute called', {
|
|
handler: this.handlerName,
|
|
operation,
|
|
availableOperations: operations.map((op: any) => ({ name: op.name, method: op.method }))
|
|
});
|
|
|
|
// Find the operation metadata
|
|
const operationMeta = operations.find((op: any) => op.name === operation);
|
|
if (!operationMeta) {
|
|
this.logger.error('Operation not found', {
|
|
requestedOperation: operation,
|
|
availableOperations: operations.map((op: any) => op.name)
|
|
});
|
|
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`);
|
|
}
|
|
|
|
this.logger.debug('Executing operation method', {
|
|
operation,
|
|
method: operationMeta.method
|
|
});
|
|
|
|
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<void> {
|
|
const queue = this.services.queue.getQueue(this.handlerName);
|
|
const jobData = {
|
|
handler: this.handlerName,
|
|
operation,
|
|
payload
|
|
};
|
|
await queue.add(operation, jobData, { delay });
|
|
}
|
|
|
|
/**
|
|
* Create execution context for operations
|
|
*/
|
|
protected createExecutionContext(type: 'http' | 'queue' | 'scheduled', metadata: Record<string, any> = {}): ExecutionContext {
|
|
return {
|
|
type,
|
|
metadata: {
|
|
...metadata,
|
|
timestamp: Date.now(),
|
|
traceId: `${this.constructor.name}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Event methods - commented for future
|
|
*/
|
|
// protected async publishEvent(eventName: string, payload: unknown): Promise<void> {
|
|
// const eventBus = await this.container.resolveAsync('eventBus');
|
|
// 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.info('Handler registered using decorator metadata', {
|
|
handlerName,
|
|
operations: operations.map((op: any) => ({ name: op.name, method: op.method })),
|
|
scheduledJobs: scheduledJobs.map((job: any) => ({
|
|
operation: job.operation,
|
|
cronPattern: job.cronPattern,
|
|
immediately: job.immediately
|
|
}))
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
async onInit?(): Promise<void>;
|
|
async onStart?(): Promise<void>;
|
|
async onStop?(): Promise<void>;
|
|
async onDispose?(): Promise<void>;
|
|
}
|
|
|
|
|
|
/**
|
|
* Specialized handler for operations that have scheduled jobs
|
|
*/
|
|
export abstract class ScheduledHandler extends BaseHandler {
|
|
/**
|
|
* Get scheduled job configurations for this handler
|
|
* Override in subclasses to define schedules
|
|
*/
|
|
getScheduledJobs?(): Array<{
|
|
operation: string;
|
|
cronPattern: string;
|
|
priority?: number;
|
|
immediately?: boolean;
|
|
description?: string;
|
|
}>;
|
|
} |