@dbs-portal/core-module-registry
Version:
Core module registry system for automatic module discovery and registration
403 lines • 13.3 kB
JavaScript
/**
* 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