UNPKG

@dbs-portal/core-module-registry

Version:

Core module registry system for automatic module discovery and registration

403 lines 13.3 kB
/** * Module Registry * * Central registry for module discovery, validation, and management. * Implements singleton pattern for global access across the application. */ import { ModuleStatus } from '../types/ModuleMetadata'; import { validateModuleMetadata } from '../types/ModuleMetadata'; import { ModuleCache } from '../utils/ModuleCache'; import { BrowserEventEmitter } from '../utils/BrowserEventEmitter'; export class ModuleRegistry { static instance = null; modules = new Map(); registryListeners = new Set(); cache; options; initialized = false; initializing = false; eventEmitter; constructor(options = {}) { this.eventEmitter = new BrowserEventEmitter(); this.options = { enableCaching: true, cacheTimeout: 5 * 60 * 1000, // 5 minutes validateOnRegister: true, allowDuplicates: false, autoInitialize: true, ...options }; this.cache = new ModuleCache(this.options.cacheTimeout); // Set up error handling this.eventEmitter.on('error', (error) => { console.error('ModuleRegistry Error:', error); this.notifyListeners('onRegistryError', error); }); } /** * Event emitter methods - delegate to internal event emitter */ on(event, listener) { this.eventEmitter.on(event, listener); return this; } once(event, listener) { this.eventEmitter.once(event, listener); return this; } off(event, listener) { this.eventEmitter.off(event, listener); return this; } emit(event, ...args) { return this.eventEmitter.emit(event, ...args); } removeAllListeners(event) { this.eventEmitter.removeAllListeners(event); return this; } listenerCount(event) { return this.eventEmitter.listenerCount(event); } listeners(event) { return this.eventEmitter.listeners(event); } /** * Get singleton instance of ModuleRegistry */ static getInstance(options) { if (!ModuleRegistry.instance) { ModuleRegistry.instance = new ModuleRegistry(options); } return ModuleRegistry.instance; } /** * Reset singleton instance (mainly for testing) */ static resetInstance() { if (ModuleRegistry.instance) { ModuleRegistry.instance.removeAllListeners(); ModuleRegistry.instance.modules.clear(); ModuleRegistry.instance.registryListeners.clear(); ModuleRegistry.instance.cache.clear(); } ModuleRegistry.instance = null; } /** * Initialize the registry with module discovery */ async initialize() { if (this.initialized) { return; } if (this.initializing) { // Wait for existing initialization to complete return new Promise((resolve) => { this.once('initialized', resolve); }); } this.initializing = true; try { // Import discovery service dynamically to avoid circular dependencies const { ModuleDiscoveryService } = await import('./ModuleDiscoveryService'); const discoveryService = new ModuleDiscoveryService(); const modules = await discoveryService.discoverModules(); // Register discovered modules for (const module of modules) { try { await this.registerModule(module, 'discovery'); } catch (error) { console.warn(`Failed to register module ${module.id}:`, error); this.emit('error', error, module.id); } } this.initialized = true; this.initializing = false; this.emit('initialized', this.getAllModules()); this.notifyListeners('onRegistryInitialized', this.getAllModules()); } catch (error) { this.initializing = false; this.emit('error', error); throw error; } } /** * Register a module in the registry (with duplicate check) */ async registerModule(metadata, source = 'runtime') { try { // Validate module metadata if enabled if (this.options.validateOnRegister) { const validation = this.validateModule(metadata); if (!validation.valid) { throw new Error(`Module validation failed: ${validation.errors.map(e => e.message).join(', ')}`); } } // Check for duplicates if not allowed if (!this.options.allowDuplicates && this.modules.has(metadata.id)) { const existingModule = this.modules.get(metadata.id); console.warn(`⚠️ Module ${metadata.id} is already registered`, { existing: { source: existingModule?.source, timestamp: existingModule?.timestamp, name: existingModule?.metadata.name }, new: { source, name: metadata.name } }); throw new Error(`Module ${metadata.id} is already registered`); } // Create registration record const registration = { metadata, timestamp: Date.now(), source }; // Register the module this.modules.set(metadata.id, registration); // Cache the module if caching is enabled if (this.options.enableCaching) { this.cache.set(`module:${metadata.id}`, registration); } // Emit events const event = { type: 'register', moduleId: metadata.id, module: metadata, timestamp: Date.now() }; this.emit('moduleRegistered', event); this.notifyListeners('onModuleRegistered', metadata); } catch (error) { this.emit('error', error, metadata.id); throw error; } } /** * Register a module safely (won't throw if already registered) */ async registerModuleSafe(metadata, source = 'runtime') { try { if (this.modules.has(metadata.id)) { console.log(`ℹ️ Module ${metadata.id} is already registered, skipping`); return false; } await this.registerModule(metadata, source); return true; } catch (error) { console.warn(`Failed to register module ${metadata.id}:`, error); return false; } } /** * Unregister a module from the registry */ unregisterModule(moduleId) { try { const registration = this.modules.get(moduleId); if (!registration) { return false; } // Remove from registry this.modules.delete(moduleId); // Remove from cache if (this.options.enableCaching) { this.cache.delete(`module:${moduleId}`); } // Emit events const event = { type: 'unregister', moduleId, module: registration.metadata, timestamp: Date.now() }; this.emit('moduleUnregistered', event); this.notifyListeners('onModuleUnregistered', registration.metadata); return true; } catch (error) { this.emit('error', error, moduleId); return false; } } /** * Update a module's metadata */ updateModule(moduleId, updates) { try { const registration = this.modules.get(moduleId); if (!registration) { return false; } // Create updated metadata const updatedMetadata = { ...registration.metadata, ...updates }; // Validate if enabled if (this.options.validateOnRegister) { const validation = this.validateModule(updatedMetadata); if (!validation.valid) { throw new Error(`Module validation failed: ${validation.errors.map(e => e.message).join(', ')}`); } } // Update registration const updatedRegistration = { ...registration, metadata: updatedMetadata, timestamp: Date.now() }; this.modules.set(moduleId, updatedRegistration); // Update cache if (this.options.enableCaching) { this.cache.set(`module:${moduleId}`, updatedRegistration); } // Emit events const event = { type: 'update', moduleId, module: updatedMetadata, timestamp: Date.now() }; this.emit('moduleUpdated', event); this.notifyListeners('onModuleUpdated', updatedMetadata); return true; } catch (error) { this.emit('error', error, moduleId); return false; } } /** * Get a specific module by ID */ getModule(moduleId) { // Try cache first if enabled if (this.options.enableCaching) { const cached = this.cache.get(`module:${moduleId}`); if (cached) { return cached.metadata; } } const registration = this.modules.get(moduleId); return registration?.metadata; } /** * Get all registered modules */ getAllModules() { return Array.from(this.modules.values()).map(reg => reg.metadata); } /** * Get modules by category */ getModulesByCategory(category) { return this.getAllModules().filter(module => module.category === category); } /** * Get modules by status */ getModulesByStatus(status) { return this.getAllModules().filter(module => module.status === status); } /** * Get active modules only */ getActiveModules() { return this.getModulesByStatus(ModuleStatus.ACTIVE); } /** * Get modules with specific permissions */ getModulesWithPermissions(permissions) { return this.getAllModules().filter(module => permissions.some(permission => module.permissions.includes(permission))); } /** * Search modules by name, description, or tags */ searchModules(query) { const lowerQuery = query.toLowerCase(); return this.getAllModules().filter(module => module.name.toLowerCase().includes(lowerQuery) || module.description.toLowerCase().includes(lowerQuery) || module.tags.some(tag => tag.toLowerCase().includes(lowerQuery))); } /** * Check if a module is registered */ hasModule(moduleId) { return this.modules.has(moduleId); } /** * Get module count */ getModuleCount() { return this.modules.size; } /** * Validate module metadata */ validateModule(metadata) { return validateModuleMetadata(metadata); } /** * Add registry listener */ addListener(listener) { this.registryListeners.add(listener); } /** * Remove registry listener */ removeListener(listener) { this.registryListeners.delete(listener); } /** * Clear all modules (mainly for testing) */ clear() { this.modules.clear(); this.cache.clear(); this.registryListeners.clear(); this.initialized = false; this.initializing = false; } /** * Get registry statistics */ getStats() { const modules = this.getAllModules(); const categories = new Map(); const statuses = new Map(); modules.forEach(module => { categories.set(module.category, (categories.get(module.category) || 0) + 1); statuses.set(module.status, (statuses.get(module.status) || 0) + 1); }); return { totalModules: modules.length, categories: Object.fromEntries(categories), statuses: Object.fromEntries(statuses), initialized: this.initialized, cacheEnabled: this.options.enableCaching }; } /** * Notify all listeners of an event */ notifyListeners(method, ...args) { this.registryListeners.forEach((listener) => { try { const fn = listener[method]; if (typeof fn === 'function') { fn.apply(listener, args); } } catch (error) { console.error(`Error in registry listener ${method}:`, error); } }); } } //# sourceMappingURL=ModuleRegistry.js.map