Files
michaelschiemer/resources/js/core/DependencyManager.js
Michael Schiemer 55a330b223 Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
2025-08-11 20:13:26 +02:00

353 lines
10 KiB
JavaScript

/**
* Dependency Management System für Module
* Verwaltet Dependencies und Initialisierungsreihenfolge
*/
import { Logger } from './logger.js';
/**
* @typedef {Object} ModuleDependency
* @property {string} name - Module name
* @property {string} version - Required version (semver)
* @property {boolean} optional - Whether dependency is optional
*/
/**
* @typedef {Object} ModuleDefinition
* @property {string} name - Module name
* @property {string} version - Module version
* @property {ModuleDependency[]} dependencies - Required dependencies
* @property {string[]} provides - Services this module provides
* @property {number} priority - Initialization priority (higher = earlier)
*/
export class DependencyManager {
constructor() {
/** @type {Map<string, ModuleDefinition>} */
this.modules = new Map();
/** @type {Map<string, string[]>} */
this.dependents = new Map(); // who depends on this module
/** @type {Set<string>} */
this.initialized = new Set();
/** @type {Set<string>} */
this.initializing = new Set();
/** @type {string[]} */
this.initializationOrder = [];
}
/**
* Register a module with its dependencies
* @param {ModuleDefinition} definition - Module definition
*/
register(definition) {
if (this.modules.has(definition.name)) {
Logger.warn(`[DependencyManager] Module '${definition.name}' already registered`);
return;
}
// Validate definition
if (!this.validateDefinition(definition)) {
return;
}
this.modules.set(definition.name, definition);
// Build dependents map
definition.dependencies.forEach(dep => {
if (!this.dependents.has(dep.name)) {
this.dependents.set(dep.name, []);
}
this.dependents.get(dep.name).push(definition.name);
});
Logger.info(`[DependencyManager] Registered '${definition.name}' v${definition.version}`);
}
/**
* Calculate initialization order based on dependencies
* @returns {string[]} Ordered list of module names
*/
calculateInitializationOrder() {
const visited = new Set();
const visiting = new Set();
const order = [];
/**
* Depth-first search with cycle detection
* @param {string} moduleName
*/
const visit = (moduleName) => {
if (visiting.has(moduleName)) {
throw new Error(`Circular dependency detected involving '${moduleName}'`);
}
if (visited.has(moduleName)) {
return;
}
const module = this.modules.get(moduleName);
if (!module) {
return; // Module not registered, might be optional
}
visiting.add(moduleName);
// Visit dependencies first
module.dependencies.forEach(dep => {
if (!dep.optional || this.modules.has(dep.name)) {
visit(dep.name);
}
});
visiting.delete(moduleName);
visited.add(moduleName);
order.push(moduleName);
};
// Sort by priority first, then resolve dependencies
const modulesByPriority = Array.from(this.modules.entries())
.sort(([, a], [, b]) => (b.priority || 0) - (a.priority || 0))
.map(([name]) => name);
modulesByPriority.forEach(moduleName => {
if (!visited.has(moduleName)) {
visit(moduleName);
}
});
this.initializationOrder = order;
Logger.info(`[DependencyManager] Initialization order: ${order.join(' → ')}`);
return order;
}
/**
* Check if all dependencies for a module are satisfied
* @param {string} moduleName - Module to check
* @returns {Object} Dependency check result
*/
checkDependencies(moduleName) {
const module = this.modules.get(moduleName);
if (!module) {
return { satisfied: false, missing: [], reason: `Module '${moduleName}' not registered` };
}
const missing = [];
const optional = [];
module.dependencies.forEach(dep => {
const depModule = this.modules.get(dep.name);
if (!depModule) {
if (dep.optional) {
optional.push(dep.name);
} else {
missing.push(dep.name);
}
} else if (!this.initialized.has(dep.name)) {
if (!dep.optional) {
missing.push(`${dep.name} (not initialized)`);
}
}
});
return {
satisfied: missing.length === 0,
missing,
optional,
reason: missing.length > 0 ? `Missing: ${missing.join(', ')}` : 'OK'
};
}
/**
* Mark module as initialized
* @param {string} moduleName - Module name
*/
markInitialized(moduleName) {
this.initialized.add(moduleName);
this.initializing.delete(moduleName);
Logger.info(`[DependencyManager] '${moduleName}' initialized`);
}
/**
* Mark module as initializing
* @param {string} moduleName - Module name
*/
markInitializing(moduleName) {
this.initializing.add(moduleName);
}
/**
* Get modules that depend on the given module
* @param {string} moduleName - Module name
* @returns {string[]} Dependent module names
*/
getDependents(moduleName) {
return this.dependents.get(moduleName) || [];
}
/**
* Get module definition
* @param {string} moduleName - Module name
* @returns {ModuleDefinition|undefined} Module definition
*/
getModule(moduleName) {
return this.modules.get(moduleName);
}
/**
* Check if module is ready to initialize
* @param {string} moduleName - Module name
* @returns {boolean} Ready status
*/
isReadyToInitialize(moduleName) {
if (this.initialized.has(moduleName) || this.initializing.has(moduleName)) {
return false;
}
const check = this.checkDependencies(moduleName);
return check.satisfied;
}
/**
* Get initialization status
* @returns {Object} Status report
*/
getStatus() {
const total = this.modules.size;
const initialized = this.initialized.size;
const initializing = this.initializing.size;
const pending = total - initialized - initializing;
return {
total,
initialized,
initializing,
pending,
modules: {
initialized: Array.from(this.initialized),
initializing: Array.from(this.initializing),
pending: Array.from(this.modules.keys()).filter(name =>
!this.initialized.has(name) && !this.initializing.has(name)
)
}
};
}
/**
* Validate module definition
* @private
* @param {ModuleDefinition} definition - Module definition to validate
* @returns {boolean} Validation result
*/
validateDefinition(definition) {
if (!definition.name || typeof definition.name !== 'string') {
Logger.error('[DependencyManager] Module name is required and must be string');
return false;
}
if (!definition.version || typeof definition.version !== 'string') {
Logger.error(`[DependencyManager] Version is required for module '${definition.name}'`);
return false;
}
if (!Array.isArray(definition.dependencies)) {
Logger.error(`[DependencyManager] Dependencies must be array for module '${definition.name}'`);
return false;
}
// Validate dependencies
for (const dep of definition.dependencies) {
if (!dep.name || typeof dep.name !== 'string') {
Logger.error(`[DependencyManager] Invalid dependency in module '${definition.name}': missing name`);
return false;
}
}
return true;
}
/**
* Reset dependency manager
*/
reset() {
this.modules.clear();
this.dependents.clear();
this.initialized.clear();
this.initializing.clear();
this.initializationOrder = [];
Logger.info('[DependencyManager] Reset complete');
}
/**
* Create module definition helper
* @param {string} name - Module name
* @param {string} version - Module version
* @returns {Object} Module definition builder
*/
static createDefinition(name, version) {
const definition = {
name,
version,
dependencies: [],
provides: [],
priority: 0
};
// Create a chainable API
const builder = {
depends(moduleName, moduleVersion = '*', optional = false) {
definition.dependencies.push({
name: moduleName,
version: moduleVersion,
optional
});
return this;
},
provides(...services) {
definition.provides.push(...services);
return this;
},
priority(level) {
definition.priority = level;
return this;
},
// Return the final definition
build() {
return definition;
}
};
// Make the builder return the definition when accessed as an object
Object.setPrototypeOf(builder, definition);
// Allow direct access to the definition properties
Object.keys(definition).forEach(key => {
if (!(key in builder)) {
Object.defineProperty(builder, key, {
get() { return definition[key]; },
set(value) { definition[key] = value; },
enumerable: true,
configurable: true
});
}
});
return builder;
}
}
// Global instance
export const dependencyManager = new DependencyManager();
// Debug access
if (typeof window !== 'undefined') {
window.dependencyManager = dependencyManager;
window.depStatus = () => dependencyManager.getStatus();
}