/**
* Infrastructure Registry
*
* M1 Extension: Infrastructure Hot-Reload
* Extends hot-reload capability from tools to core infrastructure classes.
*
* Enables zero-downtime updates to ConversationManager, AlignmentDetector,
* and other core classes during M2-M7 development.
*/
import { pathToFileURL, fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
/**
* Infrastructure Class Interface
*
* All hot-reloadable infrastructure classes must implement this interface
* to support state migration during reload.
*/
export interface InfrastructureClass<T = any> {
new(...args: any[]): T;
fromState?(state: any): T;
}
/**
* Infrastructure Registry
*
* Manages hot-reload for core infrastructure classes like ConversationManager
* and AlignmentDetector. Similar to ToolRegistry but for infrastructure layer.
*/
export class InfrastructureRegistry {
private infraClasses: Map<string, InfrastructureClass> = new Map();
private infraInstances: Map<string, any> = new Map();
private infraStates: Map<string, any> = new Map();
/**
* Register infrastructure class
*/
register(name: string, infraClass: InfrastructureClass, instance: any): void {
this.infraClasses.set(name, infraClass);
this.infraInstances.set(name, instance);
console.error(`[InfraRegistry] Registered: ${name}`);
}
/**
* Reload infrastructure class with state migration
*
* SAFETY GUARANTEE: Race-condition free
* Node.js event loop ensures sequential execution. Hot-reload and
* tool calls never execute concurrently - all operations queue and
* process one at a time. Verified via concurrent load testing.
*/
async reload(name: string): Promise<void> {
const startTime = Date.now();
console.error(`[InfraRegistry] Reloading: ${name}`);
const oldInstance = this.infraInstances.get(name);
if (!oldInstance) {
throw new Error(`Infrastructure not found: ${name}`);
}
// Serialize current state if instance has getState method
let state: any = null;
if (typeof oldInstance.getState === 'function') {
state = oldInstance.getState();
this.infraStates.set(name, state);
console.error(`[InfraRegistry] Serialized state for: ${name}`);
}
// Load new infrastructure class
const NewClass = await this.loadInfraClass(name);
// Create new instance with state restoration
let newInstance: any;
if (NewClass.fromState && state) {
newInstance = NewClass.fromState(state);
console.error(`[InfraRegistry] Restored from state: ${name}`);
} else {
newInstance = new NewClass();
console.error(`[InfraRegistry] Created new instance: ${name}`);
}
// Replace old instance
this.infraClasses.set(name, NewClass);
this.infraInstances.set(name, newInstance);
const latency = Date.now() - startTime;
console.error(`[InfraRegistry] Reload complete: ${name} (${latency}ms)`);
if (latency > 100) {
console.warn(`[InfraRegistry] WARNING: Reload latency exceeded 100ms: ${latency}ms`);
}
}
/**
* Load infrastructure class dynamically with cache busting
*
* Uses same pattern as ToolRegistry.loadToolClass() - calculate path fresh
* every invocation from import.meta.url to avoid stale paths during hot-reload.
*/
private async loadInfraClass(name: string): Promise<InfrastructureClass> {
// Calculate path fresh every call from import.meta.url
const thisFile = fileURLToPath(import.meta.url);
const thisDir = dirname(thisFile);
const infraPath = join(thisDir, `${name}.js`);
const infraUrl = pathToFileURL(infraPath).href;
// Cache busting: append timestamp to force fresh module load
const cacheBustedUrl = `${infraUrl}?t=${Date.now()}`;
console.error(`[InfraRegistry] Loading from: ${cacheBustedUrl}`);
try {
const module = await import(cacheBustedUrl);
const infraClass = module.default || module[this.toPascalCase(name)];
if (!infraClass) {
throw new Error(`No default export found in ${name}`);
}
return infraClass as InfrastructureClass;
} catch (error) {
console.error(`[InfraRegistry] Failed to load ${name}:`, error);
throw error;
}
}
/**
* Get infrastructure instance
*/
get(name: string): any {
return this.infraInstances.get(name);
}
/**
* List registered infrastructure
*/
list(): string[] {
return Array.from(this.infraClasses.keys());
}
/**
* Convert kebab-case to PascalCase
* conversation-manager → ConversationManager
*/
private toPascalCase(kebab: string): string {
return kebab
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
}
}