unified config

This commit is contained in:
Boki 2025-06-23 11:16:34 -04:00
parent e7c0fe2798
commit 3877902ff4
13 changed files with 856 additions and 476 deletions

View file

@ -15,6 +15,8 @@ export const appConfigSchema = z.object({
queue: queueConfigSchema.optional(),
service: z.object({
name: z.string(),
serviceName: z.string().optional(), // Standard kebab-case service name
port: z.number().optional(),
}).optional(),
});

View file

@ -1,6 +1,7 @@
import { createContainer, InjectionMode, asFunction, type AwilixContainer } from 'awilix';
import type { AppConfig as StockBotAppConfig } from '@stock-bot/config';
import type { AppConfig as StockBotAppConfig, UnifiedAppConfig } from '@stock-bot/config';
import { appConfigSchema, type AppConfig } from '../config/schemas';
import { toUnifiedConfig } from '@stock-bot/config';
import {
registerCoreServices,
registerCacheServices,
@ -12,6 +13,7 @@ import type { ServiceDefinitions, ContainerBuildOptions } from './types';
export class ServiceContainerBuilder {
private config: Partial<AppConfig> = {};
private unifiedConfig: UnifiedAppConfig | null = null;
private options: ContainerBuildOptions = {
enableCache: true,
enableQueue: true,
@ -24,8 +26,10 @@ export class ServiceContainerBuilder {
initializationTimeout: 30000,
};
withConfig(config: AppConfig | StockBotAppConfig): this {
this.config = this.transformStockBotConfig(config);
withConfig(config: AppConfig | StockBotAppConfig | UnifiedAppConfig): this {
// Convert to unified config format
this.unifiedConfig = toUnifiedConfig(config);
this.config = this.transformStockBotConfig(this.unifiedConfig);
return this;
}
@ -72,6 +76,19 @@ export class ServiceContainerBuilder {
}
private applyServiceOptions(config: Partial<AppConfig>): AppConfig {
// Ensure questdb config has the right field names for DI
const questdbConfig = config.questdb ? {
...config.questdb,
influxPort: (config.questdb as any).influxPort || (config.questdb as any).ilpPort || 9009,
} : {
enabled: true,
host: 'localhost',
httpPort: 9000,
pgPort: 8812,
influxPort: 9009,
database: 'questdb',
};
return {
redis: config.redis || {
enabled: this.options.enableCache ?? true,
@ -92,14 +109,7 @@ export class ServiceContainerBuilder {
user: 'postgres',
password: 'postgres',
},
questdb: this.options.enableQuestDB ? (config.questdb || {
enabled: true,
host: 'localhost',
httpPort: 9000,
pgPort: 8812,
influxPort: 9009,
database: 'questdb',
}) : undefined,
questdb: this.options.enableQuestDB ? questdbConfig : undefined,
proxy: this.options.enableProxy ? (config.proxy || { enabled: false, cachePrefix: 'proxy:', ttl: 3600 }) : undefined,
browser: this.options.enableBrowser ? (config.browser || { headless: true, timeout: 30000 }) : undefined,
queue: this.options.enableQueue ? (config.queue || {
@ -115,6 +125,7 @@ export class ServiceContainerBuilder {
removeOnFail: 50,
}
}) : undefined,
service: config.service,
};
}
@ -143,53 +154,27 @@ export class ServiceContainerBuilder {
});
}
private transformStockBotConfig(config: AppConfig | StockBotAppConfig): Partial<AppConfig> {
// If it's already in the new format (has redis AND postgres at top level), return as is
if ('redis' in config && 'postgres' in config && 'mongodb' in config) {
return config as AppConfig;
}
private transformStockBotConfig(config: UnifiedAppConfig): Partial<AppConfig> {
// Unified config already has flat structure, just extract what we need
// Handle questdb field name mapping
const questdb = config.questdb ? {
enabled: config.questdb.enabled || true,
host: config.questdb.host || 'localhost',
httpPort: config.questdb.httpPort || 9000,
pgPort: config.questdb.pgPort || 8812,
influxPort: (config.questdb as any).influxPort || (config.questdb as any).ilpPort || 9009,
database: config.questdb.database || 'questdb',
} : undefined;
// Transform from StockBotAppConfig format
const stockBotConfig = config as StockBotAppConfig;
return {
redis: stockBotConfig.database?.dragonfly ? {
enabled: true,
host: stockBotConfig.database.dragonfly.host || 'localhost',
port: stockBotConfig.database.dragonfly.port || 6379,
password: stockBotConfig.database.dragonfly.password,
db: stockBotConfig.database.dragonfly.db || 0,
} : undefined,
mongodb: stockBotConfig.database?.mongodb ? {
enabled: stockBotConfig.database.mongodb.enabled ?? true,
uri: stockBotConfig.database.mongodb.uri,
database: stockBotConfig.database.mongodb.database,
} : undefined,
postgres: stockBotConfig.database?.postgres ? {
enabled: stockBotConfig.database.postgres.enabled ?? true,
host: stockBotConfig.database.postgres.host,
port: stockBotConfig.database.postgres.port,
database: stockBotConfig.database.postgres.database,
user: stockBotConfig.database.postgres.user,
password: stockBotConfig.database.postgres.password,
} : undefined,
questdb: stockBotConfig.database?.questdb ? {
enabled: true,
host: stockBotConfig.database.questdb.host || 'localhost',
httpPort: stockBotConfig.database.questdb.httpPort || 9000,
pgPort: stockBotConfig.database.questdb.pgPort || 8812,
influxPort: stockBotConfig.database.questdb.ilpPort || 9009,
database: stockBotConfig.database.questdb.database || 'questdb',
} : undefined,
queue: stockBotConfig.queue,
browser: stockBotConfig.browser,
proxy: stockBotConfig.proxy ? {
...{
enabled: false,
cachePrefix: 'proxy:',
ttl: 3600,
},
...stockBotConfig.proxy
} : undefined,
redis: config.redis,
mongodb: config.mongodb,
postgres: config.postgres,
questdb,
queue: config.queue,
browser: config.browser,
proxy: config.proxy,
service: config.service,
};
}
}

View file

@ -11,7 +11,8 @@ export function registerCacheServices(
container.register({
cache: asFunction(() => {
const { createServiceCache } = require('@stock-bot/queue');
const serviceName = config.service?.name || 'unknown';
// Get standardized service name from config
const serviceName = config.service?.serviceName || config.service?.name || 'unknown';
// Create service-specific cache that uses the service's Redis DB
return createServiceCache(serviceName, {
@ -25,7 +26,7 @@ export function registerCacheServices(
// Also provide global cache for shared data
globalCache: asFunction(() => {
const { createServiceCache } = require('@stock-bot/queue');
const serviceName = config.service?.name || 'unknown';
const serviceName = config.service?.serviceName || config.service?.name || 'unknown';
return createServiceCache(serviceName, {
host: config.redis.host,

View file

@ -53,7 +53,7 @@ export function registerApplicationServices(
queueManager: asFunction(({ logger }) => {
const { SmartQueueManager } = require('@stock-bot/queue');
const queueConfig = {
serviceName: config.service?.name || 'unknown',
serviceName: config.service?.serviceName || config.service?.name || 'unknown',
redis: {
host: config.redis.host,
port: config.redis.port,

View file

@ -1,405 +1,414 @@
/**
* ServiceApplication - Common service initialization and lifecycle management
* Encapsulates common patterns for Hono-based microservices
*/
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 { AppConfig as StockBotAppConfig } from '@stock-bot/config';
import type { IServiceContainer } from '@stock-bot/handlers';
import type { ServiceContainer } from './awilix-container';
/**
* Configuration for ServiceApplication
*/
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;
}
/**
* Lifecycle hooks for service customization
*/
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;
}
/**
* ServiceApplication - Manages the complete lifecycle of a microservice
*/
export class ServiceApplication {
private config: StockBotAppConfig;
private serviceConfig: ServiceApplicationConfig;
private hooks: ServiceLifecycleHooks;
private logger: Logger;
private container: ServiceContainer | 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: StockBotAppConfig,
serviceConfig: ServiceApplicationConfig,
hooks: ServiceLifecycleHooks = {}
) {
this.config = config;
this.serviceConfig = {
shutdownTimeout: 15000,
enableHandlers: false,
enableScheduledJobs: false,
addInfoEndpoint: true,
...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
});
}
/**
* Configure logger based on application config
*/
private configureLogger(): void {
if (this.config.log) {
setLoggerConfig({
logLevel: this.config.log.level,
logConsole: true,
logFile: false,
environment: this.config.environment,
hideObject: this.config.log.hideObject,
});
}
}
/**
* 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: '*',
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'],
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 || {};
app.get('/', c => {
return c.json({
name: this.serviceConfig.serviceName,
version: metadata.version || '1.0.0',
description: metadata.description,
status: 'running',
timestamp: new Date().toISOString(),
endpoints: metadata.endpoints || {},
});
});
}
return app;
}
/**
* Register graceful shutdown handlers
*/
private registerShutdownHandlers(): void {
// Priority 1: Queue system (highest priority)
if (this.serviceConfig.enableScheduledJobs) {
this.shutdown.onShutdownHigh(async () => {
this.logger.info('Shutting down queue system...');
try {
const queueManager = this.container?.resolve('queueManager');
if (queueManager) {
await queueManager.shutdown();
}
this.logger.info('Queue system shut down');
} catch (error) {
this.logger.error('Error shutting down queue system', { error });
}
}, 'Queue System');
}
// Priority 1: HTTP Server (high priority)
this.shutdown.onShutdownHigh(async () => {
if (this.server) {
this.logger.info('Stopping HTTP server...');
try {
this.server.stop();
this.logger.info('HTTP server stopped');
} catch (error) {
this.logger.error('Error stopping HTTP server', { error });
}
}
}, 'HTTP Server');
// Custom shutdown hook
if (this.hooks.onBeforeShutdown) {
this.shutdown.onShutdownHigh(async () => {
try {
await this.hooks.onBeforeShutdown!();
} catch (error) {
this.logger.error('Error in custom shutdown hook', { error });
}
}, 'Custom Shutdown');
}
// Priority 2: Services and connections (medium priority)
this.shutdown.onShutdownMedium(async () => {
this.logger.info('Disposing services and connections...');
try {
if (this.container) {
// Disconnect database clients
const mongoClient = this.container.resolve('mongoClient');
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 {
this.logger.info('Shutting down loggers...');
await shutdownLoggers();
// Don't log after shutdown
} catch {
// Silently ignore logger shutdown errors
}
}, 'Loggers');
}
/**
* Start the service with full initialization
*/
async start(
containerFactory: (config: StockBotAppConfig) => Promise<ServiceContainer>,
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...');
this.container = await containerFactory(this.config);
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({
port,
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) {
console.error('DETAILED ERROR:', error);
this.logger.error('Failed to start service', {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
details: JSON.stringify(error, null, 2),
});
throw error;
}
}
/**
* Initialize scheduled jobs from handler registry
*/
private async initializeScheduledJobs(): Promise<void> {
if (!this.container) {
throw new Error('Container not initialized');
}
this.logger.debug('Creating scheduled jobs from registered handlers...');
const { handlerRegistry } = await import('@stock-bot/types');
const allHandlers = handlerRegistry.getAllHandlersWithSchedule();
let totalScheduledJobs = 0;
for (const [handlerName, config] of allHandlers) {
if (config.scheduledJobs && config.scheduledJobs.length > 0) {
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 = {
handler: handlerName,
operation: scheduledJob.operation,
payload: scheduledJob.payload,
};
// Build job options from scheduled job config
const jobOptions = {
priority: scheduledJob.priority,
delay: scheduledJob.delay,
repeat: {
immediately: scheduledJob.immediately,
},
};
await queue.addScheduledJob(
scheduledJob.operation,
jobData,
scheduledJob.cronPattern,
jobOptions
);
totalScheduledJobs++;
this.logger.debug('Scheduled job created', {
handler: handlerName,
operation: scheduledJob.operation,
cronPattern: scheduledJob.cronPattern,
immediately: scheduledJob.immediately,
priority: scheduledJob.priority,
});
}
}
}
this.logger.info('Scheduled jobs created', { totalJobs: totalScheduledJobs });
// Start queue workers
this.logger.debug('Starting queue workers...');
const queueManager = this.container.resolve('queueManager');
if (queueManager) {
queueManager.startAllWorkers();
this.logger.info('Queue workers started');
}
}
/**
* Stop the service gracefully
*/
async stop(): Promise<void> {
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;
}
/**
* ServiceApplication - Common service initialization and lifecycle management
* Encapsulates common patterns for Hono-based microservices
*/
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 { AppConfig as StockBotAppConfig, UnifiedAppConfig } from '@stock-bot/config';
import { toUnifiedConfig } from '@stock-bot/config';
import type { IServiceContainer } from '@stock-bot/handlers';
import type { ServiceContainer } from './awilix-container';
/**
* Configuration for ServiceApplication
*/
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;
}
/**
* Lifecycle hooks for service customization
*/
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;
}
/**
* ServiceApplication - Manages the complete lifecycle of a microservice
*/
export class ServiceApplication {
private config: UnifiedAppConfig;
private serviceConfig: ServiceApplicationConfig;
private hooks: ServiceLifecycleHooks;
private logger: Logger;
private container: ServiceContainer | 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: StockBotAppConfig | UnifiedAppConfig,
serviceConfig: ServiceApplicationConfig,
hooks: ServiceLifecycleHooks = {}
) {
// 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,
enableScheduledJobs: false,
addInfoEndpoint: true,
...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
});
}
/**
* Configure logger based on application config
*/
private configureLogger(): void {
if (this.config.log) {
setLoggerConfig({
logLevel: this.config.log.level,
logConsole: true,
logFile: false,
environment: this.config.environment,
hideObject: this.config.log.hideObject,
});
}
}
/**
* 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: '*',
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'],
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 || {};
app.get('/', c => {
return c.json({
name: this.serviceConfig.serviceName,
version: metadata.version || '1.0.0',
description: metadata.description,
status: 'running',
timestamp: new Date().toISOString(),
endpoints: metadata.endpoints || {},
});
});
}
return app;
}
/**
* Register graceful shutdown handlers
*/
private registerShutdownHandlers(): void {
// Priority 1: Queue system (highest priority)
if (this.serviceConfig.enableScheduledJobs) {
this.shutdown.onShutdownHigh(async () => {
this.logger.info('Shutting down queue system...');
try {
const queueManager = this.container?.resolve('queueManager');
if (queueManager) {
await queueManager.shutdown();
}
this.logger.info('Queue system shut down');
} catch (error) {
this.logger.error('Error shutting down queue system', { error });
}
}, 'Queue System');
}
// Priority 1: HTTP Server (high priority)
this.shutdown.onShutdownHigh(async () => {
if (this.server) {
this.logger.info('Stopping HTTP server...');
try {
this.server.stop();
this.logger.info('HTTP server stopped');
} catch (error) {
this.logger.error('Error stopping HTTP server', { error });
}
}
}, 'HTTP Server');
// Custom shutdown hook
if (this.hooks.onBeforeShutdown) {
this.shutdown.onShutdownHigh(async () => {
try {
await this.hooks.onBeforeShutdown!();
} catch (error) {
this.logger.error('Error in custom shutdown hook', { error });
}
}, 'Custom Shutdown');
}
// Priority 2: Services and connections (medium priority)
this.shutdown.onShutdownMedium(async () => {
this.logger.info('Disposing services and connections...');
try {
if (this.container) {
// Disconnect database clients
const mongoClient = this.container.resolve('mongoClient');
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 {
this.logger.info('Shutting down loggers...');
await shutdownLoggers();
// Don't log after shutdown
} catch {
// Silently ignore logger shutdown errors
}
}, 'Loggers');
}
/**
* Start the service with full initialization
*/
async start(
containerFactory: (config: UnifiedAppConfig) => Promise<ServiceContainer>,
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.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({
port,
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) {
console.error('DETAILED ERROR:', error);
this.logger.error('Failed to start service', {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
details: JSON.stringify(error, null, 2),
});
throw error;
}
}
/**
* Initialize scheduled jobs from handler registry
*/
private async initializeScheduledJobs(): Promise<void> {
if (!this.container) {
throw new Error('Container not initialized');
}
this.logger.debug('Creating scheduled jobs from registered handlers...');
const { handlerRegistry } = await import('@stock-bot/types');
const allHandlers = handlerRegistry.getAllHandlersWithSchedule();
let totalScheduledJobs = 0;
for (const [handlerName, config] of allHandlers) {
if (config.scheduledJobs && config.scheduledJobs.length > 0) {
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 = {
handler: handlerName,
operation: scheduledJob.operation,
payload: scheduledJob.payload,
};
// Build job options from scheduled job config
const jobOptions = {
priority: scheduledJob.priority,
delay: scheduledJob.delay,
repeat: {
immediately: scheduledJob.immediately,
},
};
await queue.addScheduledJob(
scheduledJob.operation,
jobData,
scheduledJob.cronPattern,
jobOptions
);
totalScheduledJobs++;
this.logger.debug('Scheduled job created', {
handler: handlerName,
operation: scheduledJob.operation,
cronPattern: scheduledJob.cronPattern,
immediately: scheduledJob.immediately,
priority: scheduledJob.priority,
});
}
}
}
this.logger.info('Scheduled jobs created', { totalJobs: totalScheduledJobs });
// Start queue workers
this.logger.debug('Starting queue workers...');
const queueManager = this.container.resolve('queueManager');
if (queueManager) {
queueManager.startAllWorkers();
this.logger.info('Queue workers started');
}
}
/**
* Stop the service gracefully
*/
async stop(): Promise<void> {
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;
}
}