huge refactor to remove depenencie hell and add typesafe container

This commit is contained in:
Boki 2025-06-24 09:37:51 -04:00
parent 28b9822d55
commit 843a7b9b9b
148 changed files with 3603 additions and 2378 deletions

View file

@ -5,12 +5,14 @@
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { getLogger, setLoggerConfig, shutdownLoggers, type Logger } from '@stock-bot/logger';
import { Shutdown } from '@stock-bot/shutdown';
import type { BaseAppConfig, UnifiedAppConfig } from '@stock-bot/config';
import { toUnifiedConfig } from '@stock-bot/config';
import { getLogger, setLoggerConfig, shutdownLoggers, type Logger } from '@stock-bot/logger';
import { Shutdown } from '@stock-bot/shutdown';
import type { IServiceContainer } from '@stock-bot/types';
import type { ServiceContainer } from './awilix-container';
import type { HandlerRegistry } from '@stock-bot/handler-registry';
import type { ServiceDefinitions } from './container/types';
import type { AwilixContainer } from 'awilix';
/**
* Configuration for ServiceApplication
@ -18,26 +20,26 @@ import type { ServiceContainer } from './awilix-container';
export interface ServiceApplicationConfig {
/** Service name for logging and identification */
serviceName: string;
/** CORS configuration - if not provided, uses permissive defaults */
corsConfig?: Parameters<typeof cors>[0];
/** Whether to enable handler initialization */
enableHandlers?: boolean;
/** Whether to enable scheduled job creation */
enableScheduledJobs?: boolean;
/** Custom shutdown timeout in milliseconds */
shutdownTimeout?: number;
/** Service metadata for info endpoint */
serviceMetadata?: {
version?: string;
description?: string;
endpoints?: Record<string, string>;
};
/** Whether to add a basic info endpoint at root */
addInfoEndpoint?: boolean;
}
@ -48,16 +50,16 @@ export interface ServiceApplicationConfig {
export interface ServiceLifecycleHooks {
/** Called after container is created but before routes */
onContainerReady?: (container: IServiceContainer) => Promise<void> | void;
/** Called after app is created but before routes are mounted */
onAppReady?: (app: Hono, container: IServiceContainer) => Promise<void> | void;
/** Called after routes are mounted but before server starts */
onBeforeStart?: (app: Hono, container: IServiceContainer) => Promise<void> | void;
/** Called after successful server startup */
onStarted?: (port: number) => Promise<void> | void;
/** Called during shutdown before cleanup */
onBeforeShutdown?: () => Promise<void> | void;
}
@ -70,13 +72,13 @@ export class ServiceApplication {
private serviceConfig: ServiceApplicationConfig;
private hooks: ServiceLifecycleHooks;
private logger: Logger;
private container: ServiceContainer | null = null;
private container: AwilixContainer<ServiceDefinitions> | null = null;
private serviceContainer: IServiceContainer | null = null;
private app: Hono | null = null;
private server: ReturnType<typeof Bun.serve> | null = null;
private shutdown: Shutdown;
constructor(
config: BaseAppConfig | UnifiedAppConfig,
serviceConfig: ServiceApplicationConfig,
@ -84,12 +86,12 @@ export class ServiceApplication {
) {
// Convert to unified config
this.config = toUnifiedConfig(config);
// Ensure service name is set in config
if (!this.config.service.serviceName) {
this.config.service.serviceName = serviceConfig.serviceName;
}
this.serviceConfig = {
shutdownTimeout: 15000,
enableHandlers: false,
@ -98,17 +100,17 @@ export class ServiceApplication {
...serviceConfig,
};
this.hooks = hooks;
// Initialize logger configuration
this.configureLogger();
this.logger = getLogger(this.serviceConfig.serviceName);
// Initialize shutdown manager
this.shutdown = Shutdown.getInstance({
timeout: this.serviceConfig.shutdownTimeout
this.shutdown = Shutdown.getInstance({
timeout: this.serviceConfig.shutdownTimeout,
});
}
/**
* Configure logger based on application config
*/
@ -123,13 +125,13 @@ export class ServiceApplication {
});
}
}
/**
* Create and configure Hono application with CORS
*/
private createApp(): Hono {
const app = new Hono();
// Add CORS middleware with service-specific or default configuration
const corsConfig = this.serviceConfig.corsConfig || {
origin: '*',
@ -137,9 +139,9 @@ export class ServiceApplication {
allowHeaders: ['Content-Type', 'Authorization'],
credentials: false,
};
app.use('*', cors(corsConfig));
// Add basic info endpoint if enabled
if (this.serviceConfig.addInfoEndpoint) {
const metadata = this.serviceConfig.serviceMetadata || {};
@ -154,10 +156,10 @@ export class ServiceApplication {
});
});
}
return app;
}
/**
* Register graceful shutdown handlers
*/
@ -177,7 +179,7 @@ export class ServiceApplication {
}
}, 'Queue System');
}
// Priority 1: HTTP Server (high priority)
this.shutdown.onShutdownHigh(async () => {
if (this.server) {
@ -190,7 +192,7 @@ export class ServiceApplication {
}
}
}, 'HTTP Server');
// Custom shutdown hook
if (this.hooks.onBeforeShutdown) {
this.shutdown.onShutdownHigh(async () => {
@ -201,7 +203,7 @@ export class ServiceApplication {
}
}, 'Custom Shutdown');
}
// Priority 2: Services and connections (medium priority)
this.shutdown.onShutdownMedium(async () => {
this.logger.info('Disposing services and connections...');
@ -212,24 +214,24 @@ export class ServiceApplication {
if (mongoClient?.disconnect) {
await mongoClient.disconnect();
}
const postgresClient = this.container.resolve('postgresClient');
if (postgresClient?.disconnect) {
await postgresClient.disconnect();
}
const questdbClient = this.container.resolve('questdbClient');
if (questdbClient?.disconnect) {
await questdbClient.disconnect();
}
this.logger.info('All services disposed successfully');
}
} catch (error) {
this.logger.error('Error disposing services', { error });
}
}, 'Services');
// Priority 3: Logger shutdown (lowest priority - runs last)
this.shutdown.onShutdownLow(async () => {
try {
@ -241,62 +243,62 @@ export class ServiceApplication {
}
}, 'Loggers');
}
/**
* Start the service with full initialization
*/
async start(
containerFactory: (config: UnifiedAppConfig) => Promise<ServiceContainer>,
containerFactory: (config: UnifiedAppConfig) => Promise<AwilixContainer<ServiceDefinitions>>,
routeFactory: (container: IServiceContainer) => Hono,
handlerInitializer?: (container: IServiceContainer) => Promise<void>
): Promise<void> {
this.logger.info(`Initializing ${this.serviceConfig.serviceName} service...`);
try {
// Create and initialize container
this.logger.debug('Creating DI container...');
// Config already has service name from constructor
this.container = await containerFactory(this.config);
this.serviceContainer = this.container.resolve('serviceContainer');
this.serviceContainer = this.container!.resolve('serviceContainer');
this.logger.info('DI container created and initialized');
// Call container ready hook
if (this.hooks.onContainerReady) {
await this.hooks.onContainerReady(this.serviceContainer);
}
// Create Hono application
this.app = this.createApp();
// Call app ready hook
if (this.hooks.onAppReady) {
await this.hooks.onAppReady(this.app, this.serviceContainer);
}
// Initialize handlers if enabled
if (this.serviceConfig.enableHandlers && handlerInitializer) {
this.logger.debug('Initializing handlers...');
await handlerInitializer(this.serviceContainer);
this.logger.info('Handlers initialized');
}
// Create and mount routes
const routes = routeFactory(this.serviceContainer);
this.app.route('/', routes);
// Initialize scheduled jobs if enabled
if (this.serviceConfig.enableScheduledJobs) {
await this.initializeScheduledJobs();
}
// Call before start hook
if (this.hooks.onBeforeStart) {
await this.hooks.onBeforeStart(this.app, this.serviceContainer);
}
// Register shutdown handlers
this.registerShutdownHandlers();
// Start HTTP server
const port = this.config.service.port;
this.server = Bun.serve({
@ -304,14 +306,13 @@ export class ServiceApplication {
fetch: this.app.fetch,
development: this.config.environment === 'development',
});
this.logger.info(`${this.serviceConfig.serviceName} service started on port ${port}`);
// Call started hook
if (this.hooks.onStarted) {
await this.hooks.onStarted(port);
}
} catch (error) {
this.logger.error('DETAILED ERROR:', error);
this.logger.error('Failed to start service', {
@ -322,7 +323,7 @@ export class ServiceApplication {
throw error;
}
}
/**
* Initialize scheduled jobs from handler registry
*/
@ -330,17 +331,17 @@ export class ServiceApplication {
if (!this.container) {
throw new Error('Container not initialized');
}
this.logger.debug('Creating scheduled jobs from registered handlers...');
const { handlerRegistry } = await import('@stock-bot/handlers');
const handlerRegistry = this.container.resolve<HandlerRegistry>('handlerRegistry');
const allHandlers = handlerRegistry.getAllHandlersWithSchedule();
let totalScheduledJobs = 0;
for (const [handlerName, config] of allHandlers) {
if (config.scheduledJobs && config.scheduledJobs.length > 0) {
// Check if this handler belongs to the current service
const ownerService = handlerRegistry.getHandlerService(handlerName);
if (ownerService !== this.config.service.serviceName) {
this.logger.trace('Skipping scheduled jobs for handler from different service', {
handler: handlerName,
@ -349,14 +350,14 @@ export class ServiceApplication {
});
continue;
}
const queueManager = this.container.resolve('queueManager');
if (!queueManager) {
this.logger.error('Queue manager is not initialized, cannot create scheduled jobs');
continue;
}
const queue = queueManager.getQueue(handlerName);
for (const scheduledJob of config.scheduledJobs) {
// Include handler and operation info in job data
const jobData = {
@ -364,7 +365,7 @@ export class ServiceApplication {
operation: scheduledJob.operation,
payload: scheduledJob.payload,
};
// Build job options from scheduled job config
const jobOptions = {
priority: scheduledJob.priority,
@ -373,7 +374,7 @@ export class ServiceApplication {
immediately: scheduledJob.immediately,
},
};
await queue.addScheduledJob(
scheduledJob.operation,
jobData,
@ -392,7 +393,7 @@ export class ServiceApplication {
}
}
this.logger.info('Scheduled jobs created', { totalJobs: totalScheduledJobs });
// Start queue workers
this.logger.debug('Starting queue workers...');
const queueManager = this.container.resolve('queueManager');
@ -401,7 +402,7 @@ export class ServiceApplication {
this.logger.info('Queue workers started');
}
}
/**
* Stop the service gracefully
*/
@ -409,18 +410,18 @@ export class ServiceApplication {
this.logger.info(`Stopping ${this.serviceConfig.serviceName} service...`);
await this.shutdown.shutdown();
}
/**
* Get the service container (for testing or advanced use cases)
*/
getServiceContainer(): IServiceContainer | null {
return this.serviceContainer;
}
/**
* Get the Hono app (for testing or advanced use cases)
*/
getApp(): Hono | null {
return this.app;
}
}
}