UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

434 lines 16.1 kB
/** * V3 Plugin Loader * Domain-Driven Design - Plugin-Based Architecture (ADR-004) * * Handles plugin loading, dependency resolution, and lifecycle management */ import { PluginError } from './plugin-interface.js'; /** * Default plugin loader configuration */ const DEFAULT_CONFIG = { initializationTimeout: 30000, // 30 seconds shutdownTimeout: 10000, // 10 seconds parallelInitialization: false, // Sequential by default for safety strictDependencies: true, enableHealthChecks: false, healthCheckInterval: 60000, // 1 minute }; /** * Plugin loader for managing plugin lifecycle */ export class PluginLoader { config; registry; initializationOrder = []; healthCheckIntervalId; constructor(registry, config) { this.registry = registry; this.config = { ...DEFAULT_CONFIG, ...config }; } /** * Load a single plugin */ async loadPlugin(plugin, context) { // Validate plugin this.validatePlugin(plugin); // Check for duplicates if (this.registry.hasPlugin(plugin.name)) { throw new PluginError(`Plugin '${plugin.name}' is already loaded`, plugin.name, 'DUPLICATE_PLUGIN'); } // Register plugin in uninitialized state this.registry.registerPlugin(plugin, 'uninitialized', context); // Resolve dependencies if (this.config.strictDependencies) { this.validateDependencies(plugin); } // Initialize plugin await this.initializePlugin(plugin, context); // Update initialization order this.initializationOrder.push(plugin.name); } /** * Load multiple plugins with dependency resolution */ async loadPlugins(plugins, context) { const results = { successful: [], failed: [], totalDuration: 0, }; const startTime = Date.now(); try { // Validate all plugins first for (const plugin of plugins) { this.validatePlugin(plugin); } // Build dependency graph const dependencyGraph = this.buildDependencyGraph(plugins); // Detect circular dependencies this.detectCircularDependencies(dependencyGraph); // Sort plugins by dependency order (topological sort) const sortedPlugins = this.topologicalSort(dependencyGraph); // Initialize plugins in order if (this.config.parallelInitialization) { await this.initializePluginsParallel(sortedPlugins, context, results); } else { await this.initializePluginsSequential(sortedPlugins, context, results); } } catch (error) { // If error during setup, mark all as failed for (const plugin of plugins) { if (!results.successful.includes(plugin.name) && !results.failed.some((f) => f.name === plugin.name)) { results.failed.push({ name: plugin.name, error: error instanceof Error ? error : new Error(String(error)), }); } } } finally { results.totalDuration = Date.now() - startTime; } // Start health checks if enabled if (this.config.enableHealthChecks) { this.startHealthChecks(); } return results; } /** * Unload a single plugin */ async unloadPlugin(pluginName) { const pluginInfo = this.registry.getPlugin(pluginName); if (!pluginInfo) { throw new PluginError(`Plugin '${pluginName}' not found`, pluginName, 'INVALID_PLUGIN'); } // Check for dependents const dependents = this.findDependents(pluginName); if (dependents.length > 0) { throw new PluginError(`Cannot unload plugin '${pluginName}': depended on by ${dependents.join(', ')}`, pluginName, 'DEPENDENCY_NOT_FOUND'); } // Shutdown plugin await this.shutdownPlugin(pluginInfo.plugin); // Unregister plugin this.registry.unregisterPlugin(pluginName); // Remove from initialization order const index = this.initializationOrder.indexOf(pluginName); if (index !== -1) { this.initializationOrder.splice(index, 1); } } /** * Unload all plugins in reverse initialization order */ async unloadAll() { // Stop health checks if (this.healthCheckIntervalId) { clearInterval(this.healthCheckIntervalId); this.healthCheckIntervalId = undefined; } // Shutdown in reverse order const pluginsToShutdown = [...this.initializationOrder].reverse(); for (const pluginName of pluginsToShutdown) { try { await this.unloadPlugin(pluginName); } catch (error) { // Log error but continue shutting down other plugins console.error(`Error unloading plugin '${pluginName}':`, error); } } this.initializationOrder = []; } /** * Reload a plugin */ async reloadPlugin(pluginName, newPlugin, context) { await this.unloadPlugin(pluginName); await this.loadPlugin(newPlugin, context); } /** * Get plugin initialization order */ getInitializationOrder() { return [...this.initializationOrder]; } /** * Validate plugin interface */ validatePlugin(plugin) { if (!plugin.name) { throw new PluginError('Plugin must have a name', '<unknown>', 'INVALID_PLUGIN'); } if (!plugin.version) { throw new PluginError(`Plugin '${plugin.name}' must have a version`, plugin.name, 'INVALID_PLUGIN'); } if (typeof plugin.initialize !== 'function') { throw new PluginError(`Plugin '${plugin.name}' must implement initialize()`, plugin.name, 'INVALID_PLUGIN'); } if (typeof plugin.shutdown !== 'function') { throw new PluginError(`Plugin '${plugin.name}' must implement shutdown()`, plugin.name, 'INVALID_PLUGIN'); } } /** * Validate plugin dependencies */ validateDependencies(plugin) { if (!plugin.dependencies || plugin.dependencies.length === 0) { return; } for (const dep of plugin.dependencies) { if (!this.registry.hasPlugin(dep)) { throw new PluginError(`Plugin '${plugin.name}' depends on '${dep}' which is not loaded`, plugin.name, 'DEPENDENCY_NOT_FOUND'); } // Check dependency is initialized const depInfo = this.registry.getPlugin(dep); if (depInfo && depInfo.state !== 'initialized') { throw new PluginError(`Plugin '${plugin.name}' depends on '${dep}' which is not initialized (state: ${depInfo.state})`, plugin.name, 'DEPENDENCY_NOT_FOUND'); } } } /** * Initialize a single plugin */ async initializePlugin(plugin, context) { this.registry.updatePluginState(plugin.name, 'initializing'); try { // Run initialization with timeout await this.withTimeout(plugin.initialize(context), this.config.initializationTimeout, `Plugin '${plugin.name}' initialization timed out`); this.registry.updatePluginState(plugin.name, 'initialized'); this.registry.collectPluginMetrics(plugin.name); } catch (error) { this.registry.updatePluginState(plugin.name, 'error', error instanceof Error ? error : new Error(String(error))); throw new PluginError(`Failed to initialize plugin '${plugin.name}': ${error}`, plugin.name, 'INITIALIZATION_FAILED', error instanceof Error ? error : undefined); } } /** * Shutdown a single plugin */ async shutdownPlugin(plugin) { this.registry.updatePluginState(plugin.name, 'shutting-down'); try { await this.withTimeout(plugin.shutdown(), this.config.shutdownTimeout, `Plugin '${plugin.name}' shutdown timed out`); this.registry.updatePluginState(plugin.name, 'shutdown'); } catch (error) { this.registry.updatePluginState(plugin.name, 'error', error instanceof Error ? error : new Error(String(error))); throw new PluginError(`Failed to shutdown plugin '${plugin.name}': ${error}`, plugin.name, 'SHUTDOWN_FAILED', error instanceof Error ? error : undefined); } } /** * Initialize plugins sequentially */ async initializePluginsSequential(plugins, context, results) { for (const plugin of plugins) { try { // Register and initialize this.registry.registerPlugin(plugin, 'uninitialized', context); await this.initializePlugin(plugin, context); results.successful.push(plugin.name); this.initializationOrder.push(plugin.name); } catch (error) { results.failed.push({ name: plugin.name, error: error instanceof Error ? error : new Error(String(error)), }); // Stop on first failure in sequential mode if strict if (this.config.strictDependencies) { break; } } } } /** * Initialize plugins in parallel (by dependency level) */ async initializePluginsParallel(plugins, context, results) { // Group plugins by dependency depth const dependencyGraph = this.buildDependencyGraph(plugins); const levels = this.groupByDepth(dependencyGraph); // Initialize each level in parallel for (const level of levels) { const promises = level.map(async (plugin) => { try { this.registry.registerPlugin(plugin, 'uninitialized', context); await this.initializePlugin(plugin, context); results.successful.push(plugin.name); this.initializationOrder.push(plugin.name); } catch (error) { results.failed.push({ name: plugin.name, error: error instanceof Error ? error : new Error(String(error)), }); } }); await Promise.all(promises); // Stop on failures in level if strict if (this.config.strictDependencies && results.failed.length > 0) { break; } } } /** * Build dependency graph */ buildDependencyGraph(plugins) { const graph = new Map(); // Create nodes for (const plugin of plugins) { graph.set(plugin.name, { plugin, dependencies: new Set(plugin.dependencies || []), dependents: new Set(), depth: 0, }); } // Build dependency links for (const [name, node] of Array.from(graph.entries())) { for (const dep of Array.from(node.dependencies)) { const depNode = graph.get(dep); if (depNode) { depNode.dependents.add(name); } } } // Calculate depths this.calculateDepths(graph); return graph; } /** * Calculate depth of each node (for topological sorting) */ calculateDepths(graph) { const visited = new Set(); const visit = (name) => { if (visited.has(name)) { const node = graph.get(name); return node ? node.depth : 0; } visited.add(name); const node = graph.get(name); if (!node) return 0; let maxDepth = 0; for (const dep of Array.from(node.dependencies)) { maxDepth = Math.max(maxDepth, visit(dep) + 1); } node.depth = maxDepth; return maxDepth; }; for (const name of Array.from(graph.keys())) { visit(name); } } /** * Topological sort (dependency order) */ topologicalSort(graph) { const sorted = []; const nodes = Array.from(graph.values()); // Sort by depth (dependencies first) nodes.sort((a, b) => a.depth - b.depth); for (const node of nodes) { sorted.push(node.plugin); } return sorted; } /** * Group plugins by dependency depth (for parallel initialization) */ groupByDepth(graph) { const levels = []; const maxDepth = Math.max(...Array.from(graph.values()).map((n) => n.depth)); for (let depth = 0; depth <= maxDepth; depth++) { const level = []; for (const node of Array.from(graph.values())) { if (node.depth === depth) { level.push(node.plugin); } } if (level.length > 0) { levels.push(level); } } return levels; } /** * Detect circular dependencies */ detectCircularDependencies(graph) { const visited = new Set(); const stack = new Set(); const visit = (name, path) => { if (stack.has(name)) { const cycle = [...path, name]; throw new PluginError(`Circular dependency detected: ${cycle.join(' -> ')}`, name, 'CIRCULAR_DEPENDENCY'); } if (visited.has(name)) { return; } visited.add(name); stack.add(name); const node = graph.get(name); if (node) { for (const dep of Array.from(node.dependencies)) { visit(dep, [...path, name]); } } stack.delete(name); }; for (const name of Array.from(graph.keys())) { visit(name, []); } } /** * Find plugins that depend on a given plugin */ findDependents(pluginName) { const dependents = []; for (const [name, info] of Array.from(this.registry.getAllPlugins().entries())) { if (info.plugin.dependencies?.includes(pluginName)) { dependents.push(name); } } return dependents; } /** * Start periodic health checks */ startHealthChecks() { this.healthCheckIntervalId = setInterval(async () => { for (const [name, info] of Array.from(this.registry.getAllPlugins().entries())) { if (info.state === 'initialized' && info.plugin.healthCheck) { try { const healthy = await info.plugin.healthCheck(); if (!healthy) { console.warn(`Plugin '${name}' health check failed`); this.registry.updatePluginState(name, 'error', new Error('Health check failed')); } } catch (error) { console.error(`Plugin '${name}' health check error:`, error); this.registry.updatePluginState(name, 'error', error instanceof Error ? error : new Error(String(error))); } } } }, this.config.healthCheckInterval); } /** * Utility: Run promise with timeout */ async withTimeout(promise, timeoutMs, errorMessage) { return Promise.race([ promise, new Promise((_, reject) => setTimeout(() => reject(new Error(errorMessage)), timeoutMs)), ]); } } //# sourceMappingURL=plugin-loader.js.map