227 lines
7.9 KiB
JavaScript
227 lines
7.9 KiB
JavaScript
/**
|
|
* 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<string>} [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<string>} [path=[]] - Current dependency path
|
|
* @returns {Array<string>|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<string>} - Array of dependencies
|
|
*/
|
|
getDependencies(id) {
|
|
return this.moduleDependencies.get(id) || [];
|
|
}
|
|
|
|
/**
|
|
* Check if the dependency graph has any circular dependencies
|
|
* @returns {Array<string>|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<string>} 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;
|