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
JavaScript
/**
* 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