modern decodators
This commit is contained in:
parent
8405f44bd9
commit
931f212ec7
6 changed files with 262 additions and 150 deletions
|
|
@ -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<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
|
||||
*/
|
||||
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 = {
|
||||
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<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
|
||||
*/
|
||||
|
|
@ -67,6 +147,7 @@ export abstract class BaseHandler implements IHandler {
|
|||
async onDispose?(): Promise<void>;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Specialized handler for operations that have scheduled jobs
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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 <T extends { new (...args: any[]): {} }>(constructor: T) {
|
||||
// Store handler name on the constructor for future use
|
||||
(constructor as any).__handlerName = name;
|
||||
return constructor;
|
||||
return function <T extends { new (...args: any[]): {} }>(
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue