@clipwhisperer/common
Version:
ClipWhisperer Common - Shared library providing core utilities, database schemas, authentication, bucket management, and common functionality across all ClipWhisperer microservices
674 lines (673 loc) • 26.7 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.EnterpriseServiceManager = void 0;
const axios_1 = __importStar(require("axios"));
const child_process_1 = require("child_process");
const events_1 = require("events");
const services_1 = require("../../schemas/services");
const services_2 = require("../../types/services");
const Logger_1 = require("../services/Logger");
/**
* Enterprise Service Manager - Core orchestration component
*
* This class provides enterprise-grade service management capabilities including:
* - Dependency-aware startup/shutdown
* - Health monitoring with circuit breaker patterns
* - Event-driven architecture
* - Auto-recovery and restart policies
* - Process lifecycle management
* - Service discovery and registry
*
* @example
* ```typescript
* const serviceManager = new EnterpriseServiceManager();
* await serviceManager.start();
* ```
*/
/**
* Event Bus Implementation
* Handles inter-service communication and event propagation
*/
class EventBus {
constructor() {
this.emitter = new events_1.EventEmitter();
this.eventHistory = [];
this.logger = Logger_1.Logger.getInstance().child({ component: 'EventBus' });
}
emit(event) {
this.eventHistory.push({
...event,
timestamp: new Date()
});
// Keep only last 1000 events to prevent memory leaks
if (this.eventHistory.length > 1000) {
this.eventHistory = this.eventHistory.slice(-1000);
}
this.logger.info('Event emitted', {
type: event.type,
serviceName: event.serviceName,
data: event.data
});
this.emitter.emit(event.type, event);
this.emitter.emit('*', event); // Wildcard listener
}
on(eventType, listener) {
this.emitter.on(eventType, listener);
}
off(eventType, listener) {
this.emitter.off(eventType, listener);
}
getEventHistory() {
return [...this.eventHistory];
}
clearHistory() {
this.eventHistory = [];
this.logger.info('Event history cleared');
}
}
/**
* Service Registry Implementation
* Manages service discovery and configuration
*/
class ServiceRegistry {
constructor() {
this.services = new Map();
this.logger = Logger_1.Logger.getInstance().child({ component: 'ServiceRegistry' });
}
register(service) {
this.services.set(service.name, {
...service,
registeredAt: new Date()
});
this.logger.info('Service registered', {
serviceName: service.name,
port: service.port,
dependencies: service.dependencies
});
}
get(serviceName) {
return this.services.get(serviceName);
}
getAll() {
return Array.from(this.services.values());
}
unregister(serviceName) {
const removed = this.services.delete(serviceName);
if (removed) {
this.logger.info('Service unregistered', { serviceName });
}
return removed;
}
/**
* Get services in dependency order using topological sorting
*/
getDependencyOrder() {
const services = Array.from(this.services.values());
const visited = new Set();
const visiting = new Set();
const result = [];
const visit = (serviceName) => {
if (visited.has(serviceName))
return;
if (visiting.has(serviceName)) {
throw new services_2.DependencyError(`Circular dependency detected involving ${serviceName}`);
}
visiting.add(serviceName);
const service = this.services.get(serviceName);
if (service === null || service === void 0 ? void 0 : service.dependencies) {
for (const dep of service.dependencies) {
visit(dep);
}
}
visiting.delete(serviceName);
visited.add(serviceName);
result.push(serviceName);
};
for (const service of services) {
visit(service.name);
}
return result;
}
}
/**
* Health Checker Implementation
* Monitors service health with circuit breaker patterns
*/
class HealthChecker {
constructor() {
this.healthStatus = new Map();
this.checkIntervals = new Map();
this.circuitBreakers = new Map();
this.logger = Logger_1.Logger.getInstance().child({ component: 'HealthChecker' });
this.CIRCUIT_BREAKER_THRESHOLD = 3;
this.CIRCUIT_BREAKER_TIMEOUT = 30000; // 30 seconds
}
async checkHealth(serviceName, url) {
var _a;
const startTime = Date.now();
const correlationId = Logger_1.Logger.generateCorrelationId();
try {
// Check circuit breaker
const breaker = this.circuitBreakers.get(serviceName);
if (breaker === null || breaker === void 0 ? void 0 : breaker.isOpen) {
const timeSinceLastFailure = Date.now() - breaker.lastFailure.getTime();
if (timeSinceLastFailure < this.CIRCUIT_BREAKER_TIMEOUT) {
const result = {
serviceName,
status: 'unhealthy',
timestamp: new Date(),
responseTime: 0,
error: 'Circuit breaker is open'
};
this.healthStatus.set(serviceName, result);
return result;
}
else {
// Reset circuit breaker
this.circuitBreakers.set(serviceName, { failures: 0, lastFailure: new Date(), isOpen: false });
}
}
const response = await axios_1.default.get(url, {
timeout: 5000,
headers: { 'X-Correlation-ID': correlationId }
});
const responseTime = Date.now() - startTime;
const result = {
serviceName,
status: response.status === 200 ? 'healthy' : 'unhealthy',
timestamp: new Date(),
responseTime,
details: response.data
};
// Reset circuit breaker on success
this.circuitBreakers.set(serviceName, { failures: 0, lastFailure: new Date(), isOpen: false });
this.healthStatus.set(serviceName, result);
this.logger.info('Health check completed', {
serviceName,
status: result.status,
responseTime,
correlationId
});
return result;
}
catch (error) {
const responseTime = Date.now() - startTime;
// Update circuit breaker
const currentBreaker = this.circuitBreakers.get(serviceName) || { failures: 0, lastFailure: new Date(), isOpen: false };
currentBreaker.failures++;
currentBreaker.lastFailure = new Date();
if (currentBreaker.failures >= this.CIRCUIT_BREAKER_THRESHOLD) {
currentBreaker.isOpen = true;
this.logger.warn('Circuit breaker opened', { serviceName, failures: currentBreaker.failures });
}
this.circuitBreakers.set(serviceName, currentBreaker);
const errorMessage = error instanceof axios_1.AxiosError
? `HTTP ${(_a = error.response) === null || _a === void 0 ? void 0 : _a.status}: ${error.message}`
: error instanceof Error ? error.message : String(error);
const result = {
serviceName,
status: 'unhealthy',
timestamp: new Date(),
responseTime,
error: errorMessage
};
this.healthStatus.set(serviceName, result);
this.logger.error('Health check failed', {
serviceName,
error: errorMessage,
responseTime,
correlationId
});
return result;
}
}
startMonitoring(serviceName, url, intervalMs = 5000) {
this.stopMonitoring(serviceName); // Stop any existing monitoring
const interval = setInterval(async () => {
await this.checkHealth(serviceName, url);
}, intervalMs);
this.checkIntervals.set(serviceName, interval);
this.logger.info('Health monitoring started', { serviceName, intervalMs });
}
stopMonitoring(serviceName) {
const interval = this.checkIntervals.get(serviceName);
if (interval) {
clearInterval(interval);
this.checkIntervals.delete(serviceName);
this.logger.info('Health monitoring stopped', { serviceName });
}
}
getStatus(serviceName) {
return this.healthStatus.get(serviceName);
}
getAllStatus() {
return new Map(this.healthStatus);
}
resetCircuitBreaker(serviceName) {
this.circuitBreakers.set(serviceName, { failures: 0, lastFailure: new Date(), isOpen: false });
this.logger.info('Circuit breaker reset', { serviceName });
}
}
/**
* Process Manager Implementation
* Handles process lifecycle management
*/
class ProcessManager {
constructor() {
this.processes = new Map();
this.logger = Logger_1.Logger.getInstance().child({ component: 'ProcessManager' });
}
async start(serviceName, command, args, cwd) {
var _a, _b;
try {
// Kill existing process if running
await this.stop(serviceName);
const childProcess = (0, child_process_1.spawn)(command, args, {
cwd: cwd || process.cwd(),
stdio: ['pipe', 'pipe', 'pipe'],
shell: true
});
const processInfo = {
serviceName,
pid: childProcess.pid,
command,
args,
startTime: new Date(),
status: 'running',
process: childProcess
};
// Setup process event handlers
childProcess.on('error', (error) => {
this.logger.error('Process error', { serviceName, error: error.message });
processInfo.status = 'failed';
processInfo.exitCode = -1;
processInfo.exitTime = new Date();
});
childProcess.on('exit', (code, signal) => {
this.logger.info('Process exited', { serviceName, code, signal });
processInfo.status = code === 0 ? 'stopped' : 'failed';
processInfo.exitCode = code !== null && code !== void 0 ? code : undefined;
processInfo.exitSignal = signal !== null && signal !== void 0 ? signal : undefined;
processInfo.exitTime = new Date();
});
// Log output
(_a = childProcess.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (data) => {
const output = data.toString().trim();
if (output) {
this.logger.info(`[${serviceName}] ${output}`);
}
});
(_b = childProcess.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (data) => {
const output = data.toString().trim();
if (output) {
this.logger.error(`[${serviceName}] ${output}`);
}
});
this.processes.set(serviceName, processInfo);
this.logger.info('Process started', { serviceName, pid: childProcess.pid, command });
return processInfo;
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.logger.error('Failed to start process', { serviceName, error: errorMessage });
throw new services_2.ProcessError(`Failed to start ${serviceName}: ${errorMessage}`, serviceName);
}
}
async stop(serviceName, signal = 'SIGTERM') {
const processInfo = this.processes.get(serviceName);
if (!processInfo || !processInfo.process) {
return false;
}
try {
return new Promise((resolve) => {
const childProcess = processInfo.process;
let resolved = false;
const cleanup = () => {
if (!resolved) {
resolved = true;
processInfo.status = 'stopped';
processInfo.exitTime = new Date();
this.logger.info('Process stopped', { serviceName, signal });
resolve(true);
}
};
childProcess.on('exit', cleanup);
// Force kill after timeout
const forceKillTimer = setTimeout(() => {
if (!resolved && !childProcess.killed) {
this.logger.warn('Force killing process', { serviceName });
childProcess.kill('SIGKILL');
cleanup();
}
}, 10000);
childProcess.kill(signal);
// Clean up timer if process exits normally
childProcess.on('exit', () => clearTimeout(forceKillTimer));
});
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.logger.error('Failed to stop process', { serviceName, error: errorMessage });
return false;
}
}
getProcess(serviceName) {
return this.processes.get(serviceName);
}
getAllProcesses() {
return Array.from(this.processes.values());
}
isRunning(serviceName) {
var _a;
const processInfo = this.processes.get(serviceName);
return !!((processInfo === null || processInfo === void 0 ? void 0 : processInfo.status) === 'running' && processInfo.process && !((_a = processInfo.process.killed) !== null && _a !== void 0 ? _a : false));
}
async restart(serviceName) {
const processInfo = this.processes.get(serviceName);
if (!processInfo) {
throw new services_2.ProcessError(`Service ${serviceName} not found`, serviceName);
}
await this.stop(serviceName);
return this.start(serviceName, processInfo.command, processInfo.args);
}
}
/**
* Enterprise Service Manager
* Main orchestrator implementing dependency injection and enterprise patterns
*/
class EnterpriseServiceManager {
constructor(config) {
this.logger = Logger_1.Logger.getInstance().child({ component: 'EnterpriseServiceManager' });
this.isRunning = false;
this.config = new services_1.ServiceConfig(config);
this.eventBus = new EventBus();
this.serviceRegistry = new ServiceRegistry();
this.healthChecker = new HealthChecker();
this.processManager = new ProcessManager();
// Setup event listeners
this.setupEventListeners();
this.logger.info('EnterpriseServiceManager initialized', {
environment: this.config.getEnvironment(),
servicesCount: this.config.getServices().length
});
}
setupEventListeners() {
this.eventBus.on('service:unhealthy', (event) => {
this.handleUnhealthyService(event);
});
this.eventBus.on('service:failed', (event) => {
this.handleFailedService(event);
});
this.eventBus.on('process:exit', (event) => {
this.handleProcessExit(event);
});
}
async handleUnhealthyService(event) {
var _a;
const serviceName = event.serviceName;
const serviceConfig = this.config.getService(serviceName);
if ((_a = serviceConfig === null || serviceConfig === void 0 ? void 0 : serviceConfig.restartPolicy) === null || _a === void 0 ? void 0 : _a.enabled) {
this.logger.warn('Attempting service recovery', { serviceName });
try {
await this.processManager.restart(serviceName);
this.eventBus.emit({
type: 'service:recovered',
serviceName,
data: { reason: 'unhealthy_restart' }
});
}
catch (error) {
this.logger.error('Service recovery failed', {
serviceName,
error: error instanceof Error ? error.message : String(error)
});
}
}
}
async handleFailedService(event) {
this.logger.error('Service failed', { serviceName: event.serviceName, data: event.data });
}
async handleProcessExit(event) {
this.logger.info('Process exited', { serviceName: event.serviceName, data: event.data });
}
async start() {
if (this.isRunning) {
throw new services_2.ServiceError('Service manager is already running');
}
this.logger.info('Starting Enterprise Service Manager');
try {
// Register all services
const services = this.config.getServices();
for (const service of services) {
this.serviceRegistry.register({
name: service.name,
port: service.port,
healthEndpoint: service.healthEndpoint,
dependencies: service.dependencies,
command: service.startCommand
});
}
// Start services in dependency order
const startOrder = this.serviceRegistry.getDependencyOrder();
this.logger.info('Starting services in order', { order: startOrder });
for (const serviceName of startOrder) {
await this.startService(serviceName);
// Wait a bit between service starts
await new Promise(resolve => setTimeout(resolve, 2000));
}
// Start health monitoring
this.startHealthMonitoring();
this.isRunning = true;
this.eventBus.emit({
type: 'manager:started',
serviceName: 'enterprise-manager',
data: { servicesCount: services.length }
});
this.logger.info('Enterprise Service Manager started successfully');
}
catch (error) {
this.logger.error('Failed to start Enterprise Service Manager', {
error: error instanceof Error ? error.message : String(error)
});
throw error;
}
}
async stop() {
if (!this.isRunning) {
return;
}
this.logger.info('Stopping Enterprise Service Manager');
try {
// Stop health monitoring
this.stopHealthMonitoring();
// Stop services in reverse dependency order
const stopOrder = this.serviceRegistry.getDependencyOrder().reverse();
this.logger.info('Stopping services in order', { order: stopOrder });
for (const serviceName of stopOrder) {
await this.stopService(serviceName);
}
this.isRunning = false;
this.eventBus.emit({
type: 'manager:stopped',
serviceName: 'enterprise-manager',
data: {}
});
this.logger.info('Enterprise Service Manager stopped successfully');
}
catch (error) {
this.logger.error('Error during shutdown', {
error: error instanceof Error ? error.message : String(error)
});
throw error;
}
}
async startService(serviceName) {
const serviceInfo = this.serviceRegistry.get(serviceName);
if (!serviceInfo) {
throw new services_2.ServiceError(`Service ${serviceName} not found in registry`, serviceName);
}
const serviceConfig = this.config.getService(serviceName);
if (!serviceConfig) {
throw new services_2.ServiceError(`Service configuration for ${serviceName} not found`, serviceName);
}
try {
this.logger.info('Starting service', { serviceName });
// Check dependencies are healthy
if (serviceInfo.dependencies) {
for (const dep of serviceInfo.dependencies) {
const depHealth = this.healthChecker.getStatus(dep);
if (!depHealth || depHealth.status !== 'healthy') {
throw new services_2.DependencyError(`Dependency ${dep} is not healthy`, serviceName, dep);
}
}
}
// Start the process
const [command, ...args] = serviceConfig.startCommand.split(' ');
await this.processManager.start(serviceName, command, args, serviceConfig.workingDirectory);
// Wait for service to be ready
await this.waitForServiceReady(serviceName, serviceInfo.healthEndpoint);
this.eventBus.emit({
type: 'service:started',
serviceName,
data: { port: serviceInfo.port }
});
this.logger.info('Service started successfully', { serviceName });
}
catch (error) {
this.logger.error('Failed to start service', {
serviceName,
error: error instanceof Error ? error.message : String(error)
});
throw error;
}
}
async stopService(serviceName) {
try {
this.logger.info('Stopping service', { serviceName });
// Stop health monitoring for this service
this.healthChecker.stopMonitoring(serviceName);
// Stop the process
await this.processManager.stop(serviceName);
this.eventBus.emit({
type: 'service:stopped',
serviceName,
data: {}
});
this.logger.info('Service stopped successfully', { serviceName });
}
catch (error) {
this.logger.error('Failed to stop service', {
serviceName,
error: error instanceof Error ? error.message : String(error)
});
throw error;
}
}
async waitForServiceReady(serviceName, healthEndpoint, maxAttempts = 30) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
const result = await this.healthChecker.checkHealth(serviceName, healthEndpoint);
if (result.status === 'healthy') {
return;
}
}
catch (error) {
// Expected during startup
}
if (attempt === maxAttempts) {
throw new services_2.HealthCheckError(`Service ${serviceName} failed to become healthy after ${maxAttempts} attempts`, serviceName, healthEndpoint);
}
await new Promise(resolve => setTimeout(resolve, 2000));
}
}
startHealthMonitoring() {
const services = this.serviceRegistry.getAll();
for (const service of services) {
this.healthChecker.startMonitoring(service.name, service.healthEndpoint);
}
}
stopHealthMonitoring() {
const services = this.serviceRegistry.getAll();
for (const service of services) {
this.healthChecker.stopMonitoring(service.name);
}
}
getStatus() {
const status = {};
const services = this.serviceRegistry.getAll();
for (const service of services) {
const processInfo = this.processManager.getProcess(service.name);
const healthInfo = this.healthChecker.getStatus(service.name);
if (!processInfo) {
status[service.name] = services_2.ServiceStatus.STOPPED;
}
else if (processInfo.status === 'failed') {
status[service.name] = services_2.ServiceStatus.FAILED;
}
else if ((healthInfo === null || healthInfo === void 0 ? void 0 : healthInfo.status) === 'healthy') {
status[service.name] = services_2.ServiceStatus.RUNNING;
}
else {
status[service.name] = services_2.ServiceStatus.STARTING;
}
}
return status;
}
getServiceInfo() {
return this.serviceRegistry.getAll();
}
getEventHistory() {
return this.eventBus.getEventHistory();
}
// Dependency injection getters
getEventBus() {
return this.eventBus;
}
getServiceRegistry() {
return this.serviceRegistry;
}
getHealthChecker() {
return this.healthChecker;
}
getProcessManager() {
return this.processManager;
}
getConfig() {
return this.config;
}
}
exports.EnterpriseServiceManager = EnterpriseServiceManager;
exports.default = EnterpriseServiceManager;