UNPKG

@clipwhisperer/common

Version:

ClipWhisperer Common - Shared library providing core utilities, database schemas, authentication, bucket management, and common functionality across all ClipWhisperer microservices

231 lines (205 loc) 7.4 kB
import { z } from 'zod'; /** * Environment validation schema */ export const EnvironmentSchema = z.enum(['development', 'staging', 'production', 'test']); /** * Restart policy schema */ export const RestartPolicySchema = z.object({ enabled: z.boolean().default(true), maxAttempts: z.number().int().min(0).default(3), delay: z.number().int().positive().default(5000), backoffMultiplier: z.number().positive().default(1.5), maxDelay: z.number().int().positive().default(30000), }); /** * Service configuration schema */ export const ServiceConfigItemSchema = z.object({ name: z.string().min(1), port: z.number().int().min(1000).max(65535), healthEndpoint: z.string(), startCommand: z.string().min(1), workingDirectory: z.string().optional(), dependencies: z.array(z.string()).optional(), restartPolicy: RestartPolicySchema.optional(), timeout: z.number().int().positive().default(30000), healthCheckInterval: z.number().int().positive().default(5000), gracefulShutdownTimeout: z.number().int().positive().default(5000), env: z.record(z.string()).optional(), }); /** * Manager configuration schema */ export const ManagerConfigSchema = z.object({ environment: EnvironmentSchema.default('development'), logLevel: z.enum(['error', 'warn', 'info', 'debug', 'trace']).default('info'), maxConcurrentServices: z.number().int().positive().default(10), globalTimeout: z.number().int().positive().default(60000), healthCheckInterval: z.number().int().positive().default(5000), enableMetrics: z.boolean().default(true), enableAutoRestart: z.boolean().default(true), services: z.array(ServiceConfigItemSchema).default([]), }); /** * CLI configuration schema */ export const CLIConfigSchema = z.object({ interactive: z.boolean().default(false), watch: z.boolean().default(false), verbose: z.boolean().default(false), colors: z.boolean().default(true), updateInterval: z.number().int().positive().default(1000), maxLogLines: z.number().int().positive().default(100), theme: z.enum(['default', 'minimal', 'compact']).default('default'), }); // Export types export type Environment = z.infer<typeof EnvironmentSchema>; export type ServiceConfigItem = z.infer<typeof ServiceConfigItemSchema>; export type ManagerConfig = z.infer<typeof ManagerConfigSchema>; export type CLIConfig = z.infer<typeof CLIConfigSchema>; export type RestartPolicy = z.infer<typeof RestartPolicySchema>; /** * Default service configurations for ClipWhisperer */ export const DefaultServiceConfigs: ServiceConfigItem[] = [ { name: 'Scraper', port: 9001, healthEndpoint: 'http://localhost:9001/health', startCommand: 'npm start', workingDirectory: 'Scraper', timeout: 30000, healthCheckInterval: 5000, gracefulShutdownTimeout: 5000, }, { name: 'Narrator', port: 9002, healthEndpoint: 'http://localhost:9002/health', startCommand: 'npm start', workingDirectory: 'Narrator', dependencies: ['Scraper'], timeout: 30000, healthCheckInterval: 5000, gracefulShutdownTimeout: 5000, }, { name: 'Renderer', port: 9004, healthEndpoint: 'http://localhost:9004/health', startCommand: 'npm start', workingDirectory: 'Renderer', dependencies: ['Narrator'], timeout: 30000, healthCheckInterval: 5000, gracefulShutdownTimeout: 5000, }, { name: 'Hub', port: 9003, healthEndpoint: 'http://localhost:9003/health', startCommand: 'npm start', workingDirectory: 'Hub', dependencies: ['Scraper', 'Narrator', 'Renderer'], timeout: 30000, healthCheckInterval: 5000, gracefulShutdownTimeout: 5000, }, ]; /** * Simple configuration manager for service orchestration */ export class ServiceConfig { private managerConfig: ManagerConfig; constructor(config?: Partial<ManagerConfig>) { // Load environment variables and apply defaults const environment = (process.env.NODE_ENV || 'development') as Environment; this.managerConfig = ManagerConfigSchema.parse({ environment, logLevel: process.env.LOG_LEVEL || 'info', maxConcurrentServices: parseInt(process.env.MAX_CONCURRENT_SERVICES || '10'), globalTimeout: parseInt(process.env.GLOBAL_TIMEOUT || '60000'), healthCheckInterval: parseInt(process.env.HEALTH_CHECK_INTERVAL || '5000'), enableMetrics: process.env.ENABLE_METRICS !== 'false', enableAutoRestart: process.env.ENABLE_AUTO_RESTART !== 'false', services: DefaultServiceConfigs, ...config, }); } public getEnvironment(): Environment { return this.managerConfig.environment; } public getServices(): ServiceConfigItem[] { return this.managerConfig.services; } public getService(serviceName: string): ServiceConfigItem | undefined { return this.managerConfig.services.find(service => service.name === serviceName); } public getManagerConfig(): ManagerConfig { return { ...this.managerConfig }; } public isDevelopment(): boolean { return this.managerConfig.environment === 'development'; } public isProduction(): boolean { return this.managerConfig.environment === 'production'; } public isTest(): boolean { return this.managerConfig.environment === 'test'; } public addService(service: ServiceConfigItem): void { const validatedService = ServiceConfigItemSchema.parse(service); this.managerConfig.services.push(validatedService); } public removeService(serviceName: string): boolean { const index = this.managerConfig.services.findIndex(service => service.name === serviceName); if (index !== -1) { this.managerConfig.services.splice(index, 1); return true; } return false; } public validateDependencies(): { valid: boolean; errors: string[] } { const errors: string[] = []; const serviceNames = new Set(this.managerConfig.services.map(s => s.name)); // Check if all dependencies exist for (const service of this.managerConfig.services) { if (service.dependencies) { for (const dep of service.dependencies) { if (!serviceNames.has(dep)) { errors.push(`Service '${service.name}' depends on '${dep}', but '${dep}' is not configured`); } } } } // Check for circular dependencies const visited = new Set<string>(); const visiting = new Set<string>(); const hasCycle = (serviceName: string): boolean => { if (visited.has(serviceName)) return false; if (visiting.has(serviceName)) return true; visiting.add(serviceName); const service = this.getService(serviceName); if (service?.dependencies) { for (const dep of service.dependencies) { if (hasCycle(dep)) return true; } } visiting.delete(serviceName); visited.add(serviceName); return false; }; for (const service of this.managerConfig.services) { if (hasCycle(service.name)) { errors.push(`Circular dependency detected involving service '${service.name}'`); break; } } return { valid: errors.length === 0, errors, }; } }