huge refactor to remove depenencie hell and add typesafe container
This commit is contained in:
parent
28b9822d55
commit
843a7b9b9b
148 changed files with 3603 additions and 2378 deletions
|
|
@ -20,6 +20,8 @@
|
|||
"@stock-bot/queue": "workspace:*",
|
||||
"@stock-bot/shutdown": "workspace:*",
|
||||
"@stock-bot/handlers": "workspace:*",
|
||||
"@stock-bot/handler-registry": "workspace:*",
|
||||
"glob": "^10.0.0",
|
||||
"zod": "^3.23.8",
|
||||
"hono": "^4.0.0",
|
||||
"awilix": "^12.0.5"
|
||||
|
|
|
|||
|
|
@ -3,16 +3,16 @@
|
|||
* Creates a decoupled, reusable dependency injection container
|
||||
*/
|
||||
|
||||
import { type AwilixContainer } from 'awilix';
|
||||
import type { Browser } from '@stock-bot/browser';
|
||||
import type { CacheProvider } from '@stock-bot/cache';
|
||||
import type { IServiceContainer } from '@stock-bot/types';
|
||||
import type { Logger } from '@stock-bot/logger';
|
||||
import type { MongoDBClient } from '@stock-bot/mongodb';
|
||||
import type { PostgreSQLClient } from '@stock-bot/postgres';
|
||||
import type { ProxyManager } from '@stock-bot/proxy';
|
||||
import type { QuestDBClient } from '@stock-bot/questdb';
|
||||
import type { QueueManager } from '@stock-bot/queue';
|
||||
import { type AwilixContainer } from 'awilix';
|
||||
import type { IServiceContainer } from '@stock-bot/types';
|
||||
import type { AppConfig } from './config/schemas';
|
||||
|
||||
// Re-export for backward compatibility
|
||||
|
|
@ -41,8 +41,6 @@ export interface ServiceDefinitions {
|
|||
serviceContainer: IServiceContainer;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Export typed container
|
||||
export type ServiceContainer = AwilixContainer<ServiceDefinitions>;
|
||||
export type ServiceCradle = ServiceDefinitions;
|
||||
|
|
@ -59,5 +57,3 @@ export interface ServiceContainerOptions {
|
|||
enableBrowser?: boolean;
|
||||
enableProxy?: boolean;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { z } from 'zod';
|
||||
import { redisConfigSchema } from './redis.schema';
|
||||
import { mongodbConfigSchema } from './mongodb.schema';
|
||||
import { postgresConfigSchema } from './postgres.schema';
|
||||
import { questdbConfigSchema } from './questdb.schema';
|
||||
import { proxyConfigSchema, browserConfigSchema, queueConfigSchema } from './service.schema';
|
||||
import { redisConfigSchema } from './redis.schema';
|
||||
import { browserConfigSchema, proxyConfigSchema, queueConfigSchema } from './service.schema';
|
||||
|
||||
export const appConfigSchema = z.object({
|
||||
redis: redisConfigSchema,
|
||||
|
|
@ -13,11 +13,13 @@ export const appConfigSchema = z.object({
|
|||
proxy: proxyConfigSchema.optional(),
|
||||
browser: browserConfigSchema.optional(),
|
||||
queue: queueConfigSchema.optional(),
|
||||
service: z.object({
|
||||
name: z.string(),
|
||||
serviceName: z.string().optional(), // Standard kebab-case service name
|
||||
port: z.number().optional(),
|
||||
}).optional(),
|
||||
service: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
serviceName: z.string().optional(), // Standard kebab-case service name
|
||||
port: z.number().optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export type AppConfig = z.infer<typeof appConfigSchema>;
|
||||
|
|
@ -27,4 +29,4 @@ export * from './redis.schema';
|
|||
export * from './mongodb.schema';
|
||||
export * from './postgres.schema';
|
||||
export * from './questdb.schema';
|
||||
export * from './service.schema';
|
||||
export * from './service.schema';
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const mongodbConfigSchema = z.object({
|
||||
enabled: z.boolean().optional().default(true),
|
||||
uri: z.string(),
|
||||
database: z.string(),
|
||||
});
|
||||
|
||||
export type MongoDBConfig = z.infer<typeof mongodbConfigSchema>;
|
||||
import { z } from 'zod';
|
||||
|
||||
export const mongodbConfigSchema = z.object({
|
||||
enabled: z.boolean().optional().default(true),
|
||||
uri: z.string(),
|
||||
database: z.string(),
|
||||
});
|
||||
|
||||
export type MongoDBConfig = z.infer<typeof mongodbConfigSchema>;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const postgresConfigSchema = z.object({
|
||||
enabled: z.boolean().optional().default(true),
|
||||
host: z.string().default('localhost'),
|
||||
port: z.number().default(5432),
|
||||
database: z.string(),
|
||||
user: z.string(),
|
||||
password: z.string(),
|
||||
});
|
||||
|
||||
export type PostgresConfig = z.infer<typeof postgresConfigSchema>;
|
||||
import { z } from 'zod';
|
||||
|
||||
export const postgresConfigSchema = z.object({
|
||||
enabled: z.boolean().optional().default(true),
|
||||
host: z.string().default('localhost'),
|
||||
port: z.number().default(5432),
|
||||
database: z.string(),
|
||||
user: z.string(),
|
||||
password: z.string(),
|
||||
});
|
||||
|
||||
export type PostgresConfig = z.infer<typeof postgresConfigSchema>;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const questdbConfigSchema = z.object({
|
||||
enabled: z.boolean().optional().default(true),
|
||||
host: z.string().default('localhost'),
|
||||
httpPort: z.number().optional().default(9000),
|
||||
pgPort: z.number().optional().default(8812),
|
||||
influxPort: z.number().optional().default(9009),
|
||||
database: z.string().optional().default('questdb'),
|
||||
});
|
||||
|
||||
export type QuestDBConfig = z.infer<typeof questdbConfigSchema>;
|
||||
import { z } from 'zod';
|
||||
|
||||
export const questdbConfigSchema = z.object({
|
||||
enabled: z.boolean().optional().default(true),
|
||||
host: z.string().default('localhost'),
|
||||
httpPort: z.number().optional().default(9000),
|
||||
pgPort: z.number().optional().default(8812),
|
||||
influxPort: z.number().optional().default(9009),
|
||||
database: z.string().optional().default('questdb'),
|
||||
});
|
||||
|
||||
export type QuestDBConfig = z.infer<typeof questdbConfigSchema>;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const redisConfigSchema = z.object({
|
||||
enabled: z.boolean().optional().default(true),
|
||||
host: z.string().default('localhost'),
|
||||
port: z.number().default(6379),
|
||||
password: z.string().optional(),
|
||||
username: z.string().optional(),
|
||||
db: z.number().optional().default(0),
|
||||
});
|
||||
|
||||
export type RedisConfig = z.infer<typeof redisConfigSchema>;
|
||||
import { z } from 'zod';
|
||||
|
||||
export const redisConfigSchema = z.object({
|
||||
enabled: z.boolean().optional().default(true),
|
||||
host: z.string().default('localhost'),
|
||||
port: z.number().default(6379),
|
||||
password: z.string().optional(),
|
||||
username: z.string().optional(),
|
||||
db: z.number().optional().default(0),
|
||||
});
|
||||
|
||||
export type RedisConfig = z.infer<typeof redisConfigSchema>;
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@ export const proxyConfigSchema = z.object({
|
|||
enabled: z.boolean().default(false),
|
||||
cachePrefix: z.string().optional().default('proxy:'),
|
||||
ttl: z.number().optional().default(3600),
|
||||
webshare: z.object({
|
||||
apiKey: z.string(),
|
||||
apiUrl: z.string().default('https://proxy.webshare.io/api/v2/'),
|
||||
}).optional(),
|
||||
webshare: z
|
||||
.object({
|
||||
apiKey: z.string(),
|
||||
apiUrl: z.string().default('https://proxy.webshare.io/api/v2/'),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const browserConfigSchema = z.object({
|
||||
|
|
@ -21,18 +23,23 @@ export const queueConfigSchema = z.object({
|
|||
concurrency: z.number().optional().default(1),
|
||||
enableScheduledJobs: z.boolean().optional().default(true),
|
||||
delayWorkerStart: z.boolean().optional().default(false),
|
||||
defaultJobOptions: z.object({
|
||||
attempts: z.number().default(3),
|
||||
backoff: z.object({
|
||||
type: z.enum(['exponential', 'fixed']).default('exponential'),
|
||||
delay: z.number().default(1000),
|
||||
}).default({}),
|
||||
removeOnComplete: z.number().default(100),
|
||||
removeOnFail: z.number().default(50),
|
||||
timeout: z.number().optional(),
|
||||
}).optional().default({}),
|
||||
defaultJobOptions: z
|
||||
.object({
|
||||
attempts: z.number().default(3),
|
||||
backoff: z
|
||||
.object({
|
||||
type: z.enum(['exponential', 'fixed']).default('exponential'),
|
||||
delay: z.number().default(1000),
|
||||
})
|
||||
.default({}),
|
||||
removeOnComplete: z.number().default(100),
|
||||
removeOnFail: z.number().default(50),
|
||||
timeout: z.number().optional(),
|
||||
})
|
||||
.optional()
|
||||
.default({}),
|
||||
});
|
||||
|
||||
export type ProxyConfig = z.infer<typeof proxyConfigSchema>;
|
||||
export type BrowserConfig = z.infer<typeof browserConfigSchema>;
|
||||
export type QueueConfig = z.infer<typeof queueConfigSchema>;
|
||||
export type QueueConfig = z.infer<typeof queueConfigSchema>;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
import { createContainer, InjectionMode, asFunction, type AwilixContainer } from 'awilix';
|
||||
import { asClass, asFunction, createContainer, InjectionMode, type AwilixContainer } from 'awilix';
|
||||
import type { BaseAppConfig as StockBotAppConfig, UnifiedAppConfig } from '@stock-bot/config';
|
||||
import { appConfigSchema, type AppConfig } from '../config/schemas';
|
||||
import { toUnifiedConfig } from '@stock-bot/config';
|
||||
import {
|
||||
registerCoreServices,
|
||||
import { HandlerRegistry } from '@stock-bot/handler-registry';
|
||||
import { appConfigSchema, type AppConfig } from '../config/schemas';
|
||||
import {
|
||||
registerApplicationServices,
|
||||
registerCacheServices,
|
||||
registerCoreServices,
|
||||
registerDatabaseServices,
|
||||
registerApplicationServices
|
||||
} from '../registrations';
|
||||
import { HandlerScanner } from '../scanner';
|
||||
import { ServiceLifecycleManager } from '../utils/lifecycle';
|
||||
import type { ServiceDefinitions, ContainerBuildOptions } from './types';
|
||||
import type { ContainerBuildOptions, ServiceDefinitions } from './types';
|
||||
|
||||
export class ServiceContainerBuilder {
|
||||
private config: Partial<AppConfig> = {};
|
||||
|
|
@ -38,7 +40,10 @@ export class ServiceContainerBuilder {
|
|||
return this;
|
||||
}
|
||||
|
||||
enableService(service: keyof Omit<ContainerBuildOptions, 'skipInitialization' | 'initializationTimeout'>, enabled = true): this {
|
||||
enableService(
|
||||
service: keyof Omit<ContainerBuildOptions, 'skipInitialization' | 'initializationTimeout'>,
|
||||
enabled = true
|
||||
): this {
|
||||
this.options[service] = enabled;
|
||||
return this;
|
||||
}
|
||||
|
|
@ -51,7 +56,7 @@ export class ServiceContainerBuilder {
|
|||
async build(): Promise<AwilixContainer<ServiceDefinitions>> {
|
||||
// Validate and prepare config
|
||||
const validatedConfig = this.prepareConfig();
|
||||
|
||||
|
||||
// Create container
|
||||
const container = createContainer<ServiceDefinitions>({
|
||||
injectionMode: InjectionMode.PROXY,
|
||||
|
|
@ -77,17 +82,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',
|
||||
};
|
||||
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 || {
|
||||
|
|
@ -110,61 +117,88 @@ export class ServiceContainerBuilder {
|
|||
password: 'postgres',
|
||||
},
|
||||
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 || {
|
||||
enabled: true,
|
||||
workers: 1,
|
||||
concurrency: 1,
|
||||
enableScheduledJobs: true,
|
||||
delayWorkerStart: false,
|
||||
defaultJobOptions: {
|
||||
attempts: 3,
|
||||
backoff: { type: 'exponential' as const, delay: 1000 },
|
||||
removeOnComplete: 100,
|
||||
removeOnFail: 50,
|
||||
}
|
||||
}) : 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 || {
|
||||
enabled: true,
|
||||
workers: 1,
|
||||
concurrency: 1,
|
||||
enableScheduledJobs: true,
|
||||
delayWorkerStart: false,
|
||||
defaultJobOptions: {
|
||||
attempts: 3,
|
||||
backoff: { type: 'exponential' as const, delay: 1000 },
|
||||
removeOnComplete: 100,
|
||||
removeOnFail: 50,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
service: config.service,
|
||||
};
|
||||
}
|
||||
|
||||
private registerServices(container: AwilixContainer<ServiceDefinitions>, config: AppConfig): void {
|
||||
private registerServices(
|
||||
container: AwilixContainer<ServiceDefinitions>,
|
||||
config: AppConfig
|
||||
): void {
|
||||
// Register handler infrastructure first
|
||||
container.register({
|
||||
handlerRegistry: asClass(HandlerRegistry).singleton(),
|
||||
handlerScanner: asClass(HandlerScanner).singleton(),
|
||||
});
|
||||
|
||||
registerCoreServices(container, config);
|
||||
registerCacheServices(container, config);
|
||||
registerDatabaseServices(container, config);
|
||||
registerApplicationServices(container, config);
|
||||
|
||||
|
||||
// Register service container aggregate
|
||||
container.register({
|
||||
serviceContainer: asFunction(({
|
||||
config: _config, logger, cache, globalCache, proxyManager, browser,
|
||||
queueManager, mongoClient, postgresClient, questdbClient
|
||||
}) => ({
|
||||
logger,
|
||||
cache,
|
||||
globalCache,
|
||||
proxy: proxyManager, // Map proxyManager to proxy
|
||||
browser,
|
||||
queue: queueManager, // Map queueManager to queue
|
||||
mongodb: mongoClient, // Map mongoClient to mongodb
|
||||
postgres: postgresClient, // Map postgresClient to postgres
|
||||
questdb: questdbClient, // Map questdbClient to questdb
|
||||
})).singleton(),
|
||||
serviceContainer: asFunction(
|
||||
({
|
||||
config: _config,
|
||||
logger,
|
||||
cache,
|
||||
globalCache,
|
||||
proxyManager,
|
||||
browser,
|
||||
queueManager,
|
||||
mongoClient,
|
||||
postgresClient,
|
||||
questdbClient,
|
||||
}) => ({
|
||||
logger,
|
||||
cache,
|
||||
globalCache,
|
||||
proxy: proxyManager, // Map proxyManager to proxy
|
||||
browser,
|
||||
queue: queueManager, // Map queueManager to queue
|
||||
mongodb: mongoClient, // Map mongoClient to mongodb
|
||||
postgres: postgresClient, // Map postgresClient to postgres
|
||||
questdb: questdbClient, // Map questdbClient to questdb
|
||||
})
|
||||
).singleton(),
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
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;
|
||||
|
||||
return {
|
||||
redis: config.redis,
|
||||
|
|
@ -177,4 +211,4 @@ export class ServiceContainerBuilder {
|
|||
service: config.service,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,48 +1,54 @@
|
|||
import type { Browser } from '@stock-bot/browser';
|
||||
import type { CacheProvider } from '@stock-bot/cache';
|
||||
import type { IServiceContainer } from '@stock-bot/types';
|
||||
import type { Logger } from '@stock-bot/logger';
|
||||
import type { MongoDBClient } from '@stock-bot/mongodb';
|
||||
import type { PostgreSQLClient } from '@stock-bot/postgres';
|
||||
import type { ProxyManager } from '@stock-bot/proxy';
|
||||
import type { QuestDBClient } from '@stock-bot/questdb';
|
||||
import type { SmartQueueManager } from '@stock-bot/queue';
|
||||
import type { AppConfig } from '../config/schemas';
|
||||
|
||||
export interface ServiceDefinitions {
|
||||
// Configuration
|
||||
config: AppConfig;
|
||||
logger: Logger;
|
||||
|
||||
// Core services
|
||||
cache: CacheProvider | null;
|
||||
globalCache: CacheProvider | null;
|
||||
proxyManager: ProxyManager | null;
|
||||
browser: Browser;
|
||||
queueManager: SmartQueueManager | null;
|
||||
|
||||
// Database clients
|
||||
mongoClient: MongoDBClient | null;
|
||||
postgresClient: PostgreSQLClient | null;
|
||||
questdbClient: QuestDBClient | null;
|
||||
|
||||
// Aggregate service container
|
||||
serviceContainer: IServiceContainer;
|
||||
}
|
||||
|
||||
export type ServiceCradle = ServiceDefinitions;
|
||||
|
||||
export interface ServiceContainerOptions {
|
||||
enableQuestDB?: boolean;
|
||||
enableMongoDB?: boolean;
|
||||
enablePostgres?: boolean;
|
||||
enableCache?: boolean;
|
||||
enableQueue?: boolean;
|
||||
enableBrowser?: boolean;
|
||||
enableProxy?: boolean;
|
||||
}
|
||||
|
||||
export interface ContainerBuildOptions extends ServiceContainerOptions {
|
||||
skipInitialization?: boolean;
|
||||
initializationTimeout?: number;
|
||||
}
|
||||
import type { Browser } from '@stock-bot/browser';
|
||||
import type { CacheProvider } from '@stock-bot/cache';
|
||||
import type { HandlerRegistry } from '@stock-bot/handler-registry';
|
||||
import type { Logger } from '@stock-bot/logger';
|
||||
import type { MongoDBClient } from '@stock-bot/mongodb';
|
||||
import type { PostgreSQLClient } from '@stock-bot/postgres';
|
||||
import type { ProxyManager } from '@stock-bot/proxy';
|
||||
import type { QuestDBClient } from '@stock-bot/questdb';
|
||||
import type { SmartQueueManager } from '@stock-bot/queue';
|
||||
import type { IServiceContainer } from '@stock-bot/types';
|
||||
import type { AppConfig } from '../config/schemas';
|
||||
import type { HandlerScanner } from '../scanner';
|
||||
|
||||
export interface ServiceDefinitions {
|
||||
// Configuration
|
||||
config: AppConfig;
|
||||
logger: Logger;
|
||||
|
||||
// Handler infrastructure
|
||||
handlerRegistry: HandlerRegistry;
|
||||
handlerScanner: HandlerScanner;
|
||||
|
||||
// Core services
|
||||
cache: CacheProvider | null;
|
||||
globalCache: CacheProvider | null;
|
||||
proxyManager: ProxyManager | null;
|
||||
browser: Browser;
|
||||
queueManager: SmartQueueManager | null;
|
||||
|
||||
// Database clients
|
||||
mongoClient: MongoDBClient | null;
|
||||
postgresClient: PostgreSQLClient | null;
|
||||
questdbClient: QuestDBClient | null;
|
||||
|
||||
// Aggregate service container
|
||||
serviceContainer: IServiceContainer;
|
||||
}
|
||||
|
||||
export type ServiceCradle = ServiceDefinitions;
|
||||
|
||||
export interface ServiceContainerOptions {
|
||||
enableQuestDB?: boolean;
|
||||
enableMongoDB?: boolean;
|
||||
enablePostgres?: boolean;
|
||||
enableCache?: boolean;
|
||||
enableQueue?: boolean;
|
||||
enableBrowser?: boolean;
|
||||
enableProxy?: boolean;
|
||||
}
|
||||
|
||||
export interface ContainerBuildOptions extends ServiceContainerOptions {
|
||||
skipInitialization?: boolean;
|
||||
initializationTimeout?: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,7 @@ import { NamespacedCache, type CacheProvider } from '@stock-bot/cache';
|
|||
import type { ServiceDefinitions } from '../container/types';
|
||||
|
||||
export class CacheFactory {
|
||||
static createNamespacedCache(
|
||||
baseCache: CacheProvider,
|
||||
namespace: string
|
||||
): NamespacedCache {
|
||||
static createNamespacedCache(baseCache: CacheProvider, namespace: string): NamespacedCache {
|
||||
return new NamespacedCache(baseCache, namespace);
|
||||
}
|
||||
|
||||
|
|
@ -15,8 +12,10 @@ export class CacheFactory {
|
|||
serviceName: string
|
||||
): CacheProvider | null {
|
||||
const baseCache = container.cradle.cache;
|
||||
if (!baseCache) {return null;}
|
||||
|
||||
if (!baseCache) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.createNamespacedCache(baseCache, serviceName);
|
||||
}
|
||||
|
||||
|
|
@ -25,8 +24,10 @@ export class CacheFactory {
|
|||
handlerName: string
|
||||
): CacheProvider | null {
|
||||
const baseCache = container.cradle.cache;
|
||||
if (!baseCache) {return null;}
|
||||
|
||||
if (!baseCache) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.createNamespacedCache(baseCache, `handler:${handlerName}`);
|
||||
}
|
||||
|
||||
|
|
@ -35,10 +36,12 @@ export class CacheFactory {
|
|||
prefix: string
|
||||
): CacheProvider | null {
|
||||
const baseCache = container.cradle.cache;
|
||||
if (!baseCache) {return null;}
|
||||
|
||||
if (!baseCache) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Remove 'cache:' prefix if already included
|
||||
const cleanPrefix = prefix.replace(/^cache:/, '');
|
||||
return this.createNamespacedCache(baseCache, cleanPrefix);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { CacheFactory } from './cache.factory';
|
||||
export { CacheFactory } from './cache.factory';
|
||||
|
|
|
|||
|
|
@ -33,3 +33,7 @@ export {
|
|||
type ServiceApplicationConfig,
|
||||
type ServiceLifecycleHooks,
|
||||
} from './service-application';
|
||||
|
||||
// Handler scanner
|
||||
export { HandlerScanner } from './scanner';
|
||||
export type { HandlerScannerOptions } from './scanner';
|
||||
|
|
|
|||
|
|
@ -12,26 +12,34 @@ export function registerCacheServices(
|
|||
const { createServiceCache } = require('@stock-bot/queue');
|
||||
// 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, {
|
||||
host: config.redis.host,
|
||||
port: config.redis.port,
|
||||
password: config.redis.password,
|
||||
db: config.redis.db, // This will be overridden by ServiceCache
|
||||
}, { logger });
|
||||
return createServiceCache(
|
||||
serviceName,
|
||||
{
|
||||
host: config.redis.host,
|
||||
port: config.redis.port,
|
||||
password: config.redis.password,
|
||||
db: config.redis.db, // This will be overridden by ServiceCache
|
||||
},
|
||||
{ logger }
|
||||
);
|
||||
}).singleton(),
|
||||
|
||||
|
||||
// Also provide global cache for shared data
|
||||
globalCache: asFunction(({ logger }) => {
|
||||
const { createServiceCache } = require('@stock-bot/queue');
|
||||
const serviceName = config.service?.serviceName || config.service?.name || 'unknown';
|
||||
|
||||
return createServiceCache(serviceName, {
|
||||
host: config.redis.host,
|
||||
port: config.redis.port,
|
||||
password: config.redis.password,
|
||||
}, { global: true, logger });
|
||||
|
||||
return createServiceCache(
|
||||
serviceName,
|
||||
{
|
||||
host: config.redis.host,
|
||||
port: config.redis.port,
|
||||
password: config.redis.password,
|
||||
},
|
||||
{ global: true, logger }
|
||||
);
|
||||
}).singleton(),
|
||||
});
|
||||
} else {
|
||||
|
|
@ -40,4 +48,4 @@ export function registerCacheServices(
|
|||
globalCache: asValue(null),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,4 +11,4 @@ export function registerCoreServices(
|
|||
config: asValue(config),
|
||||
logger: asValue(getLogger('di-container')),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { asFunction, asValue, type AwilixContainer } from 'awilix';
|
||||
import { MongoDBClient } from '@stock-bot/mongodb';
|
||||
import { PostgreSQLClient } from '@stock-bot/postgres';
|
||||
import { QuestDBClient } from '@stock-bot/questdb';
|
||||
import { asFunction, asValue, type AwilixContainer } from 'awilix';
|
||||
import type { AppConfig } from '../config/schemas';
|
||||
import type { ServiceDefinitions } from '../container/types';
|
||||
|
||||
|
|
@ -14,7 +14,9 @@ export function registerDatabaseServices(
|
|||
container.register({
|
||||
mongoClient: asFunction(({ logger }) => {
|
||||
// Parse MongoDB URI to extract components
|
||||
const uriMatch = config.mongodb.uri.match(/mongodb:\/\/(?:([^:]+):([^@]+)@)?([^:/]+):(\d+)\/([^?]+)(?:\?authSource=(.+))?/);
|
||||
const uriMatch = config.mongodb.uri.match(
|
||||
/mongodb:\/\/(?:([^:]+):([^@]+)@)?([^:/]+):(\d+)\/([^?]+)(?:\?authSource=(.+))?/
|
||||
);
|
||||
const mongoConfig = {
|
||||
host: uriMatch?.[3] || 'localhost',
|
||||
port: parseInt(uriMatch?.[4] || '27017'),
|
||||
|
|
@ -44,9 +46,9 @@ export function registerDatabaseServices(
|
|||
username: config.postgres.user,
|
||||
password: String(config.postgres.password), // Ensure password is a string
|
||||
};
|
||||
|
||||
logger.debug('PostgreSQL config:', {
|
||||
...pgConfig,
|
||||
|
||||
logger.debug('PostgreSQL config:', {
|
||||
...pgConfig,
|
||||
password: pgConfig.password ? '***' : 'NO_PASSWORD',
|
||||
});
|
||||
return new PostgreSQLClient(pgConfig, logger);
|
||||
|
|
@ -79,4 +81,4 @@ export function registerDatabaseServices(
|
|||
questdbClient: asValue(null),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export { registerCoreServices } from './core.registration';
|
||||
export { registerCacheServices } from './cache.registration';
|
||||
export { registerDatabaseServices } from './database.registration';
|
||||
export { registerApplicationServices } from './service.registration';
|
||||
export { registerCoreServices } from './core.registration';
|
||||
export { registerCacheServices } from './cache.registration';
|
||||
export { registerDatabaseServices } from './database.registration';
|
||||
export { registerApplicationServices } from './service.registration';
|
||||
|
|
|
|||
|
|
@ -44,9 +44,9 @@ export function registerApplicationServices(
|
|||
enableMetrics: true,
|
||||
logger,
|
||||
});
|
||||
|
||||
|
||||
const proxyManager = new ProxyManager(proxyCache, config.proxy, logger);
|
||||
|
||||
|
||||
// Note: Initialization will be handled by the lifecycle manager
|
||||
return proxyManager;
|
||||
}).singleton(),
|
||||
|
|
@ -60,7 +60,7 @@ export function registerApplicationServices(
|
|||
// Queue Manager
|
||||
if (config.queue?.enabled && config.redis.enabled) {
|
||||
container.register({
|
||||
queueManager: asFunction(({ logger }) => {
|
||||
queueManager: asFunction(({ logger, handlerRegistry }) => {
|
||||
const { SmartQueueManager } = require('@stock-bot/queue');
|
||||
const queueConfig = {
|
||||
serviceName: config.service?.serviceName || config.service?.name || 'unknown',
|
||||
|
|
@ -79,7 +79,7 @@ export function registerApplicationServices(
|
|||
delayWorkerStart: config.queue!.delayWorkerStart ?? false,
|
||||
autoDiscoverHandlers: true,
|
||||
};
|
||||
return new SmartQueueManager(queueConfig, logger);
|
||||
return new SmartQueueManager(queueConfig, handlerRegistry, logger);
|
||||
}).singleton(),
|
||||
});
|
||||
} else {
|
||||
|
|
@ -87,4 +87,4 @@ export function registerApplicationServices(
|
|||
queueManager: asValue(null),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
201
libs/core/di/src/scanner/handler-scanner.ts
Normal file
201
libs/core/di/src/scanner/handler-scanner.ts
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
/**
|
||||
* Handler Scanner
|
||||
* Discovers and registers handlers with the DI container
|
||||
*/
|
||||
|
||||
import { asClass, type AwilixContainer } from 'awilix';
|
||||
import { glob } from 'glob';
|
||||
import type {
|
||||
HandlerConfiguration,
|
||||
HandlerMetadata,
|
||||
HandlerRegistry,
|
||||
} from '@stock-bot/handler-registry';
|
||||
import { createJobHandler } from '@stock-bot/handlers';
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
import type { ExecutionContext, IHandler } from '@stock-bot/types';
|
||||
|
||||
export interface HandlerScannerOptions {
|
||||
serviceName?: string;
|
||||
autoRegister?: boolean;
|
||||
patterns?: string[];
|
||||
}
|
||||
|
||||
export class HandlerScanner {
|
||||
private logger = getLogger('handler-scanner');
|
||||
private discoveredHandlers = new Map<string, any>();
|
||||
|
||||
constructor(
|
||||
private registry: HandlerRegistry,
|
||||
private container: AwilixContainer,
|
||||
private options: HandlerScannerOptions = {}
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Scan for handlers matching the given patterns
|
||||
*/
|
||||
async scanHandlers(patterns: string[] = this.options.patterns || []): Promise<void> {
|
||||
this.logger.info('Starting handler scan', { patterns });
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const files = await glob(pattern, { absolute: true });
|
||||
this.logger.debug(`Found ${files.length} files for pattern: ${pattern}`);
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
await this.scanFile(file);
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to scan file', { file, error });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.info('Handler scan complete', {
|
||||
discovered: this.discoveredHandlers.size,
|
||||
patterns,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan a single file for handlers
|
||||
*/
|
||||
private async scanFile(filePath: string): Promise<void> {
|
||||
try {
|
||||
const module = await import(filePath);
|
||||
this.registerHandlersFromModule(module, filePath);
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to import module', { filePath, error });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register handlers found in a module
|
||||
*/
|
||||
private registerHandlersFromModule(module: any, filePath: string): void {
|
||||
for (const [exportName, exported] of Object.entries(module)) {
|
||||
if (this.isHandler(exported)) {
|
||||
this.registerHandler(exported, exportName, filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an exported value is a handler
|
||||
*/
|
||||
private isHandler(exported: any): boolean {
|
||||
if (typeof exported !== 'function') return false;
|
||||
|
||||
// Check for handler metadata added by decorators
|
||||
const hasHandlerName = !!(exported as any).__handlerName;
|
||||
const hasOperations = Array.isArray((exported as any).__operations);
|
||||
|
||||
return hasHandlerName && hasOperations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a handler with the registry and DI container
|
||||
*/
|
||||
private registerHandler(HandlerClass: any, exportName: string, filePath: string): void {
|
||||
const handlerName = HandlerClass.__handlerName;
|
||||
const operations = HandlerClass.__operations || [];
|
||||
const schedules = HandlerClass.__schedules || [];
|
||||
const isDisabled = HandlerClass.__disabled || false;
|
||||
|
||||
if (isDisabled) {
|
||||
this.logger.debug('Skipping disabled handler', { handlerName });
|
||||
return;
|
||||
}
|
||||
|
||||
// Build metadata
|
||||
const metadata: HandlerMetadata = {
|
||||
name: handlerName,
|
||||
service: this.options.serviceName,
|
||||
operations: operations.map((op: any) => ({
|
||||
name: op.name,
|
||||
method: op.method,
|
||||
})),
|
||||
schedules: schedules.map((schedule: any) => ({
|
||||
operation: schedule.operation,
|
||||
cronPattern: schedule.cronPattern,
|
||||
priority: schedule.priority,
|
||||
immediately: schedule.immediately,
|
||||
description: schedule.description,
|
||||
})),
|
||||
};
|
||||
|
||||
// Build configuration with operation handlers
|
||||
const operationHandlers: Record<string, any> = {};
|
||||
for (const op of operations) {
|
||||
operationHandlers[op.name] = createJobHandler(async payload => {
|
||||
const handler = this.container.resolve<IHandler>(handlerName);
|
||||
const context: ExecutionContext = {
|
||||
type: 'queue',
|
||||
metadata: { source: 'queue', timestamp: Date.now() },
|
||||
};
|
||||
return await handler.execute(op.name, payload, context);
|
||||
});
|
||||
}
|
||||
|
||||
const configuration: HandlerConfiguration = {
|
||||
name: handlerName,
|
||||
operations: operationHandlers,
|
||||
scheduledJobs: schedules.map((schedule: any) => {
|
||||
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}`,
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
// Register with registry
|
||||
this.registry.register(metadata, configuration);
|
||||
|
||||
// Register with DI container if auto-register is enabled
|
||||
if (this.options.autoRegister !== false) {
|
||||
this.container.register({
|
||||
[handlerName]: asClass(HandlerClass).singleton(),
|
||||
});
|
||||
}
|
||||
|
||||
// Track discovered handler
|
||||
this.discoveredHandlers.set(handlerName, HandlerClass);
|
||||
|
||||
this.logger.info('Registered handler', {
|
||||
handlerName,
|
||||
exportName,
|
||||
filePath,
|
||||
operations: operations.length,
|
||||
schedules: schedules.length,
|
||||
service: this.options.serviceName,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all discovered handlers
|
||||
*/
|
||||
getDiscoveredHandlers(): Map<string, any> {
|
||||
return new Map(this.discoveredHandlers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually register a handler class
|
||||
*/
|
||||
registerHandlerClass(HandlerClass: any, options: { serviceName?: string } = {}): void {
|
||||
const serviceName = options.serviceName || this.options.serviceName;
|
||||
const originalServiceName = this.options.serviceName;
|
||||
|
||||
// Temporarily override service name if provided
|
||||
if (serviceName) {
|
||||
this.options.serviceName = serviceName;
|
||||
}
|
||||
|
||||
this.registerHandler(HandlerClass, HandlerClass.name, 'manual');
|
||||
|
||||
// Restore original service name
|
||||
this.options.serviceName = originalServiceName;
|
||||
}
|
||||
}
|
||||
2
libs/core/di/src/scanner/index.ts
Normal file
2
libs/core/di/src/scanner/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { HandlerScanner } from './handler-scanner';
|
||||
export type { HandlerScannerOptions } from './handler-scanner';
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { AwilixContainer } from 'awilix';
|
||||
import type { ServiceDefinitions } from '../container/types';
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
import type { ServiceDefinitions } from '../container/types';
|
||||
|
||||
interface ServiceWithLifecycle {
|
||||
connect?: () => Promise<void>;
|
||||
|
|
@ -29,13 +29,16 @@ export class ServiceLifecycleManager {
|
|||
|
||||
for (const { name, key } of this.services) {
|
||||
const service = container.cradle[key] as ServiceWithLifecycle | null;
|
||||
|
||||
|
||||
if (service) {
|
||||
const initPromise = this.initializeService(name, service);
|
||||
initPromises.push(
|
||||
Promise.race([
|
||||
initPromise,
|
||||
this.createTimeoutPromise(timeout, `${name} initialization timed out after ${timeout}ms`),
|
||||
this.createTimeoutPromise(
|
||||
timeout,
|
||||
`${name} initialization timed out after ${timeout}ms`
|
||||
),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
|
@ -51,7 +54,7 @@ export class ServiceLifecycleManager {
|
|||
// Shutdown in reverse order
|
||||
for (const { name, key } of [...this.services].reverse()) {
|
||||
const service = container.cradle[key] as ServiceWithLifecycle | null;
|
||||
|
||||
|
||||
if (service) {
|
||||
shutdownPromises.push(this.shutdownService(name, service));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue