UNPKG

bc-code-intelligence-mcp

Version:

BC Code Intelligence MCP Server - Complete Specialist Bundle with AI-driven expert consultation, seamless handoffs, and context-preserving workflows

358 lines 14 kB
/** * Hot Reload System for Development Experience * * Watches for configuration changes, layer updates, and code modifications * to provide instant feedback during development without server restart. */ import { watch } from 'chokidar'; import { stat } from 'fs/promises'; import { EventEmitter } from 'events'; export class HotReloadSystem extends EventEmitter { enabled; debounceMs; verboseLogging; watchers = []; layerService; configLoader; currentConfig; reloadCount = 0; startTime = Date.now(); lastReload = 0; watchedPaths = new Set(); debounceTimers = new Map(); constructor(enabled = false, debounceMs = 1000, verboseLogging = true) { super(); this.enabled = enabled; this.debounceMs = debounceMs; this.verboseLogging = verboseLogging; if (this.enabled) { console.log('🔥 Hot reload system initialized'); } } /** * Initialize hot reload with layer service and config loader */ async initialize(layerService, configLoader) { if (!this.enabled) { console.log('⏭️ Hot reload disabled, skipping initialization'); return; } this.layerService = layerService; this.configLoader = configLoader; // Load initial configuration const configResult = await configLoader.loadConfiguration(); this.currentConfig = configResult.config; // Setup watchers await this.setupConfigurationWatcher(); await this.setupLayerWatchers(); console.log(`🔥 Hot reload system active with ${this.watchers.length} watchers`); // Emit initial ready event this.emitReloadEvent('config_changed', 'system', { message: 'Hot reload system ready' }); } /** * Add custom file path to watch */ watchPath(path, eventType = 'topic_changed') { if (!this.enabled || this.watchedPaths.has(path)) return; const watcher = watch(path, { ignoreInitial: true }) .on('change', (changedPath) => this.handleFileChange(changedPath, eventType)) .on('add', (addedPath) => this.handleFileChange(addedPath, eventType)) .on('unlink', (removedPath) => this.handleFileChange(removedPath, eventType)) .on('error', (error) => console.error(`🔥 Watcher error for ${path}:`, error)); this.watchers.push(watcher); this.watchedPaths.add(path); if (this.verboseLogging) { console.log(`👀 Now watching: ${path}`); } } /** * Trigger manual reload for testing or forced updates */ async triggerReload(reason = 'manual') { if (!this.layerService || !this.configLoader) { console.warn('🔥 Cannot trigger reload: services not initialized'); return; } console.log(`🔄 Triggering manual reload: ${reason}`); try { // Reload configuration const configResult = await this.configLoader.loadConfiguration(); // Check if configuration changed if (JSON.stringify(configResult.config) !== JSON.stringify(this.currentConfig)) { await this.handleConfigurationChange(configResult.config); } else { // Force layer refresh await this.layerService.refreshCache(); this.emitReloadEvent('layer_updated', 'manual', { reason }); } } catch (error) { console.error('🔥 Manual reload failed:', error); this.emit('error', error); } } /** * Get development server statistics */ getStats() { const uptime = Date.now() - this.startTime; // Simple performance impact calculation let performanceImpact = 'low'; if (this.watchers.length > 50) { performanceImpact = 'high'; } else if (this.watchers.length > 20) { performanceImpact = 'medium'; } return { uptime, reload_count: this.reloadCount, last_reload: this.lastReload, watched_files: this.watchedPaths.size, active_watchers: this.watchers.length, performance_impact: performanceImpact }; } /** * Shutdown hot reload system and cleanup watchers */ async shutdown() { console.log('🔥 Shutting down hot reload system...'); // Close all watchers await Promise.all(this.watchers.map(watcher => watcher.close())); this.watchers = []; this.watchedPaths.clear(); // Clear any pending debounce timers for (const timer of this.debounceTimers.values()) { clearTimeout(timer); } this.debounceTimers.clear(); console.log('✅ Hot reload system shutdown complete'); } // Private implementation methods /** * Setup configuration file watcher */ async setupConfigurationWatcher() { // Watch common configuration file locations const configPaths = [ './bckb-config.json', './bckb-config.yaml', './.bckb/config.json', './package.json' // For environment changes ]; for (const configPath of configPaths) { try { // Check if file exists before watching await stat(configPath); const watcher = watch(configPath, { ignoreInitial: true }) .on('change', () => this.handleConfigurationFileChange(configPath)) .on('error', (error) => { if (this.verboseLogging) { console.log(`📝 Config file watcher error for ${configPath}:`, error.message); } }); this.watchers.push(watcher); this.watchedPaths.add(configPath); } catch (error) { // File doesn't exist, skip watching if (this.verboseLogging) { console.log(`📝 Config file not found, skipping: ${configPath}`); } } } } /** * Setup layer-specific watchers based on current configuration */ async setupLayerWatchers() { if (!this.currentConfig) return; for (const layerConfig of this.currentConfig.layers.filter(l => l.enabled)) { try { let watchPath; switch (layerConfig.source.type) { case 'local': if ('path' in layerConfig.source && layerConfig.source.path) { watchPath = layerConfig.source.path; } else { continue; } break; case 'embedded': watchPath = 'embedded-knowledge'; // Default embedded path break; case 'git': // Watch the local git cache directory watchPath = '.bckb-cache/git-repos'; break; default: continue; // Skip unsupported source types for watching } const watcher = watch(watchPath, { ignoreInitial: true, persistent: true, depth: 3 // Reasonable depth to avoid performance issues }) .on('change', (path) => this.handleLayerChange(layerConfig.name, path, 'changed')) .on('add', (path) => this.handleLayerChange(layerConfig.name, path, 'added')) .on('unlink', (path) => this.handleLayerChange(layerConfig.name, path, 'removed')) .on('error', (error) => { if (this.verboseLogging) { console.warn(`🔥 Layer watcher error for ${layerConfig.name}:`, error.message); } }); this.watchers.push(watcher); this.watchedPaths.add(watchPath); if (this.verboseLogging) { console.log(`👀 Watching layer ${layerConfig.name}: ${watchPath}`); } } catch (error) { console.warn(`🔥 Failed to setup watcher for layer ${layerConfig.name}:`, error); } } } /** * Handle configuration file changes */ async handleConfigurationFileChange(configPath) { if (!this.configLoader) return; this.debounceAction(`config:${configPath}`, async () => { try { if (this.verboseLogging) { console.log(`📝 Configuration file changed: ${configPath}`); } // Reload configuration const configResult = await this.configLoader.loadConfiguration(); // Check if configuration actually changed if (JSON.stringify(configResult.config) !== JSON.stringify(this.currentConfig)) { await this.handleConfigurationChange(configResult.config); } } catch (error) { console.error(`🔥 Failed to reload configuration from ${configPath}:`, error); this.emit('error', error); } }); } /** * Handle layer file changes */ handleLayerChange(layerName, filePath, changeType) { // Only watch markdown files and relevant files if (!filePath.match(/\.(md|yaml|yml|json)$/i)) { return; } this.debounceAction(`layer:${layerName}:${filePath}`, async () => { try { if (this.verboseLogging) { console.log(`📄 Layer file ${changeType}: ${filePath} in ${layerName}`); } if (this.layerService) { // Invalidate cache for the specific layer const cacheStats = this.layerService.getCacheStats(); if ('advanced_cache_enabled' in cacheStats && cacheStats.advanced_cache_enabled) { // Could invalidate specific layer cache here } // Refresh the entire layer service (could be optimized to refresh only affected layer) await this.layerService.refreshCache(); this.emitReloadEvent('layer_updated', filePath, { layer_name: layerName, change_type: changeType, file_path: filePath }); } } catch (error) { console.error(`🔥 Failed to handle layer change in ${layerName}:`, error); this.emit('error', error); } }); } /** * Handle any file change with generic handling */ handleFileChange(filePath, eventType) { this.debounceAction(`file:${filePath}`, async () => { if (this.verboseLogging) { console.log(`📁 File changed: ${filePath}`); } this.emitReloadEvent(eventType, filePath, { file_path: filePath }); }); } /** * Handle configuration changes and trigger appropriate reloads */ async handleConfigurationChange(newConfig) { try { console.log('🔧 Configuration changed, reinitializing services...'); const oldConfig = this.currentConfig; this.currentConfig = newConfig; // Reinitialize layer service with new configuration if (this.layerService) { await this.layerService.initializeFromConfiguration(newConfig); } // Update watchers if layer configuration changed const layersChanged = !oldConfig || JSON.stringify(oldConfig.layers) !== JSON.stringify(newConfig.layers); if (layersChanged) { // Close existing layer watchers await Promise.all(this.watchers.map(w => w.close())); this.watchers = []; this.watchedPaths.clear(); // Setup new watchers await this.setupConfigurationWatcher(); await this.setupLayerWatchers(); } this.emitReloadEvent('config_changed', 'configuration', { layers_changed: layersChanged, new_layer_count: newConfig.layers.filter(l => l.enabled).length }); console.log('✅ Configuration reload complete'); } catch (error) { console.error('🔥 Configuration reload failed:', error); this.emit('error', error); } } /** * Debounce actions to prevent excessive reloading */ debounceAction(key, action) { // Clear existing timer const existingTimer = this.debounceTimers.get(key); if (existingTimer) { clearTimeout(existingTimer); } // Set new timer const timer = setTimeout(async () => { this.debounceTimers.delete(key); await action(); }, this.debounceMs); this.debounceTimers.set(key, timer); } /** * Emit reload event with consistent structure */ emitReloadEvent(type, source, details) { this.reloadCount++; this.lastReload = Date.now(); const event = { type, source, timestamp: this.lastReload, details }; this.emit('reload', event); if (this.verboseLogging) { console.log(`🔥 Hot reload event: ${type} from ${source}`); } } } //# sourceMappingURL=hot-reload.js.map