UNPKG

prodobit

Version:

Open-core business application development platform

416 lines (349 loc) 11.9 kB
import type { ModuleDefinition, ModuleState, ModuleRegistry, ModulesConfig } from '../schemas/modules.js'; export interface ModuleManagerOptions { enableHotReloading?: boolean; developmentMode?: boolean; maxRetries?: number; retryDelay?: number; } export class ModuleManager { private registry: Map<string, ModuleState> = new Map(); private dependencyGraph: Map<string, Set<string>> = new Map(); private reverseDependencyGraph: Map<string, Set<string>> = new Map(); private options: Required<ModuleManagerOptions>; constructor(options: ModuleManagerOptions = {}) { this.options = { enableHotReloading: options.enableHotReloading ?? false, developmentMode: options.developmentMode ?? false, maxRetries: options.maxRetries ?? 3, retryDelay: options.retryDelay ?? 1000, }; } async initialize(config: ModulesConfig): Promise<void> { // Clear existing registry this.registry.clear(); this.dependencyGraph.clear(); this.reverseDependencyGraph.clear(); // Check if config and manifest exist if (!config?.manifest?.modules) { console.warn('No modules configuration found, skipping module initialization'); return; } // Build dependency graph this.buildDependencyGraph(config.manifest.modules); // Initialize module states for (const module of config.manifest.modules) { const state: ModuleState = { id: module.id, enabled: module.enabled, loaded: false, initialized: false, started: false, lastUpdated: new Date(), }; this.registry.set(module.id, state); } // Load and start enabled modules in dependency order const loadOrder = this.calculateLoadOrder(config.manifest.modules); for (const moduleId of loadOrder) { const state = this.registry.get(moduleId); if (state?.enabled) { await this.loadModule(moduleId); await this.initializeModule(moduleId); const module = config.manifest.modules.find(m => m.id === moduleId); if (module?.autoStart) { await this.startModule(moduleId); } } } } async enableModule(moduleId: string): Promise<void> { const state = this.registry.get(moduleId); if (!state) { throw new Error(`Module ${moduleId} not found`); } if (state.enabled) { return; // Already enabled } // Check dependencies const dependencies = this.dependencyGraph.get(moduleId) || new Set(); for (const depId of dependencies) { const depState = this.registry.get(depId); if (!depState?.enabled || !depState.started) { throw new Error(`Module ${moduleId} requires ${depId} to be enabled and started`); } } // Enable and start the module state.enabled = true; state.lastUpdated = new Date(); this.registry.set(moduleId, state); await this.loadModule(moduleId); await this.initializeModule(moduleId); await this.startModule(moduleId); } async disableModule(moduleId: string): Promise<void> { const state = this.registry.get(moduleId); if (!state) { throw new Error(`Module ${moduleId} not found`); } if (!state.enabled) { return; // Already disabled } // Check reverse dependencies const reverseDeps = this.reverseDependencyGraph.get(moduleId) || new Set(); for (const depId of reverseDeps) { const depState = this.registry.get(depId); if (depState?.enabled) { throw new Error(`Cannot disable ${moduleId} because ${depId} depends on it`); } } // Stop and disable the module await this.stopModule(moduleId); state.enabled = false; state.loaded = false; state.initialized = false; state.started = false; state.lastUpdated = new Date(); this.registry.set(moduleId, state); } async reloadModule(moduleId: string): Promise<void> { if (!this.options.enableHotReloading) { throw new Error('Hot reloading is not enabled'); } const state = this.registry.get(moduleId); if (!state?.enabled) { throw new Error(`Module ${moduleId} is not enabled`); } await this.stopModule(moduleId); state.loaded = false; state.initialized = false; state.started = false; await this.loadModule(moduleId); await this.initializeModule(moduleId); await this.startModule(moduleId); } getModuleState(moduleId: string): ModuleState | undefined { return this.registry.get(moduleId); } getAllModuleStates(): ModuleState[] { return Array.from(this.registry.values()); } getEnabledModules(): string[] { return Array.from(this.registry.values()) .filter(state => state.enabled) .map(state => state.id); } getStartedModules(): string[] { return Array.from(this.registry.values()) .filter(state => state.started) .map(state => state.id); } isModuleReady(moduleId: string): boolean { const state = this.registry.get(moduleId); return state?.enabled === true && state?.loaded === true && state?.initialized === true && state?.started === true && !state?.error; } getModuleError(moduleId: string): string | undefined { return this.registry.get(moduleId)?.error; } validateModuleConfiguration(modules: ModuleDefinition[]): string[] { const errors: string[] = []; const moduleIds = new Set(modules.map(m => m.id)); for (const module of modules) { // Check for duplicate IDs const duplicates = modules.filter(m => m.id === module.id); if (duplicates.length > 1) { errors.push(`Duplicate module ID: ${module.id}`); } // Check dependencies exist if (module.dependencies) { for (const depId of module.dependencies) { if (!moduleIds.has(depId)) { errors.push(`Module ${module.id} has invalid dependency: ${depId}`); } } } // Check for conflicts if (module.conflicts) { for (const conflictId of module.conflicts) { const conflictModule = modules.find(m => m.id === conflictId); if (conflictModule?.enabled && module.enabled) { errors.push(`Module ${module.id} conflicts with enabled module: ${conflictId}`); } } } } // Check for circular dependencies const circularDeps = this.findCircularDependencies(modules); errors.push(...circularDeps); return errors; } private buildDependencyGraph(modules: ModuleDefinition[]): void { for (const module of modules) { const deps = new Set(module.dependencies || []); this.dependencyGraph.set(module.id, deps); // Build reverse dependency graph for (const depId of deps) { if (!this.reverseDependencyGraph.has(depId)) { this.reverseDependencyGraph.set(depId, new Set()); } this.reverseDependencyGraph.get(depId)!.add(module.id); } } } private calculateLoadOrder(modules: ModuleDefinition[]): string[] { const visited = new Set<string>(); const visiting = new Set<string>(); const order: string[] = []; const visit = (moduleId: string): void => { if (visited.has(moduleId)) { return; } if (visiting.has(moduleId)) { throw new Error(`Circular dependency detected involving module: ${moduleId}`); } visiting.add(moduleId); const deps = this.dependencyGraph.get(moduleId) || new Set(); for (const depId of deps) { visit(depId); } visiting.delete(moduleId); visited.add(moduleId); order.push(moduleId); }; // Sort by priority first const sortedModules = modules .slice() .sort((a, b) => b.priority - a.priority); for (const module of sortedModules) { if (!visited.has(module.id)) { visit(module.id); } } return order; } private findCircularDependencies(modules: ModuleDefinition[]): string[] { const errors: string[] = []; const visited = new Set<string>(); const visiting = new Set<string>(); const visit = (moduleId: string, path: string[]): void => { if (visited.has(moduleId)) { return; } if (visiting.has(moduleId)) { const cycle = path.slice(path.indexOf(moduleId)); errors.push(`Circular dependency: ${cycle.join(' -> ')} -> ${moduleId}`); return; } visiting.add(moduleId); const newPath = [...path, moduleId]; const module = modules.find(m => m.id === moduleId); if (module && module.dependencies) { for (const depId of module.dependencies) { visit(depId, newPath); } } visiting.delete(moduleId); visited.add(moduleId); }; for (const module of modules) { if (!visited.has(module.id)) { visit(module.id, []); } } return errors; } private async loadModule(moduleId: string): Promise<void> { const state = this.registry.get(moduleId); if (!state) { throw new Error(`Module ${moduleId} not found`); } try { // Simulate module loading // In real implementation, this would dynamically import the module await this.delay(100); state.loaded = true; delete state.error; state.lastUpdated = new Date(); this.registry.set(moduleId, state); } catch (error) { state.error = error instanceof Error ? error.message : 'Unknown load error'; state.lastUpdated = new Date(); this.registry.set(moduleId, state); throw error; } } private async initializeModule(moduleId: string): Promise<void> { const state = this.registry.get(moduleId); if (!state) { throw new Error(`Module ${moduleId} not found`); } if (!state.loaded) { throw new Error(`Module ${moduleId} must be loaded before initialization`); } try { // Simulate module initialization await this.delay(50); state.initialized = true; delete state.error; state.lastUpdated = new Date(); this.registry.set(moduleId, state); } catch (error) { state.error = error instanceof Error ? error.message : 'Unknown initialization error'; state.lastUpdated = new Date(); this.registry.set(moduleId, state); throw error; } } private async startModule(moduleId: string): Promise<void> { const state = this.registry.get(moduleId); if (!state) { throw new Error(`Module ${moduleId} not found`); } if (!state.initialized) { throw new Error(`Module ${moduleId} must be initialized before starting`); } try { // Simulate module start await this.delay(50); state.started = true; delete state.error; state.lastUpdated = new Date(); this.registry.set(moduleId, state); } catch (error) { state.error = error instanceof Error ? error.message : 'Unknown start error'; state.lastUpdated = new Date(); this.registry.set(moduleId, state); throw error; } } private async stopModule(moduleId: string): Promise<void> { const state = this.registry.get(moduleId); if (!state) { throw new Error(`Module ${moduleId} not found`); } try { // Simulate module stop await this.delay(50); state.started = false; delete state.error; state.lastUpdated = new Date(); this.registry.set(moduleId, state); } catch (error) { state.error = error instanceof Error ? error.message : 'Unknown stop error'; state.lastUpdated = new Date(); this.registry.set(moduleId, state); throw error; } } private delay(ms: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms)); } }