UX-agent / backend /core /ServiceContainer.js
AUXteam's picture
Deploying UX Analyst AI to Hugging Face (V2)
21cac8a verified
const EventEmitter = require('events');
/**
* Service Container for dependency injection and service lifecycle management
*/
class ServiceContainer extends EventEmitter {
constructor() {
super();
this.services = new Map();
this.factories = new Map();
this.singletons = new Map();
this.circuitBreakers = new Map();
this.isShuttingDown = false;
this.startupOrder = [];
this.shutdownOrder = [];
}
/**
* Register a service factory
* @param {string} name - Service name
* @param {Function} factory - Factory function that creates the service
* @param {Object} options - Registration options
* @param {boolean} options.singleton - Whether to create only one instance
* @param {Array<string>} options.dependencies - List of service dependencies
* @param {Object} options.circuitBreaker - Circuit breaker configuration
* @param {number} options.startupPriority - Startup priority (lower = earlier)
* @param {Function} options.healthCheck - Health check function
*/
register(name, factory, options = {}) {
if (this.factories.has(name)) {
throw new Error(`Service '${name}' is already registered`);
}
const serviceConfig = {
factory,
singleton: options.singleton || false,
dependencies: options.dependencies || [],
circuitBreaker: options.circuitBreaker || null,
startupPriority: options.startupPriority || 100,
healthCheck: options.healthCheck || null,
startupTimeout: options.startupTimeout || 30000,
shutdownTimeout: options.shutdownTimeout || 10000
};
this.factories.set(name, serviceConfig);
// Add to startup order based on priority
this.updateStartupOrder(name, serviceConfig.startupPriority);
console.log(`Service '${name}' registered${serviceConfig.singleton ? ' (singleton)' : ''}`);
}
/**
* Register a service instance directly
* @param {string} name - Service name
* @param {*} instance - Service instance
* @param {Object} options - Registration options
*/
registerInstance(name, instance, options = {}) {
if (this.services.has(name)) {
throw new Error(`Service instance '${name}' is already registered`);
}
this.services.set(name, instance);
if (options.healthCheck) {
this.factories.set(name, { healthCheck: options.healthCheck });
}
console.log(`Service instance '${name}' registered`);
}
/**
* Get a service instance
* @param {string} name - Service name
* @returns {*} Service instance
*/
get(name) {
if (this.isShuttingDown) {
throw new Error(`Cannot get service '${name}' during shutdown`);
}
// Return existing instance if available
if (this.services.has(name)) {
return this.services.get(name);
}
// Return singleton if available
if (this.singletons.has(name)) {
return this.singletons.get(name);
}
// Create new instance
return this.createInstance(name);
}
/**
* Create a new service instance
* @param {string} name - Service name
* @returns {*} Service instance
*/
createInstance(name) {
const serviceConfig = this.factories.get(name);
if (!serviceConfig) {
throw new Error(`Service '${name}' is not registered`);
}
// Check for circular dependencies
this.checkCircularDependencies(name, new Set());
// Create dependency instances
const dependencies = {};
for (const depName of serviceConfig.dependencies) {
dependencies[depName] = this.get(depName);
}
// Create service instance
let instance;
try {
instance = serviceConfig.factory(dependencies, this);
} catch (error) {
console.error(`Failed to create service '${name}':`, error);
throw new Error(`Service creation failed for '${name}': ${error.message}`);
}
// Store instance
if (serviceConfig.singleton) {
this.singletons.set(name, instance);
} else {
this.services.set(name, instance);
}
// Set up circuit breaker if configured
if (serviceConfig.circuitBreaker) {
this.setupCircuitBreaker(name, instance, serviceConfig.circuitBreaker);
}
this.emit('serviceCreated', { name, instance, config: serviceConfig });
console.log(`Service '${name}' created successfully`);
return instance;
}
/**
* Check for circular dependencies
* @param {string} name - Service name
* @param {Set} visited - Set of visited services
*/
checkCircularDependencies(name, visited) {
if (visited.has(name)) {
const cycle = Array.from(visited).concat(name).join(' -> ');
throw new Error(`Circular dependency detected: ${cycle}`);
}
const serviceConfig = this.factories.get(name);
if (!serviceConfig) return;
visited.add(name);
for (const depName of serviceConfig.dependencies) {
this.checkCircularDependencies(depName, new Set(visited));
}
}
/**
* Set up circuit breaker for a service
* @param {string} name - Service name
* @param {*} instance - Service instance
* @param {Object} circuitBreakerConfig - Circuit breaker configuration
*/
setupCircuitBreaker(name, instance, circuitBreakerConfig) {
const CircuitBreaker = require('./CircuitBreaker');
const breaker = new CircuitBreaker({
name: `Service_${name}`,
...circuitBreakerConfig,
onStateChange: (event) => {
this.emit('circuitBreakerStateChange', { serviceName: name, ...event });
},
onFailure: (event) => {
this.emit('circuitBreakerFailure', { serviceName: name, ...event });
}
});
this.circuitBreakers.set(name, breaker);
// Wrap service methods with circuit breaker
this.wrapServiceWithCircuitBreaker(instance, breaker);
}
/**
* Wrap service methods with circuit breaker
* @param {*} instance - Service instance
* @param {CircuitBreaker} breaker - Circuit breaker instance
*/
wrapServiceWithCircuitBreaker(instance, breaker) {
const methodsToWrap = ['execute', 'run', 'process', 'analyze', 'generate'];
for (const methodName of methodsToWrap) {
if (typeof instance[methodName] === 'function') {
const originalMethod = instance[methodName].bind(instance);
instance[methodName] = (...args) => {
return breaker.execute(originalMethod, ...args);
};
}
}
}
/**
* Check if a service is registered
* @param {string} name - Service name
* @returns {boolean} Registration status
*/
has(name) {
return this.factories.has(name) || this.services.has(name);
}
/**
* Update startup order based on priority
* @param {string} name - Service name
* @param {number} priority - Startup priority
*/
updateStartupOrder(name, priority) {
// Remove if already exists
const existingIndex = this.startupOrder.findIndex(item => item.name === name);
if (existingIndex !== -1) {
this.startupOrder.splice(existingIndex, 1);
}
// Insert at correct position based on priority
const insertIndex = this.startupOrder.findIndex(item => item.priority > priority);
if (insertIndex === -1) {
this.startupOrder.push({ name, priority });
} else {
this.startupOrder.splice(insertIndex, 0, { name, priority });
}
// Update shutdown order (reverse of startup)
this.shutdownOrder = [...this.startupOrder].reverse();
}
/**
* Start all services in priority order
*/
async startServices() {
console.log('Starting services...');
for (const { name } of this.startupOrder) {
const serviceConfig = this.factories.get(name);
if (!serviceConfig) continue;
try {
console.log(`Starting service: ${name}`);
// Create instance with timeout
const instance = await this.createInstanceWithTimeout(name, serviceConfig.startupTimeout);
// Call startup method if it exists
if (instance && typeof instance.startup === 'function') {
await instance.startup();
}
this.emit('serviceStarted', { name, instance });
console.log(`Service '${name}' started successfully`);
} catch (error) {
console.error(`Failed to start service '${name}':`, error);
this.emit('serviceStartFailed', { name, error });
// Decide whether to continue or stop based on service criticality
if (serviceConfig.critical !== false) {
throw new Error(`Critical service '${name}' failed to start: ${error.message}`);
}
}
}
console.log('All services started successfully');
}
/**
* Create service instance with timeout
* @param {string} name - Service name
* @param {number} timeout - Timeout in milliseconds
* @returns {Promise<*>} Service instance
*/
async createInstanceWithTimeout(name, timeout) {
return Promise.race([
Promise.resolve(this.createInstance(name)),
new Promise((_, reject) => {
setTimeout(() => reject(new Error(`Service startup timeout`)), timeout);
})
]);
}
/**
* Shutdown all services in reverse order
*/
async shutdown() {
if (this.isShuttingDown) {
return;
}
console.log('Shutting down services...');
this.isShuttingDown = true;
for (const { name } of this.shutdownOrder) {
try {
await this.shutdownService(name);
} catch (error) {
console.error(`Error shutting down service '${name}':`, error);
}
}
// Stop circuit breakers
for (const [name, breaker] of this.circuitBreakers) {
try {
breaker.stopMonitoring();
} catch (error) {
console.error(`Error stopping circuit breaker for '${name}':`, error);
}
}
this.services.clear();
this.singletons.clear();
this.circuitBreakers.clear();
console.log('All services shut down');
this.emit('shutdown');
}
/**
* Shutdown a specific service
* @param {string} name - Service name
*/
async shutdownService(name) {
const instance = this.services.get(name) || this.singletons.get(name);
if (!instance) return;
const serviceConfig = this.factories.get(name);
const timeout = serviceConfig?.shutdownTimeout || 10000;
console.log(`Shutting down service: ${name}`);
try {
// Call shutdown method if it exists
if (typeof instance.shutdown === 'function') {
await Promise.race([
instance.shutdown(),
new Promise((_, reject) => {
setTimeout(() => reject(new Error('Shutdown timeout')), timeout);
})
]);
}
this.emit('serviceShutdown', { name, instance });
console.log(`Service '${name}' shut down successfully`);
} catch (error) {
console.error(`Error during shutdown of service '${name}':`, error);
this.emit('serviceShutdownError', { name, error });
}
// Remove from containers
this.services.delete(name);
this.singletons.delete(name);
}
/**
* Get health status of all services
*/
async getHealthStatus() {
const healthStatus = {
status: 'healthy',
services: {},
circuitBreakers: {},
summary: {
total: 0,
healthy: 0,
unhealthy: 0,
unknown: 0
}
};
// Check service health
for (const [name, serviceConfig] of this.factories) {
const instance = this.services.get(name) || this.singletons.get(name);
if (!instance) {
healthStatus.services[name] = { status: 'not_started' };
healthStatus.summary.unknown++;
continue;
}
healthStatus.summary.total++;
try {
let serviceHealth = { status: 'healthy' };
// Use custom health check if available
if (serviceConfig.healthCheck) {
serviceHealth = await serviceConfig.healthCheck(instance);
} else if (typeof instance.getHealthStatus === 'function') {
serviceHealth = await instance.getHealthStatus();
} else if (typeof instance.healthCheck === 'function') {
serviceHealth = await instance.healthCheck();
}
healthStatus.services[name] = serviceHealth;
if (serviceHealth.status === 'healthy') {
healthStatus.summary.healthy++;
} else {
healthStatus.summary.unhealthy++;
}
} catch (error) {
healthStatus.services[name] = {
status: 'unhealthy',
error: error.message
};
healthStatus.summary.unhealthy++;
}
}
// Check circuit breaker status
for (const [name, breaker] of this.circuitBreakers) {
healthStatus.circuitBreakers[name] = breaker.getStatus();
}
// Determine overall status
if (healthStatus.summary.unhealthy > 0) {
healthStatus.status = 'unhealthy';
} else if (healthStatus.summary.unknown > 0) {
healthStatus.status = 'degraded';
}
return healthStatus;
}
/**
* Get service statistics
*/
getStats() {
return {
registered: this.factories.size,
active: this.services.size,
singletons: this.singletons.size,
circuitBreakers: this.circuitBreakers.size,
startupOrder: this.startupOrder.map(item => item.name),
isShuttingDown: this.isShuttingDown
};
}
/**
* List all registered services
*/
listServices() {
const services = [];
for (const [name, config] of this.factories) {
const isActive = this.services.has(name) || this.singletons.has(name);
const hasCircuitBreaker = this.circuitBreakers.has(name);
services.push({
name,
isActive,
isSingleton: config.singleton,
hasCircuitBreaker,
dependencies: config.dependencies,
startupPriority: config.startupPriority
});
}
return services.sort((a, b) => a.startupPriority - b.startupPriority);
}
}
// Create singleton instance
const serviceContainer = new ServiceContainer();
module.exports = serviceContainer;