cleaner dev experience refactor

This commit is contained in:
Boki 2025-06-22 07:31:00 -04:00
parent 8b17f98845
commit 742e590382
7 changed files with 407 additions and 17 deletions

View file

@ -99,6 +99,52 @@ export abstract class BaseHandler implements IHandler {
};
}
/**
* Helper methods for common operations
*/
/**
* Get a MongoDB collection with type safety
*/
protected collection(name: string) {
return this.mongodb.collection(name);
}
/**
* Set cache with handler-prefixed key
*/
protected async cacheSet(key: string, value: any, ttl?: number): Promise<void> {
return this.cache.set(`${this.handlerName}:${key}`, value, ttl);
}
/**
* Get cache with handler-prefixed key
*/
protected async cacheGet(key: string): Promise<any | null> {
return this.cache.get(`${this.handlerName}:${key}`);
}
/**
* Delete cache with handler-prefixed key
*/
protected async cacheDel(key: string): Promise<void> {
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 });
}
/**
* Event methods - commented for future
*/

View file

@ -76,6 +76,34 @@ export function QueueSchedule(
};
}
/**
* 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) {

View file

@ -22,7 +22,10 @@ export type { IServiceContainer } from './types/service-container';
export { createJobHandler } from './types/types';
// Decorators
export { Handler, Operation, QueueSchedule } from './decorators/decorators';
export { Handler, Operation, QueueSchedule, ScheduledOperation } 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

@ -0,0 +1,174 @@
/**
* 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;
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;
}
};
}