@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
text/typescript
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,
};
}
}