This commit is contained in:
Boki 2025-06-22 17:55:51 -04:00
parent d858222af7
commit 7d9044ab29
202 changed files with 10755 additions and 10972 deletions

View file

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

View file

@ -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;
// };
// }

View file

@ -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';

View file

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

View file

@ -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;
},
};
}

View file

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

View file

@ -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';