From caf1c5fcaff367f40403e7028a61c23ae8da3835 Mon Sep 17 00:00:00 2001 From: Boki Date: Fri, 20 Jun 2025 20:47:31 -0400 Subject: [PATCH] made config async --- apps/data-service/src/index.ts | 5 +- libs/config/src/config-manager.ts | 63 +++------------ libs/config/src/index.ts | 108 ++++++++++++------------- libs/config/src/loaders/env.loader.ts | 22 +++-- libs/config/src/loaders/file.loader.ts | 34 ++++---- libs/config/src/types/index.ts | 2 +- 6 files changed, 91 insertions(+), 143 deletions(-) diff --git a/apps/data-service/src/index.ts b/apps/data-service/src/index.ts index 21c40ce..f5f41f4 100644 --- a/apps/data-service/src/index.ts +++ b/apps/data-service/src/index.ts @@ -1,7 +1,7 @@ // Framework imports import { Hono } from 'hono'; import { cors } from 'hono/cors'; -import { initializeConfigSync } from '@stock-bot/config'; +import { initializeConfig } from '@stock-bot/config'; // Library imports import { getLogger, setLoggerConfig, shutdownLoggers } from '@stock-bot/logger'; import { connectMongoDB } from '@stock-bot/mongodb-client'; @@ -12,7 +12,8 @@ import { ProxyManager } from '@stock-bot/utils'; // Local imports import { exchangeRoutes, healthRoutes, queueRoutes } from './routes'; -const config = initializeConfigSync(); +const config = initializeConfig(); +console.log('Configuration loaded:', config); const serviceConfig = config.service; const databaseConfig = config.database; const queueConfig = config.queue; diff --git a/libs/config/src/config-manager.ts b/libs/config/src/config-manager.ts index e06d098..717c308 100644 --- a/libs/config/src/config-manager.ts +++ b/libs/config/src/config-manager.ts @@ -33,9 +33,9 @@ export class ConfigManager> { } /** - * Initialize the configuration by loading from all sources + * Initialize the configuration by loading from all sources synchronously. */ - async initialize(schema?: ConfigSchema): Promise { + initialize(schema?: ConfigSchema): T { if (this.config) { return this.config; } @@ -48,7 +48,8 @@ export class ConfigManager> { // Load configurations from all sources const configs: Record[] = []; for (const loader of sortedLoaders) { - const config = await loader.load(); + // Assuming all loaders now have a synchronous `load` method + const config = loader.load(); if (config && Object.keys(config).length > 0) { configs.push(config); } @@ -82,51 +83,6 @@ export class ConfigManager> { return this.config; } - /** - * Initialize the configuration synchronously (only env vars, no file loading) - */ - initializeSync(schema?: ConfigSchema): T { - if (this.config) { - return this.config; - } - - this.schema = schema; - - // Only use EnvLoader for sync initialization - const envLoader = this.loaders.find(loader => loader.constructor.name === 'EnvLoader'); - if (!envLoader) { - throw new ConfigError('No EnvLoader found for synchronous initialization'); - } - - // Load env vars synchronously - const envLoaderInstance = envLoader as any; - const config = envLoaderInstance.loadSync ? envLoaderInstance.loadSync() : {}; - - // Add environment if not present - if (typeof config === 'object' && config !== null && !('environment' in config)) { - (config as Record)['environment'] = this.environment; - } - - // Validate if schema provided - if (this.schema) { - try { - this.config = this.schema.parse(config) as T; - } catch (error) { - if (error instanceof z.ZodError) { - throw new ConfigValidationError( - 'Configuration validation failed', - error.errors - ); - } - throw error; - } - } else { - this.config = config as T; - } - - return this.config; - } - /** * Get the current configuration */ @@ -176,7 +132,7 @@ export class ConfigManager> { throw new ConfigError('Configuration not initialized. Call initialize() first.'); } - const updated = this.deepMerge(this.config as any, updates as any) as T; + const updated = this.deepMerge(this.config as Record, updates as Record) as T; // Re-validate if schema is present if (this.schema) { @@ -240,8 +196,8 @@ export class ConfigManager> { } } - private deepMerge(...objects: Record[]): Record { - const result: Record = {}; + private deepMerge(...objects: Record[]): Record { + const result: Record = {}; for (const obj of objects) { for (const [key, value] of Object.entries(obj)) { @@ -253,7 +209,10 @@ export class ConfigManager> { !(value instanceof Date) && !(value instanceof RegExp) ) { - result[key] = this.deepMerge(result[key] || {}, value); + result[key] = this.deepMerge( + (result[key] as Record) || {} as Record, + value as Record + ); } else { result[key] = value; } diff --git a/libs/config/src/index.ts b/libs/config/src/index.ts index 93ee4f5..1097816 100644 --- a/libs/config/src/index.ts +++ b/libs/config/src/index.ts @@ -1,28 +1,8 @@ -// Export all schemas -export * from './schemas'; - -// Export types -export * from './types'; - -// Export errors -export * from './errors'; - -// Export loaders -export { EnvLoader } from './loaders/env.loader'; -export { FileLoader } from './loaders/file.loader'; - -// Export ConfigManager -export { ConfigManager } from './config-manager'; - -// Export utilities -export * from './utils/secrets'; -export * from './utils/validation'; - // Import necessary types for singleton +import { EnvLoader } from './loaders/env.loader'; +import { FileLoader } from './loaders/file.loader'; import { ConfigManager } from './config-manager'; import { AppConfig, appConfigSchema } from './schemas'; -import { FileLoader } from './loaders/file.loader'; -import { EnvLoader } from './loaders/env.loader'; // Create singleton instance let configInstance: ConfigManager | null = null; @@ -37,30 +17,36 @@ function loadCriticalEnvVarsSync(): void { if (fs.existsSync(envPath)) { const envContent = fs.readFileSync(envPath, 'utf-8'); const lines = envContent.split('\n'); - + for (const line of lines) { const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith('#')) continue; - + if (!trimmed || trimmed.startsWith('#')) { + continue; + } + const equalIndex = trimmed.indexOf('='); - if (equalIndex === -1) continue; - + if (equalIndex === -1) { + continue; + } + const key = trimmed.substring(0, equalIndex).trim(); let value = trimmed.substring(equalIndex + 1).trim(); - + // Remove surrounding quotes - if ((value.startsWith('"') && value.endsWith('"')) || - (value.startsWith("'") && value.endsWith("'"))) { + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { value = value.slice(1, -1); } - + // Only set if not already set if (!(key in process.env)) { process.env[key] = value; } } } - } catch (error) { + } catch { // Ignore errors - env file is optional } } @@ -69,26 +55,16 @@ function loadCriticalEnvVarsSync(): void { loadCriticalEnvVarsSync(); /** - * Initialize configuration synchronously (env vars only) - * This should be called at the very start of the application + * Initialize the global configuration synchronously. + * + * This loads configuration from all sources in the correct hierarchy: + * 1. Schema defaults (lowest priority) + * 2. default.json + * 3. [environment].json (e.g., development.json) + * 4. .env file values + * 5. process.env values (highest priority) */ -export function initializeConfigSync(): AppConfig { - if (!configInstance) { - configInstance = new ConfigManager({ - loaders: [ - new EnvLoader(''), // Environment variables only for sync - ] - }); - } - return configInstance.initializeSync(appConfigSchema); -} - -/** - * Initialize the global configuration - */ -export async function initializeConfig( - configPath?: string -): Promise { +export function initializeConfig(configPath?: string): AppConfig { if (!configInstance) { configInstance = new ConfigManager({ configPath, @@ -98,21 +74,21 @@ export async function initializeConfig( } /** - * Initialize configuration for a service in a monorepo + * Initialize configuration for a service in a monorepo. * Automatically loads configs from: * 1. Root config directory (../../config) * 2. Service-specific config directory (./config) * 3. Environment variables */ -export async function initializeServiceConfig(): Promise { +export function initializeServiceConfig(): AppConfig { if (!configInstance) { const environment = process.env.NODE_ENV || 'development'; configInstance = new ConfigManager({ loaders: [ new FileLoader('../../config', environment), // Root config - new FileLoader('./config', environment), // Service config + new FileLoader('./config', environment), // Service config new EnvLoader(''), // Environment variables - ] + ], }); } return configInstance.initialize(appConfigSchema); @@ -185,4 +161,24 @@ export function isProduction(): boolean { export function isTest(): boolean { return getConfig().environment === 'test'; -} \ No newline at end of file +} + +// Export all schemas +export * from './schemas'; + +// Export types +export * from './types'; + +// Export errors +export * from './errors'; + +// Export loaders +export { EnvLoader } from './loaders/env.loader'; +export { FileLoader } from './loaders/file.loader'; + +// Export ConfigManager +export { ConfigManager } from './config-manager'; + +// Export utilities +export * from './utils/secrets'; +export * from './utils/validation'; diff --git a/libs/config/src/loaders/env.loader.ts b/libs/config/src/loaders/env.loader.ts index 7d444ce..50ed7da 100644 --- a/libs/config/src/loaders/env.loader.ts +++ b/libs/config/src/loaders/env.loader.ts @@ -25,11 +25,7 @@ export class EnvLoader implements ConfigLoader { }; } - async load(): Promise> { - return this.loadSync(); - } - - loadSync(): Record { + load(): Record { try { // Load root .env file - try multiple possible locations const possiblePaths = ['./.env', '../.env', '../../.env']; @@ -67,7 +63,7 @@ export class EnvLoader implements ConfigLoader { } } - private setConfigValue(config: Record, key: string, value: string): void { + private setConfigValue(config: Record, key: string, value: string): void { const parsedValue = this.parseValue(value); try { @@ -104,7 +100,7 @@ export class EnvLoader implements ConfigLoader { } } - private setNestedValue(obj: Record, path: string[], value: unknown): boolean { + private setNestedValue(obj: Record, path: string[], value: unknown): boolean { if (path.length === 0) { return false; // Cannot set value on empty path } @@ -115,13 +111,13 @@ export class EnvLoader implements ConfigLoader { try { const target = path.reduce((acc, key) => { - if (!acc[key]) { + if (!acc[key] || typeof acc[key] !== 'object') { acc[key] = {}; } - return acc[key]; + return acc[key] as Record; }, obj); - target[lastKey] = value; + (target as Record)[lastKey] = value; return true; } catch { // If we can't assign to any property (readonly), skip this env var silently @@ -257,11 +253,11 @@ export class EnvLoader implements ConfigLoader { process.env[key] = value; } } - } catch (error: any) { + } catch (error: unknown) { // File not found is not an error (env files are optional) - if (error.code !== 'ENOENT') { + if (error && typeof error === 'object' && 'code' in error && error.code !== 'ENOENT') { // eslint-disable-next-line no-console - console.warn(`Warning: Could not load env file ${filePath}:`, error.message); + console.warn(`Warning: Could not load env file ${filePath}:`, error instanceof Error ? error.message : String(error)); } } } diff --git a/libs/config/src/loaders/file.loader.ts b/libs/config/src/loaders/file.loader.ts index 397718d..a6484f3 100644 --- a/libs/config/src/loaders/file.loader.ts +++ b/libs/config/src/loaders/file.loader.ts @@ -1,4 +1,4 @@ -import { readFile } from 'fs/promises'; +import { readFileSync, existsSync } from 'fs'; import { join } from 'path'; import { ConfigLoader } from '../types'; import { ConfigLoaderError } from '../errors'; @@ -11,18 +11,18 @@ export class FileLoader implements ConfigLoader { private environment: string ) {} - async load(): Promise> { + load(): Record { try { const configs: Record[] = []; // Load default config - const defaultConfig = await this.loadFile('default.json'); + const defaultConfig = this.loadFile('default.json'); if (defaultConfig) { configs.push(defaultConfig); } // Load environment-specific config - const envConfig = await this.loadFile(`${this.environment}.json`); + const envConfig = this.loadFile(`${this.environment}.json`); if (envConfig) { configs.push(envConfig); } @@ -37,30 +37,26 @@ export class FileLoader implements ConfigLoader { } } - private async loadFile(filename: string): Promise | null> { + private loadFile(filename: string): Record | null { const filepath = join(this.configPath, filename); - - try { - const content = await readFile(filepath, 'utf-8'); - return JSON.parse(content); - } catch (error: unknown) { - // File not found is not an error (configs are optional) - if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') { - return null; - } - throw error; + + if (!existsSync(filepath)) { + return null; } + + const content = readFileSync(filepath, 'utf-8'); + return JSON.parse(content); } - private deepMerge(...objects: Record[]): Record { - const result: Record = {}; + private deepMerge(...objects: Record[]): Record { + const result: Record = {}; for (const obj of objects) { for (const [key, value] of Object.entries(obj)) { if (value === null || value === undefined) { result[key] = value; - } else if (typeof value === 'object' && !Array.isArray(value)) { - result[key] = this.deepMerge(result[key] || {}, value); + } else if (typeof value === 'object' && !Array.isArray(value) && value !== null) { + result[key] = this.deepMerge(result[key] as Record || {}, value as Record); } else { result[key] = value; } diff --git a/libs/config/src/types/index.ts b/libs/config/src/types/index.ts index 07863ef..3f7472a 100644 --- a/libs/config/src/types/index.ts +++ b/libs/config/src/types/index.ts @@ -3,7 +3,7 @@ import { z } from 'zod'; export type Environment = 'development' | 'test' | 'production'; export interface ConfigLoader { - load(): Promise>; + load(): Record; readonly priority: number; }