format
This commit is contained in:
parent
d858222af7
commit
7d9044ab29
202 changed files with 10755 additions and 10972 deletions
|
|
@ -1,297 +1,307 @@
|
|||
import { getLogger } from '@stock-bot/logger';
|
||||
import { createJobHandler, handlerRegistry, type HandlerConfigWithSchedule } from '@stock-bot/types';
|
||||
import { fetch } from '@stock-bot/utils';
|
||||
import type { Collection } from 'mongodb';
|
||||
import type { IServiceContainer } from '../types/service-container';
|
||||
import type { ExecutionContext, IHandler } from '../types/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 {
|
||||
// Direct service properties - flattened for cleaner access
|
||||
readonly logger;
|
||||
readonly cache;
|
||||
readonly queue;
|
||||
readonly proxy;
|
||||
readonly browser;
|
||||
readonly mongodb;
|
||||
readonly postgres;
|
||||
readonly questdb;
|
||||
|
||||
private handlerName: string;
|
||||
|
||||
constructor(services: IServiceContainer, handlerName?: string) {
|
||||
// Flatten all services onto the handler instance
|
||||
this.logger = getLogger(this.constructor.name);
|
||||
this.cache = services.cache;
|
||||
this.queue = services.queue;
|
||||
this.proxy = services.proxy;
|
||||
this.browser = services.browser;
|
||||
this.mongodb = services.mongodb;
|
||||
this.postgres = services.postgres;
|
||||
this.questdb = services.questdb;
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
async scheduleOperation(operation: string, payload: unknown, delay?: number): Promise<void> {
|
||||
if (!this.queue) {
|
||||
throw new Error('Queue service is not available');
|
||||
}
|
||||
const queue = this.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)}`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper methods for common operations
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get a MongoDB collection with type safety
|
||||
*/
|
||||
protected collection<T extends {} = any>(name: string): Collection<T> {
|
||||
if (!this.mongodb) {
|
||||
throw new Error('MongoDB service is not available');
|
||||
}
|
||||
return this.mongodb.collection(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cache with handler-prefixed key
|
||||
*/
|
||||
protected async cacheSet(key: string, value: any, ttl?: number): Promise<void> {
|
||||
if (!this.cache) {
|
||||
return;
|
||||
}
|
||||
return this.cache.set(`${this.handlerName}:${key}`, value, ttl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache with handler-prefixed key
|
||||
*/
|
||||
protected async cacheGet<T = any>(key: string): Promise<T | null> {
|
||||
if (!this.cache) {
|
||||
return null;
|
||||
}
|
||||
return this.cache.get(`${this.handlerName}:${key}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete cache with handler-prefixed key
|
||||
*/
|
||||
protected async cacheDel(key: string): Promise<void> {
|
||||
if (!this.cache) {
|
||||
return;
|
||||
}
|
||||
return this.cache.del(`${this.handlerName}:${key}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule operation with delay in seconds
|
||||
*/
|
||||
protected async scheduleIn(operation: string, payload: unknown, delaySeconds: number): Promise<void> {
|
||||
return this.scheduleOperation(operation, payload, delaySeconds * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log with handler context
|
||||
*/
|
||||
protected log(level: 'info' | 'warn' | 'error' | 'debug', message: string, meta?: any): void {
|
||||
this.logger[level](message, { handler: this.handlerName, ...meta });
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP client helper using fetch from utils
|
||||
*/
|
||||
protected get http() {
|
||||
return {
|
||||
get: (url: string, options?: any) =>
|
||||
fetch(url, { ...options, method: 'GET', logger: this.logger }),
|
||||
post: (url: string, data?: any, options?: any) =>
|
||||
fetch(url, {
|
||||
...options,
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
||||
logger: this.logger
|
||||
}),
|
||||
put: (url: string, data?: any, options?: any) =>
|
||||
fetch(url, {
|
||||
...options,
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
||||
logger: this.logger
|
||||
}),
|
||||
delete: (url: string, options?: any) =>
|
||||
fetch(url, { ...options, method: 'DELETE', logger: this.logger }),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a service is available
|
||||
*/
|
||||
protected hasService(name: keyof IServiceContainer): boolean {
|
||||
const service = this[name as keyof this];
|
||||
return service !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}>;
|
||||
}
|
||||
import type { Collection } from 'mongodb';
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
import {
|
||||
createJobHandler,
|
||||
handlerRegistry,
|
||||
type HandlerConfigWithSchedule,
|
||||
} from '@stock-bot/types';
|
||||
import { fetch } from '@stock-bot/utils';
|
||||
import type { IServiceContainer } from '../types/service-container';
|
||||
import type { ExecutionContext, IHandler } from '../types/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 {
|
||||
// Direct service properties - flattened for cleaner access
|
||||
readonly logger;
|
||||
readonly cache;
|
||||
readonly queue;
|
||||
readonly proxy;
|
||||
readonly browser;
|
||||
readonly mongodb;
|
||||
readonly postgres;
|
||||
readonly questdb;
|
||||
|
||||
private handlerName: string;
|
||||
|
||||
constructor(services: IServiceContainer, handlerName?: string) {
|
||||
// Flatten all services onto the handler instance
|
||||
this.logger = getLogger(this.constructor.name);
|
||||
this.cache = services.cache;
|
||||
this.queue = services.queue;
|
||||
this.proxy = services.proxy;
|
||||
this.browser = services.browser;
|
||||
this.mongodb = services.mongodb;
|
||||
this.postgres = services.postgres;
|
||||
this.questdb = services.questdb;
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
async scheduleOperation(operation: string, payload: unknown, delay?: number): Promise<void> {
|
||||
if (!this.queue) {
|
||||
throw new Error('Queue service is not available');
|
||||
}
|
||||
const queue = this.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)}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper methods for common operations
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get a MongoDB collection with type safety
|
||||
*/
|
||||
protected collection<T extends {} = any>(name: string): Collection<T> {
|
||||
if (!this.mongodb) {
|
||||
throw new Error('MongoDB service is not available');
|
||||
}
|
||||
return this.mongodb.collection(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cache with handler-prefixed key
|
||||
*/
|
||||
protected async cacheSet(key: string, value: any, ttl?: number): Promise<void> {
|
||||
if (!this.cache) {
|
||||
return;
|
||||
}
|
||||
return this.cache.set(`${this.handlerName}:${key}`, value, ttl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache with handler-prefixed key
|
||||
*/
|
||||
protected async cacheGet<T = any>(key: string): Promise<T | null> {
|
||||
if (!this.cache) {
|
||||
return null;
|
||||
}
|
||||
return this.cache.get(`${this.handlerName}:${key}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete cache with handler-prefixed key
|
||||
*/
|
||||
protected async cacheDel(key: string): Promise<void> {
|
||||
if (!this.cache) {
|
||||
return;
|
||||
}
|
||||
return this.cache.del(`${this.handlerName}:${key}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule operation with delay in seconds
|
||||
*/
|
||||
protected async scheduleIn(
|
||||
operation: string,
|
||||
payload: unknown,
|
||||
delaySeconds: number
|
||||
): Promise<void> {
|
||||
return this.scheduleOperation(operation, payload, delaySeconds * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log with handler context
|
||||
*/
|
||||
protected log(level: 'info' | 'warn' | 'error' | 'debug', message: string, meta?: any): void {
|
||||
this.logger[level](message, { handler: this.handlerName, ...meta });
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP client helper using fetch from utils
|
||||
*/
|
||||
protected get http() {
|
||||
return {
|
||||
get: (url: string, options?: any) =>
|
||||
fetch(url, { ...options, method: 'GET', logger: this.logger }),
|
||||
post: (url: string, data?: any, options?: any) =>
|
||||
fetch(url, {
|
||||
...options,
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
||||
logger: this.logger,
|
||||
}),
|
||||
put: (url: string, data?: any, options?: any) =>
|
||||
fetch(url, {
|
||||
...options,
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
||||
logger: this.logger,
|
||||
}),
|
||||
delete: (url: string, options?: any) =>
|
||||
fetch(url, { ...options, method: 'DELETE', logger: this.logger }),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a service is available
|
||||
*/
|
||||
protected hasService(name: keyof IServiceContainer): boolean {
|
||||
const service = this[name as keyof this];
|
||||
return service !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,148 +1,130 @@
|
|||
// Bun-compatible decorators (hybrid approach)
|
||||
|
||||
/**
|
||||
* 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[]): {} }>(
|
||||
target: T,
|
||||
_context?: any
|
||||
) {
|
||||
// Store handler name on the constructor
|
||||
(target as any).__handlerName = name;
|
||||
(target as any).__needsAutoRegistration = true;
|
||||
|
||||
return target;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Operation decorator - marks a method as an operation
|
||||
* @param name Operation name
|
||||
*/
|
||||
export function Operation(name: string): any {
|
||||
return function (
|
||||
target: any,
|
||||
methodName: string,
|
||||
descriptor?: PropertyDescriptor
|
||||
): any {
|
||||
// Store metadata directly on the class constructor
|
||||
const constructor = target.constructor;
|
||||
|
||||
if (!constructor.__operations) {
|
||||
constructor.__operations = [];
|
||||
}
|
||||
constructor.__operations.push({
|
||||
name,
|
||||
method: methodName,
|
||||
});
|
||||
|
||||
return descriptor;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue schedule decorator - marks an operation as scheduled
|
||||
* @param cronPattern Cron pattern for scheduling
|
||||
* @param options Additional scheduling options
|
||||
*/
|
||||
export function QueueSchedule(
|
||||
cronPattern: string,
|
||||
options?: {
|
||||
priority?: number;
|
||||
immediately?: boolean;
|
||||
description?: string;
|
||||
}
|
||||
): any {
|
||||
return function (
|
||||
target: any,
|
||||
methodName: string,
|
||||
descriptor?: PropertyDescriptor
|
||||
): any {
|
||||
// Store metadata directly on the class constructor
|
||||
const constructor = target.constructor;
|
||||
|
||||
if (!constructor.__schedules) {
|
||||
constructor.__schedules = [];
|
||||
}
|
||||
constructor.__schedules.push({
|
||||
operation: methodName,
|
||||
cronPattern,
|
||||
...options,
|
||||
});
|
||||
|
||||
return descriptor;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Disabled decorator - marks a handler as disabled for auto-registration
|
||||
* Handlers marked with @Disabled() will be skipped during auto-registration
|
||||
*/
|
||||
export function Disabled() {
|
||||
return function <T extends { new (...args: any[]): {} }>(
|
||||
target: T,
|
||||
_context?: any
|
||||
) {
|
||||
// Store disabled flag on the constructor
|
||||
(target as any).__disabled = true;
|
||||
|
||||
return target;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Combined decorator for scheduled operations
|
||||
* Automatically creates both an operation and a schedule
|
||||
* @param name Operation name
|
||||
* @param cronPattern Cron pattern for scheduling
|
||||
* @param options Schedule options
|
||||
*/
|
||||
export function ScheduledOperation(
|
||||
name: string,
|
||||
cronPattern: string,
|
||||
options?: {
|
||||
priority?: number;
|
||||
immediately?: boolean;
|
||||
description?: string;
|
||||
}
|
||||
): any {
|
||||
return function (
|
||||
target: any,
|
||||
methodName: string,
|
||||
descriptor?: PropertyDescriptor
|
||||
): any {
|
||||
// Apply both decorators
|
||||
Operation(name)(target, methodName, descriptor);
|
||||
QueueSchedule(cronPattern, options)(target, methodName, descriptor);
|
||||
return descriptor;
|
||||
};
|
||||
}
|
||||
|
||||
// Future event decorators - commented for now
|
||||
// export function EventListener(eventName: string) {
|
||||
// return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
|
||||
// if (!target.constructor.__eventListeners) {
|
||||
// target.constructor.__eventListeners = [];
|
||||
// }
|
||||
// target.constructor.__eventListeners.push({
|
||||
// eventName,
|
||||
// method: propertyName,
|
||||
// });
|
||||
// return descriptor;
|
||||
// };
|
||||
// }
|
||||
|
||||
// export function EventPublisher(eventName: string) {
|
||||
// return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
|
||||
// if (!target.constructor.__eventPublishers) {
|
||||
// target.constructor.__eventPublishers = [];
|
||||
// }
|
||||
// target.constructor.__eventPublishers.push({
|
||||
// eventName,
|
||||
// method: propertyName,
|
||||
// });
|
||||
// return descriptor;
|
||||
// };
|
||||
// }
|
||||
// Bun-compatible decorators (hybrid approach)
|
||||
|
||||
/**
|
||||
* 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[]): {} }>(target: T, _context?: any) {
|
||||
// Store handler name on the constructor
|
||||
(target as any).__handlerName = name;
|
||||
(target as any).__needsAutoRegistration = true;
|
||||
|
||||
return target;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Operation decorator - marks a method as an operation
|
||||
* @param name Operation name
|
||||
*/
|
||||
export function Operation(name: string): any {
|
||||
return function (target: any, methodName: string, descriptor?: PropertyDescriptor): any {
|
||||
// Store metadata directly on the class constructor
|
||||
const constructor = target.constructor;
|
||||
|
||||
if (!constructor.__operations) {
|
||||
constructor.__operations = [];
|
||||
}
|
||||
constructor.__operations.push({
|
||||
name,
|
||||
method: methodName,
|
||||
});
|
||||
|
||||
return descriptor;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue schedule decorator - marks an operation as scheduled
|
||||
* @param cronPattern Cron pattern for scheduling
|
||||
* @param options Additional scheduling options
|
||||
*/
|
||||
export function QueueSchedule(
|
||||
cronPattern: string,
|
||||
options?: {
|
||||
priority?: number;
|
||||
immediately?: boolean;
|
||||
description?: string;
|
||||
}
|
||||
): any {
|
||||
return function (target: any, methodName: string, descriptor?: PropertyDescriptor): any {
|
||||
// Store metadata directly on the class constructor
|
||||
const constructor = target.constructor;
|
||||
|
||||
if (!constructor.__schedules) {
|
||||
constructor.__schedules = [];
|
||||
}
|
||||
constructor.__schedules.push({
|
||||
operation: methodName,
|
||||
cronPattern,
|
||||
...options,
|
||||
});
|
||||
|
||||
return descriptor;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Disabled decorator - marks a handler as disabled for auto-registration
|
||||
* Handlers marked with @Disabled() will be skipped during auto-registration
|
||||
*/
|
||||
export function Disabled() {
|
||||
return function <T extends { new (...args: any[]): {} }>(target: T, _context?: any) {
|
||||
// Store disabled flag on the constructor
|
||||
(target as any).__disabled = true;
|
||||
|
||||
return target;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Combined decorator for scheduled operations
|
||||
* Automatically creates both an operation and a schedule
|
||||
* @param name Operation name
|
||||
* @param cronPattern Cron pattern for scheduling
|
||||
* @param options Schedule options
|
||||
*/
|
||||
export function ScheduledOperation(
|
||||
name: string,
|
||||
cronPattern: string,
|
||||
options?: {
|
||||
priority?: number;
|
||||
immediately?: boolean;
|
||||
description?: string;
|
||||
}
|
||||
): any {
|
||||
return function (target: any, methodName: string, descriptor?: PropertyDescriptor): any {
|
||||
// Apply both decorators
|
||||
Operation(name)(target, methodName, descriptor);
|
||||
QueueSchedule(cronPattern, options)(target, methodName, descriptor);
|
||||
return descriptor;
|
||||
};
|
||||
}
|
||||
|
||||
// Future event decorators - commented for now
|
||||
// export function EventListener(eventName: string) {
|
||||
// return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
|
||||
// if (!target.constructor.__eventListeners) {
|
||||
// target.constructor.__eventListeners = [];
|
||||
// }
|
||||
// target.constructor.__eventListeners.push({
|
||||
// eventName,
|
||||
// method: propertyName,
|
||||
// });
|
||||
// return descriptor;
|
||||
// };
|
||||
// }
|
||||
|
||||
// export function EventPublisher(eventName: string) {
|
||||
// return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
|
||||
// if (!target.constructor.__eventPublishers) {
|
||||
// target.constructor.__eventPublishers = [];
|
||||
// }
|
||||
// target.constructor.__eventPublishers.push({
|
||||
// eventName,
|
||||
// method: propertyName,
|
||||
// });
|
||||
// return descriptor;
|
||||
// };
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -1,31 +1,37 @@
|
|||
// Base handler classes
|
||||
export { BaseHandler, ScheduledHandler } from './base/BaseHandler';
|
||||
|
||||
// Handler registry (re-exported from types to avoid circular deps)
|
||||
export { handlerRegistry } from '@stock-bot/types';
|
||||
|
||||
// Types
|
||||
export type {
|
||||
ExecutionContext,
|
||||
IHandler,
|
||||
JobHandler,
|
||||
ScheduledJob,
|
||||
HandlerConfig,
|
||||
HandlerConfigWithSchedule,
|
||||
TypedJobHandler,
|
||||
HandlerMetadata,
|
||||
OperationMetadata,
|
||||
} from './types/types';
|
||||
|
||||
export type { IServiceContainer } from './types/service-container';
|
||||
|
||||
export { createJobHandler } from './types/types';
|
||||
|
||||
// Decorators
|
||||
export { Handler, Operation, QueueSchedule, ScheduledOperation, Disabled } from './decorators/decorators';
|
||||
|
||||
// Auto-registration utilities
|
||||
export { autoRegisterHandlers, createAutoHandlerRegistry } from './registry/auto-register';
|
||||
|
||||
// Future exports - commented for now
|
||||
// export { EventListener, EventPublisher } from './decorators/decorators';
|
||||
// Base handler classes
|
||||
export { BaseHandler, ScheduledHandler } from './base/BaseHandler';
|
||||
|
||||
// Handler registry (re-exported from types to avoid circular deps)
|
||||
export { handlerRegistry } from '@stock-bot/types';
|
||||
|
||||
// Types
|
||||
export type {
|
||||
ExecutionContext,
|
||||
IHandler,
|
||||
JobHandler,
|
||||
ScheduledJob,
|
||||
HandlerConfig,
|
||||
HandlerConfigWithSchedule,
|
||||
TypedJobHandler,
|
||||
HandlerMetadata,
|
||||
OperationMetadata,
|
||||
} from './types/types';
|
||||
|
||||
export type { IServiceContainer } from './types/service-container';
|
||||
|
||||
export { createJobHandler } from './types/types';
|
||||
|
||||
// Decorators
|
||||
export {
|
||||
Handler,
|
||||
Operation,
|
||||
QueueSchedule,
|
||||
ScheduledOperation,
|
||||
Disabled,
|
||||
} from './decorators/decorators';
|
||||
|
||||
// Auto-registration utilities
|
||||
export { autoRegisterHandlers, createAutoHandlerRegistry } from './registry/auto-register';
|
||||
|
||||
// Future exports - commented for now
|
||||
// export { EventListener, EventPublisher } from './decorators/decorators';
|
||||
|
|
|
|||
|
|
@ -1,191 +1,193 @@
|
|||
import { getLogger } from '@stock-bot/logger';
|
||||
import type { JobHandler, HandlerConfig, HandlerConfigWithSchedule, ScheduledJob } from '../types/types';
|
||||
|
||||
const logger = getLogger('handler-registry');
|
||||
|
||||
class HandlerRegistry {
|
||||
private handlers = new Map<string, HandlerConfig>();
|
||||
private handlerSchedules = new Map<string, ScheduledJob[]>();
|
||||
|
||||
/**
|
||||
* Register a handler with its operations (simple config)
|
||||
*/
|
||||
register(handlerName: string, config: HandlerConfig): void {
|
||||
logger.info(`Registering handler: ${handlerName}`, {
|
||||
operations: Object.keys(config),
|
||||
});
|
||||
|
||||
this.handlers.set(handlerName, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a handler with operations and scheduled jobs (full config)
|
||||
*/
|
||||
registerWithSchedule(config: HandlerConfigWithSchedule): void {
|
||||
logger.info(`Registering handler with schedule: ${config.name}`, {
|
||||
operations: Object.keys(config.operations),
|
||||
scheduledJobs: config.scheduledJobs?.length || 0,
|
||||
});
|
||||
|
||||
this.handlers.set(config.name, config.operations);
|
||||
|
||||
if (config.scheduledJobs && config.scheduledJobs.length > 0) {
|
||||
this.handlerSchedules.set(config.name, config.scheduledJobs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a handler for a specific handler and operation
|
||||
*/
|
||||
getHandler(handler: string, operation: string): JobHandler | null {
|
||||
const handlerConfig = this.handlers.get(handler);
|
||||
if (!handlerConfig) {
|
||||
logger.warn(`Handler not found: ${handler}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const jobHandler = handlerConfig[operation];
|
||||
if (!jobHandler) {
|
||||
logger.warn(`Operation not found: ${handler}:${operation}`, {
|
||||
availableOperations: Object.keys(handlerConfig),
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
return jobHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all scheduled jobs from all handlers
|
||||
*/
|
||||
getAllScheduledJobs(): Array<{ handler: string; job: ScheduledJob }> {
|
||||
const allJobs: Array<{ handler: string; job: ScheduledJob }> = [];
|
||||
|
||||
for (const [handlerName, jobs] of this.handlerSchedules) {
|
||||
for (const job of jobs) {
|
||||
allJobs.push({
|
||||
handler: handlerName,
|
||||
job,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return allJobs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get scheduled jobs for a specific handler
|
||||
*/
|
||||
getScheduledJobs(handler: string): ScheduledJob[] {
|
||||
return this.handlerSchedules.get(handler) || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a handler has scheduled jobs
|
||||
*/
|
||||
hasScheduledJobs(handler: string): boolean {
|
||||
return this.handlerSchedules.has(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered handlers with their configurations
|
||||
*/
|
||||
getHandlerConfigs(): Array<{ name: string; operations: string[]; scheduledJobs: number }> {
|
||||
return Array.from(this.handlers.keys()).map(name => ({
|
||||
name,
|
||||
operations: Object.keys(this.handlers.get(name) || {}),
|
||||
scheduledJobs: this.handlerSchedules.get(name)?.length || 0,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all handlers with their full configurations for queue manager registration
|
||||
*/
|
||||
getAllHandlers(): Map<string, { operations: HandlerConfig; scheduledJobs?: ScheduledJob[] }> {
|
||||
const result = new Map<
|
||||
string,
|
||||
{ operations: HandlerConfig; scheduledJobs?: ScheduledJob[] }
|
||||
>();
|
||||
|
||||
for (const [name, operations] of this.handlers) {
|
||||
const scheduledJobs = this.handlerSchedules.get(name);
|
||||
result.set(name, {
|
||||
operations,
|
||||
scheduledJobs,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered handlers
|
||||
*/
|
||||
getHandlers(): string[] {
|
||||
return Array.from(this.handlers.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get operations for a specific handler
|
||||
*/
|
||||
getOperations(handler: string): string[] {
|
||||
const handlerConfig = this.handlers.get(handler);
|
||||
return handlerConfig ? Object.keys(handlerConfig) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a handler exists
|
||||
*/
|
||||
hasHandler(handler: string): boolean {
|
||||
return this.handlers.has(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a handler has a specific operation
|
||||
*/
|
||||
hasOperation(handler: string, operation: string): boolean {
|
||||
const handlerConfig = this.handlers.get(handler);
|
||||
return handlerConfig ? operation in handlerConfig : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a handler
|
||||
*/
|
||||
unregister(handler: string): boolean {
|
||||
this.handlerSchedules.delete(handler);
|
||||
return this.handlers.delete(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all handlers
|
||||
*/
|
||||
clear(): void {
|
||||
this.handlers.clear();
|
||||
this.handlerSchedules.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get registry statistics
|
||||
*/
|
||||
getStats(): { handlers: number; totalOperations: number; totalScheduledJobs: number } {
|
||||
let totalOperations = 0;
|
||||
let totalScheduledJobs = 0;
|
||||
|
||||
for (const config of this.handlers.values()) {
|
||||
totalOperations += Object.keys(config).length;
|
||||
}
|
||||
|
||||
for (const jobs of this.handlerSchedules.values()) {
|
||||
totalScheduledJobs += jobs.length;
|
||||
}
|
||||
|
||||
return {
|
||||
handlers: this.handlers.size,
|
||||
totalOperations,
|
||||
totalScheduledJobs,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const handlerRegistry = new HandlerRegistry();
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
import type {
|
||||
HandlerConfig,
|
||||
HandlerConfigWithSchedule,
|
||||
JobHandler,
|
||||
ScheduledJob,
|
||||
} from '../types/types';
|
||||
|
||||
const logger = getLogger('handler-registry');
|
||||
|
||||
class HandlerRegistry {
|
||||
private handlers = new Map<string, HandlerConfig>();
|
||||
private handlerSchedules = new Map<string, ScheduledJob[]>();
|
||||
|
||||
/**
|
||||
* Register a handler with its operations (simple config)
|
||||
*/
|
||||
register(handlerName: string, config: HandlerConfig): void {
|
||||
logger.info(`Registering handler: ${handlerName}`, {
|
||||
operations: Object.keys(config),
|
||||
});
|
||||
|
||||
this.handlers.set(handlerName, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a handler with operations and scheduled jobs (full config)
|
||||
*/
|
||||
registerWithSchedule(config: HandlerConfigWithSchedule): void {
|
||||
logger.info(`Registering handler with schedule: ${config.name}`, {
|
||||
operations: Object.keys(config.operations),
|
||||
scheduledJobs: config.scheduledJobs?.length || 0,
|
||||
});
|
||||
|
||||
this.handlers.set(config.name, config.operations);
|
||||
|
||||
if (config.scheduledJobs && config.scheduledJobs.length > 0) {
|
||||
this.handlerSchedules.set(config.name, config.scheduledJobs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a handler for a specific handler and operation
|
||||
*/
|
||||
getHandler(handler: string, operation: string): JobHandler | null {
|
||||
const handlerConfig = this.handlers.get(handler);
|
||||
if (!handlerConfig) {
|
||||
logger.warn(`Handler not found: ${handler}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const jobHandler = handlerConfig[operation];
|
||||
if (!jobHandler) {
|
||||
logger.warn(`Operation not found: ${handler}:${operation}`, {
|
||||
availableOperations: Object.keys(handlerConfig),
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
return jobHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all scheduled jobs from all handlers
|
||||
*/
|
||||
getAllScheduledJobs(): Array<{ handler: string; job: ScheduledJob }> {
|
||||
const allJobs: Array<{ handler: string; job: ScheduledJob }> = [];
|
||||
|
||||
for (const [handlerName, jobs] of this.handlerSchedules) {
|
||||
for (const job of jobs) {
|
||||
allJobs.push({
|
||||
handler: handlerName,
|
||||
job,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return allJobs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get scheduled jobs for a specific handler
|
||||
*/
|
||||
getScheduledJobs(handler: string): ScheduledJob[] {
|
||||
return this.handlerSchedules.get(handler) || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a handler has scheduled jobs
|
||||
*/
|
||||
hasScheduledJobs(handler: string): boolean {
|
||||
return this.handlerSchedules.has(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered handlers with their configurations
|
||||
*/
|
||||
getHandlerConfigs(): Array<{ name: string; operations: string[]; scheduledJobs: number }> {
|
||||
return Array.from(this.handlers.keys()).map(name => ({
|
||||
name,
|
||||
operations: Object.keys(this.handlers.get(name) || {}),
|
||||
scheduledJobs: this.handlerSchedules.get(name)?.length || 0,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all handlers with their full configurations for queue manager registration
|
||||
*/
|
||||
getAllHandlers(): Map<string, { operations: HandlerConfig; scheduledJobs?: ScheduledJob[] }> {
|
||||
const result = new Map<string, { operations: HandlerConfig; scheduledJobs?: ScheduledJob[] }>();
|
||||
|
||||
for (const [name, operations] of this.handlers) {
|
||||
const scheduledJobs = this.handlerSchedules.get(name);
|
||||
result.set(name, {
|
||||
operations,
|
||||
scheduledJobs,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered handlers
|
||||
*/
|
||||
getHandlers(): string[] {
|
||||
return Array.from(this.handlers.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get operations for a specific handler
|
||||
*/
|
||||
getOperations(handler: string): string[] {
|
||||
const handlerConfig = this.handlers.get(handler);
|
||||
return handlerConfig ? Object.keys(handlerConfig) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a handler exists
|
||||
*/
|
||||
hasHandler(handler: string): boolean {
|
||||
return this.handlers.has(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a handler has a specific operation
|
||||
*/
|
||||
hasOperation(handler: string, operation: string): boolean {
|
||||
const handlerConfig = this.handlers.get(handler);
|
||||
return handlerConfig ? operation in handlerConfig : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a handler
|
||||
*/
|
||||
unregister(handler: string): boolean {
|
||||
this.handlerSchedules.delete(handler);
|
||||
return this.handlers.delete(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all handlers
|
||||
*/
|
||||
clear(): void {
|
||||
this.handlers.clear();
|
||||
this.handlerSchedules.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get registry statistics
|
||||
*/
|
||||
getStats(): { handlers: number; totalOperations: number; totalScheduledJobs: number } {
|
||||
let totalOperations = 0;
|
||||
let totalScheduledJobs = 0;
|
||||
|
||||
for (const config of this.handlers.values()) {
|
||||
totalOperations += Object.keys(config).length;
|
||||
}
|
||||
|
||||
for (const jobs of this.handlerSchedules.values()) {
|
||||
totalScheduledJobs += jobs.length;
|
||||
}
|
||||
|
||||
return {
|
||||
handlers: this.handlers.size,
|
||||
totalOperations,
|
||||
totalScheduledJobs,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const handlerRegistry = new HandlerRegistry();
|
||||
|
|
|
|||
|
|
@ -1,180 +1,188 @@
|
|||
/**
|
||||
* Auto-registration utilities for handlers
|
||||
* Automatically discovers and registers handlers based on file patterns
|
||||
*/
|
||||
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
import type { IServiceContainer } from '../types/service-container';
|
||||
import { BaseHandler } from '../base/BaseHandler';
|
||||
import { readdirSync, statSync } from 'fs';
|
||||
import { join, relative } from 'path';
|
||||
|
||||
const logger = getLogger('handler-auto-register');
|
||||
|
||||
/**
|
||||
* Recursively find all handler files in a directory
|
||||
*/
|
||||
function findHandlerFiles(dir: string, pattern = '.handler.'): string[] {
|
||||
const files: string[] = [];
|
||||
|
||||
function scan(currentDir: string) {
|
||||
const entries = readdirSync(currentDir);
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(currentDir, entry);
|
||||
const stat = statSync(fullPath);
|
||||
|
||||
if (stat.isDirectory() && !entry.startsWith('.') && entry !== 'node_modules') {
|
||||
scan(fullPath);
|
||||
} else if (stat.isFile() && entry.includes(pattern) && entry.endsWith('.ts')) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scan(dir);
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract handler classes from a module
|
||||
*/
|
||||
function extractHandlerClasses(module: any): Array<new (services: IServiceContainer) => BaseHandler> {
|
||||
const handlers: Array<new (services: IServiceContainer) => BaseHandler> = [];
|
||||
|
||||
for (const key of Object.keys(module)) {
|
||||
const exported = module[key];
|
||||
|
||||
// Check if it's a class that extends BaseHandler
|
||||
if (
|
||||
typeof exported === 'function' &&
|
||||
exported.prototype &&
|
||||
exported.prototype instanceof BaseHandler
|
||||
) {
|
||||
handlers.push(exported);
|
||||
}
|
||||
}
|
||||
|
||||
return handlers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-register all handlers in a directory
|
||||
* @param directory The directory to scan for handlers
|
||||
* @param services The service container to inject into handlers
|
||||
* @param options Configuration options
|
||||
*/
|
||||
export async function autoRegisterHandlers(
|
||||
directory: string,
|
||||
services: IServiceContainer,
|
||||
options: {
|
||||
pattern?: string;
|
||||
exclude?: string[];
|
||||
dryRun?: boolean;
|
||||
} = {}
|
||||
): Promise<{ registered: string[]; failed: string[] }> {
|
||||
const { pattern = '.handler.', exclude = [], dryRun = false } = options;
|
||||
const registered: string[] = [];
|
||||
const failed: string[] = [];
|
||||
|
||||
try {
|
||||
logger.info('Starting auto-registration of handlers', { directory, pattern });
|
||||
|
||||
// Find all handler files
|
||||
const handlerFiles = findHandlerFiles(directory, pattern);
|
||||
logger.debug(`Found ${handlerFiles.length} handler files`, { files: handlerFiles });
|
||||
|
||||
// Process each handler file
|
||||
for (const file of handlerFiles) {
|
||||
const relativePath = relative(directory, file);
|
||||
|
||||
// Skip excluded files
|
||||
if (exclude.some(ex => relativePath.includes(ex))) {
|
||||
logger.debug(`Skipping excluded file: ${relativePath}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// Import the module
|
||||
const module = await import(file);
|
||||
const handlerClasses = extractHandlerClasses(module);
|
||||
|
||||
if (handlerClasses.length === 0) {
|
||||
logger.warn(`No handler classes found in ${relativePath}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Register each handler class
|
||||
for (const HandlerClass of handlerClasses) {
|
||||
const handlerName = HandlerClass.name;
|
||||
|
||||
// Check if handler is disabled
|
||||
if ((HandlerClass as any).__disabled) {
|
||||
logger.info(`Skipping disabled handler: ${handlerName} from ${relativePath}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dryRun) {
|
||||
logger.info(`[DRY RUN] Would register handler: ${handlerName} from ${relativePath}`);
|
||||
registered.push(handlerName);
|
||||
} else {
|
||||
logger.info(`Registering handler: ${handlerName} from ${relativePath}`);
|
||||
|
||||
// Create instance and register
|
||||
const handler = new HandlerClass(services);
|
||||
handler.register();
|
||||
|
||||
registered.push(handlerName);
|
||||
logger.info(`Successfully registered handler: ${handlerName}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to process handler file: ${relativePath}`, { error });
|
||||
failed.push(relativePath);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('Auto-registration complete', {
|
||||
totalFiles: handlerFiles.length,
|
||||
registered: registered.length,
|
||||
failed: failed.length
|
||||
});
|
||||
|
||||
return { registered, failed };
|
||||
} catch (error) {
|
||||
logger.error('Auto-registration failed', { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a handler registry that auto-discovers handlers
|
||||
*/
|
||||
export function createAutoHandlerRegistry(services: IServiceContainer) {
|
||||
return {
|
||||
/**
|
||||
* Register all handlers from a directory
|
||||
*/
|
||||
async registerDirectory(directory: string, options?: Parameters<typeof autoRegisterHandlers>[2]) {
|
||||
return autoRegisterHandlers(directory, services, options);
|
||||
},
|
||||
|
||||
/**
|
||||
* Register handlers from multiple directories
|
||||
*/
|
||||
async registerDirectories(directories: string[], options?: Parameters<typeof autoRegisterHandlers>[2]) {
|
||||
const results = {
|
||||
registered: [] as string[],
|
||||
failed: [] as string[]
|
||||
};
|
||||
|
||||
for (const dir of directories) {
|
||||
const result = await autoRegisterHandlers(dir, services, options);
|
||||
results.registered.push(...result.registered);
|
||||
results.failed.push(...result.failed);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Auto-registration utilities for handlers
|
||||
* Automatically discovers and registers handlers based on file patterns
|
||||
*/
|
||||
|
||||
import { readdirSync, statSync } from 'fs';
|
||||
import { join, relative } from 'path';
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
import { BaseHandler } from '../base/BaseHandler';
|
||||
import type { IServiceContainer } from '../types/service-container';
|
||||
|
||||
const logger = getLogger('handler-auto-register');
|
||||
|
||||
/**
|
||||
* Recursively find all handler files in a directory
|
||||
*/
|
||||
function findHandlerFiles(dir: string, pattern = '.handler.'): string[] {
|
||||
const files: string[] = [];
|
||||
|
||||
function scan(currentDir: string) {
|
||||
const entries = readdirSync(currentDir);
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(currentDir, entry);
|
||||
const stat = statSync(fullPath);
|
||||
|
||||
if (stat.isDirectory() && !entry.startsWith('.') && entry !== 'node_modules') {
|
||||
scan(fullPath);
|
||||
} else if (stat.isFile() && entry.includes(pattern) && entry.endsWith('.ts')) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scan(dir);
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract handler classes from a module
|
||||
*/
|
||||
function extractHandlerClasses(
|
||||
module: any
|
||||
): Array<new (services: IServiceContainer) => BaseHandler> {
|
||||
const handlers: Array<new (services: IServiceContainer) => BaseHandler> = [];
|
||||
|
||||
for (const key of Object.keys(module)) {
|
||||
const exported = module[key];
|
||||
|
||||
// Check if it's a class that extends BaseHandler
|
||||
if (
|
||||
typeof exported === 'function' &&
|
||||
exported.prototype &&
|
||||
exported.prototype instanceof BaseHandler
|
||||
) {
|
||||
handlers.push(exported);
|
||||
}
|
||||
}
|
||||
|
||||
return handlers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-register all handlers in a directory
|
||||
* @param directory The directory to scan for handlers
|
||||
* @param services The service container to inject into handlers
|
||||
* @param options Configuration options
|
||||
*/
|
||||
export async function autoRegisterHandlers(
|
||||
directory: string,
|
||||
services: IServiceContainer,
|
||||
options: {
|
||||
pattern?: string;
|
||||
exclude?: string[];
|
||||
dryRun?: boolean;
|
||||
} = {}
|
||||
): Promise<{ registered: string[]; failed: string[] }> {
|
||||
const { pattern = '.handler.', exclude = [], dryRun = false } = options;
|
||||
const registered: string[] = [];
|
||||
const failed: string[] = [];
|
||||
|
||||
try {
|
||||
logger.info('Starting auto-registration of handlers', { directory, pattern });
|
||||
|
||||
// Find all handler files
|
||||
const handlerFiles = findHandlerFiles(directory, pattern);
|
||||
logger.debug(`Found ${handlerFiles.length} handler files`, { files: handlerFiles });
|
||||
|
||||
// Process each handler file
|
||||
for (const file of handlerFiles) {
|
||||
const relativePath = relative(directory, file);
|
||||
|
||||
// Skip excluded files
|
||||
if (exclude.some(ex => relativePath.includes(ex))) {
|
||||
logger.debug(`Skipping excluded file: ${relativePath}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// Import the module
|
||||
const module = await import(file);
|
||||
const handlerClasses = extractHandlerClasses(module);
|
||||
|
||||
if (handlerClasses.length === 0) {
|
||||
logger.warn(`No handler classes found in ${relativePath}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Register each handler class
|
||||
for (const HandlerClass of handlerClasses) {
|
||||
const handlerName = HandlerClass.name;
|
||||
|
||||
// Check if handler is disabled
|
||||
if ((HandlerClass as any).__disabled) {
|
||||
logger.info(`Skipping disabled handler: ${handlerName} from ${relativePath}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dryRun) {
|
||||
logger.info(`[DRY RUN] Would register handler: ${handlerName} from ${relativePath}`);
|
||||
registered.push(handlerName);
|
||||
} else {
|
||||
logger.info(`Registering handler: ${handlerName} from ${relativePath}`);
|
||||
|
||||
// Create instance and register
|
||||
const handler = new HandlerClass(services);
|
||||
handler.register();
|
||||
|
||||
registered.push(handlerName);
|
||||
logger.info(`Successfully registered handler: ${handlerName}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to process handler file: ${relativePath}`, { error });
|
||||
failed.push(relativePath);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('Auto-registration complete', {
|
||||
totalFiles: handlerFiles.length,
|
||||
registered: registered.length,
|
||||
failed: failed.length,
|
||||
});
|
||||
|
||||
return { registered, failed };
|
||||
} catch (error) {
|
||||
logger.error('Auto-registration failed', { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a handler registry that auto-discovers handlers
|
||||
*/
|
||||
export function createAutoHandlerRegistry(services: IServiceContainer) {
|
||||
return {
|
||||
/**
|
||||
* Register all handlers from a directory
|
||||
*/
|
||||
async registerDirectory(
|
||||
directory: string,
|
||||
options?: Parameters<typeof autoRegisterHandlers>[2]
|
||||
) {
|
||||
return autoRegisterHandlers(directory, services, options);
|
||||
},
|
||||
|
||||
/**
|
||||
* Register handlers from multiple directories
|
||||
*/
|
||||
async registerDirectories(
|
||||
directories: string[],
|
||||
options?: Parameters<typeof autoRegisterHandlers>[2]
|
||||
) {
|
||||
const results = {
|
||||
registered: [] as string[],
|
||||
failed: [] as string[],
|
||||
};
|
||||
|
||||
for (const dir of directories) {
|
||||
const result = await autoRegisterHandlers(dir, services, options);
|
||||
results.registered.push(...result.registered);
|
||||
results.failed.push(...result.failed);
|
||||
}
|
||||
|
||||
return results;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,27 @@
|
|||
/**
|
||||
* Universal Service Container for Handlers
|
||||
* Simple, comprehensive container with all services available
|
||||
*/
|
||||
|
||||
import type { ProxyManager } from '@stock-bot/proxy';
|
||||
|
||||
/**
|
||||
* Universal service container with all common services
|
||||
* Designed to work across different service contexts (data-ingestion, processing, etc.)
|
||||
*/
|
||||
export interface IServiceContainer {
|
||||
// Core infrastructure
|
||||
readonly logger: any; // Logger instance
|
||||
readonly cache?: any; // Cache provider (Redis/Dragonfly) - optional
|
||||
readonly queue?: any; // Queue manager (BullMQ) - optional
|
||||
readonly proxy?: ProxyManager; // Proxy manager service - optional (depends on cache)
|
||||
readonly browser?: any; // Browser automation (Playwright)
|
||||
|
||||
// Database clients - all optional to support selective enabling
|
||||
readonly mongodb?: any; // MongoDB client
|
||||
readonly postgres?: any; // PostgreSQL client
|
||||
readonly questdb?: any; // QuestDB client (time-series)
|
||||
|
||||
// Optional extensions for future use
|
||||
readonly custom?: Record<string, any>;
|
||||
}
|
||||
/**
|
||||
* Universal Service Container for Handlers
|
||||
* Simple, comprehensive container with all services available
|
||||
*/
|
||||
|
||||
import type { ProxyManager } from '@stock-bot/proxy';
|
||||
|
||||
/**
|
||||
* Universal service container with all common services
|
||||
* Designed to work across different service contexts (data-ingestion, processing, etc.)
|
||||
*/
|
||||
export interface IServiceContainer {
|
||||
// Core infrastructure
|
||||
readonly logger: any; // Logger instance
|
||||
readonly cache?: any; // Cache provider (Redis/Dragonfly) - optional
|
||||
readonly queue?: any; // Queue manager (BullMQ) - optional
|
||||
readonly proxy?: ProxyManager; // Proxy manager service - optional (depends on cache)
|
||||
readonly browser?: any; // Browser automation (Playwright)
|
||||
|
||||
// Database clients - all optional to support selective enabling
|
||||
readonly mongodb?: any; // MongoDB client
|
||||
readonly postgres?: any; // PostgreSQL client
|
||||
readonly questdb?: any; // QuestDB client (time-series)
|
||||
|
||||
// Optional extensions for future use
|
||||
readonly custom?: Record<string, any>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
// Re-export all handler types from the shared types package
|
||||
export type {
|
||||
ExecutionContext,
|
||||
HandlerConfig,
|
||||
HandlerConfigWithSchedule,
|
||||
HandlerMetadata,
|
||||
IHandler,
|
||||
JobHandler,
|
||||
OperationMetadata,
|
||||
ScheduledJob,
|
||||
TypedJobHandler,
|
||||
} from '@stock-bot/types';
|
||||
|
||||
export { createJobHandler } from '@stock-bot/types';
|
||||
// Re-export all handler types from the shared types package
|
||||
export type {
|
||||
ExecutionContext,
|
||||
HandlerConfig,
|
||||
HandlerConfigWithSchedule,
|
||||
HandlerMetadata,
|
||||
IHandler,
|
||||
JobHandler,
|
||||
OperationMetadata,
|
||||
ScheduledJob,
|
||||
TypedJobHandler,
|
||||
} from '@stock-bot/types';
|
||||
|
||||
export { createJobHandler } from '@stock-bot/types';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue