/** * Module Registry * Manages module registration and dependency tracking */ export class ModuleRegistry { constructor() { this.modules = {}; this.readyPromises = {}; this.moduleDependencies = new Map(); // Track module dependencies this.visitedModules = new Set(); // For circular dependency detection this.recursionStack = new Set(); // For circular dependency detection this.untrackedDependencies = new Map(); // Track unregistered dependencies } /** * Register a module * @param {BaseModule} module - Module to register * @param {Array} [dependencies] - Optional array of module dependencies */ register(module, dependencies = null) { if (!module || !module.id) { console.error('Invalid module - must have an id property'); return; } // Store the module this.modules[module.id] = module; // Store dependencies if provided, otherwise use module.dependencies if (dependencies) { this.moduleDependencies.set(module.id, dependencies); // Also set them on the module itself for backwards compatibility if (module.dependencies === undefined) { module.dependencies = [...dependencies]; } } else if (module.dependencies) { // Use the module's own dependencies property this.moduleDependencies.set(module.id, [...module.dependencies]); } else { // No dependencies this.moduleDependencies.set(module.id, []); } // Check for circular dependencies this.visitedModules.clear(); this.recursionStack.clear(); const circularDependency = this.detectCircularDependency(module.id); if (circularDependency) { const errorMsg = `Circular dependency detected: ${circularDependency.join(' -> ')} -> ${circularDependency[0]}`; console.error(errorMsg); throw new Error(errorMsg); } // Create a promise that will resolve when this module is ready this.readyPromises[module.id] = new Promise((resolve) => { // Set up a state change listener for this module document.addEventListener('module:stateChange', (event) => { if (event.detail.moduleId === module.id && (event.detail.state === 'FINISHED' || event.detail.state === 'ERROR')) { resolve(event.detail.state === 'FINISHED'); } }); // Check if already in finished state if (module.state === 'FINISHED') { resolve(true); } else if (module.state === 'ERROR') { resolve(false); } }); } /** * Detect circular dependencies using DFS algorithm * @param {string} moduleId - Starting module ID * @param {Array} [path=[]] - Current dependency path * @returns {Array|null} - Array representing the circular dependency path, or null if none */ detectCircularDependency(moduleId, path = []) { // If we've already checked this module completely, no need to check again if (this.visitedModules.has(moduleId)) { return null; } // If we're already visiting this module in the current path, we found a cycle if (this.recursionStack.has(moduleId)) { // Return the path that forms the cycle const cycleStartIndex = path.indexOf(moduleId); if (cycleStartIndex >= 0) { return path.slice(cycleStartIndex); } return path; } // Add to recursion stack to mark as being visited this.recursionStack.add(moduleId); path.push(moduleId); // Get dependencies for this module const dependencies = this.getDependencies(moduleId); // Check each dependency for (const depId of dependencies) { // Even if the dependency isn't registered yet, we need to track it // for potential circular dependencies that will manifest later // Create a temporary placeholder in the path for unregistered dependencies const depPath = [...path]; if (!this.modules[depId]) { // Log that we're tracking an unregistered dependency console.log(`Module Registry: Tracking potential circular dependency with unregistered module: ${depId}`); // Add to the dependency tracking for future checks this.trackDependency(moduleId, depId); continue; } const result = this.detectCircularDependency(depId, depPath); if (result) { return result; } } // Remove from recursion stack as we're done with this module this.recursionStack.delete(moduleId); // Mark as fully visited this.visitedModules.add(moduleId); return null; } /** * Track an unregistered dependency * @param {string} moduleId - Module ID * @param {string} depId - Unregistered dependency ID */ trackDependency(moduleId, depId) { if (!this.untrackedDependencies.has(moduleId)) { this.untrackedDependencies.set(moduleId, new Set()); } this.untrackedDependencies.get(moduleId).add(depId); } /** * Get a module by id * @param {string} id - Module id * @returns {BaseModule} - The module, or null if not found */ getModule(id) { return this.modules[id] || null; } /** * Get all registered modules * @returns {Object} - Map of modules */ getAllModules() { return this.modules; } /** * Get dependencies for a module * @param {string} id - Module id * @returns {Array} - Array of dependencies */ getDependencies(id) { return this.moduleDependencies.get(id) || []; } /** * Check if the dependency graph has any circular dependencies * @returns {Array|null} - Array representing the circular dependency path, or null if none */ checkForCircularDependencies() { this.visitedModules.clear(); for (const moduleId in this.modules) { this.recursionStack.clear(); const result = this.detectCircularDependency(moduleId); if (result) { return result; } } return null; } /** * Wait for a module to be ready (in FINISHED state) * @param {string} id - Module id to wait for * @param {number} timeout - Optional timeout in ms * @returns {Promise} - Resolves when the module is ready */ waitForModule(id, timeout = null) { if (!this.readyPromises[id]) { return Promise.resolve(false); } if (timeout) { // Add timeout logic return Promise.race([ this.readyPromises[id], new Promise(resolve => setTimeout(() => resolve(false), timeout)) ]); } return this.readyPromises[id]; } /** * Wait for multiple modules to be ready * @param {Array} ids - Array of module ids to wait for * @param {number} timeout - Optional timeout in ms * @returns {Promise} - Resolves when all modules are ready */ waitForModules(ids, timeout = null) { const promises = ids.map(id => this.waitForModule(id, timeout)); return Promise.all(promises); } } // Create and export a singleton instance export const moduleRegistry = new ModuleRegistry(); // Make registry accessible globally window.moduleRegistry = moduleRegistry;