UNPKG

@sirmrmarty/n8n-nodes-tmux-orchestrator

Version:

n8n nodes for orchestrating Claude AI agents through tmux sessions

419 lines (358 loc) 9.99 kB
/** * Resource Manager Utility * Provides proper cleanup for intervals, timeouts, and other resources */ export interface ManagedResource { id: string; type: 'interval' | 'timeout' | 'process' | 'custom'; resource: any; cleanup: () => void | Promise<void>; createdAt: number; description?: string; } export interface ResourceManagerOptions { autoCleanupOnExit?: boolean; maxAge?: number; // Maximum age in milliseconds before auto-cleanup maxResources?: number; // Maximum number of resources to track } export class ResourceManager { private static instance: ResourceManager; private resources = new Map<string, ManagedResource>(); private cleanupInterval?: NodeJS.Timeout; private options: ResourceManagerOptions; private constructor(options: ResourceManagerOptions = {}) { this.options = { autoCleanupOnExit: true, maxAge: 5 * 60 * 1000, // 5 minutes default maxResources: 1000, ...options }; if (this.options.autoCleanupOnExit) { this.setupExitHandlers(); } // Schedule periodic cleanup this.cleanupInterval = setInterval(() => { this.performPeriodicCleanup(); }, 30000); // Every 30 seconds } public static getInstance(options?: ResourceManagerOptions): ResourceManager { if (!ResourceManager.instance) { ResourceManager.instance = new ResourceManager(options); } return ResourceManager.instance; } /** * Create and register a managed interval */ public createInterval( callback: () => void | Promise<void>, delay: number, description?: string ): { id: string; intervalId: NodeJS.Timeout } { const id = this.generateId('interval'); const intervalId = setInterval(async () => { try { await callback(); } catch (error) { console.warn(`Interval ${id} callback error:`, error.message); } }, delay); const resource: ManagedResource = { id, type: 'interval', resource: intervalId, cleanup: () => clearInterval(intervalId), createdAt: Date.now(), description }; this.resources.set(id, resource); this.enforceResourceLimits(); return { id, intervalId }; } /** * Create and register a managed timeout */ public createTimeout( callback: () => void | Promise<void>, delay: number, description?: string ): { id: string; timeoutId: NodeJS.Timeout } { const id = this.generateId('timeout'); const timeoutId = setTimeout(async () => { try { await callback(); } catch (error) { console.warn(`Timeout ${id} callback error:`, error.message); } finally { // Auto-remove after execution this.cleanup(id); } }, delay); const resource: ManagedResource = { id, type: 'timeout', resource: timeoutId, cleanup: () => clearTimeout(timeoutId), createdAt: Date.now(), description }; this.resources.set(id, resource); this.enforceResourceLimits(); return { id, timeoutId }; } /** * Register a custom resource for cleanup */ public registerResource( type: 'process' | 'custom', resource: any, cleanup: () => void | Promise<void>, description?: string ): string { const id = this.generateId(type); const managedResource: ManagedResource = { id, type, resource, cleanup, createdAt: Date.now(), description }; this.resources.set(id, managedResource); this.enforceResourceLimits(); return id; } /** * Clean up a specific resource */ public async cleanup(id: string): Promise<boolean> { const resource = this.resources.get(id); if (!resource) { return false; } try { await resource.cleanup(); this.resources.delete(id); return true; } catch (error) { console.warn(`Failed to cleanup resource ${id}:`, error.message); // Remove from tracking even if cleanup failed this.resources.delete(id); return false; } } /** * Clean up all resources of a specific type */ public async cleanupByType(type: ManagedResource['type']): Promise<number> { const resourcesOfType = Array.from(this.resources.values()) .filter(resource => resource.type === type); let cleanedUp = 0; for (const resource of resourcesOfType) { const success = await this.cleanup(resource.id); if (success) cleanedUp++; } return cleanedUp; } /** * Clean up all resources */ public async cleanupAll(): Promise<number> { const resourceIds = Array.from(this.resources.keys()); let cleanedUp = 0; for (const id of resourceIds) { const success = await this.cleanup(id); if (success) cleanedUp++; } // Also cleanup the periodic cleanup interval if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = undefined; } return cleanedUp; } /** * Get statistics about managed resources */ public getStats(): { total: number; byType: Record<string, number>; oldestAge: number; averageAge: number; } { const now = Date.now(); const resources = Array.from(this.resources.values()); const byType: Record<string, number> = {}; let totalAge = 0; let oldestAge = 0; for (const resource of resources) { byType[resource.type] = (byType[resource.type] || 0) + 1; const age = now - resource.createdAt; totalAge += age; oldestAge = Math.max(oldestAge, age); } return { total: resources.length, byType, oldestAge, averageAge: resources.length > 0 ? totalAge / resources.length : 0 }; } /** * List active resources (for debugging) */ public listResources(): ManagedResource[] { return Array.from(this.resources.values()).map(resource => ({ ...resource, resource: `[${typeof resource.resource}]` // Don't expose actual resource })); } /** * Create a scoped resource manager for a specific context */ public createScope(description?: string): ResourceScope { return new ResourceScope(this, description); } private generateId(type: string): string { const timestamp = Date.now(); const random = Math.random().toString(36).substring(2, 8); return `${type}_${timestamp}_${random}`; } private setupExitHandlers(): void { const cleanup = async () => { console.log('Cleaning up resources on exit...'); const cleaned = await this.cleanupAll(); console.log(`Cleaned up ${cleaned} resources`); }; process.on('exit', () => { // Synchronous cleanup for exit for (const resource of this.resources.values()) { try { if (typeof resource.cleanup === 'function') { resource.cleanup(); } } catch (error) { // Ignore errors during exit cleanup } } }); process.on('SIGTERM', async () => { await cleanup(); process.exit(0); }); process.on('SIGINT', async () => { await cleanup(); process.exit(0); }); process.on('uncaughtException', async (error) => { console.error('Uncaught exception:', error); await cleanup(); process.exit(1); }); process.on('unhandledRejection', async (reason) => { console.error('Unhandled rejection:', reason); await cleanup(); process.exit(1); }); } private performPeriodicCleanup(): void { const now = Date.now(); const toCleanup: string[] = []; // Find resources that are too old for (const [id, resource] of this.resources.entries()) { const age = now - resource.createdAt; if (this.options.maxAge && age > this.options.maxAge) { toCleanup.push(id); } } // Clean up old resources for (const id of toCleanup) { this.cleanup(id).catch(error => { console.warn(`Failed to cleanup old resource ${id}:`, error.message); }); } } private enforceResourceLimits(): void { if (!this.options.maxResources) return; const resourceCount = this.resources.size; if (resourceCount <= this.options.maxResources) return; // Remove oldest resources to stay within limit const resources = Array.from(this.resources.entries()) .sort((a, b) => a[1].createdAt - b[1].createdAt); const toRemove = resources.slice(0, resourceCount - this.options.maxResources); for (const [id] of toRemove) { this.cleanup(id).catch(error => { console.warn(`Failed to cleanup resource during limit enforcement ${id}:`, error.message); }); } } } /** * Scoped resource manager for automatic cleanup of resources within a specific context */ export class ResourceScope { private resourceIds: string[] = []; private manager: ResourceManager; private description?: string; constructor(manager: ResourceManager, description?: string) { this.manager = manager; this.description = description; } /** * Create an interval within this scope */ public createInterval( callback: () => void | Promise<void>, delay: number, description?: string ): { id: string; intervalId: NodeJS.Timeout } { const result = this.manager.createInterval(callback, delay, description); this.resourceIds.push(result.id); return result; } /** * Create a timeout within this scope */ public createTimeout( callback: () => void | Promise<void>, delay: number, description?: string ): { id: string; timeoutId: NodeJS.Timeout } { const result = this.manager.createTimeout(callback, delay, description); this.resourceIds.push(result.id); return result; } /** * Register a resource within this scope */ public registerResource( type: 'process' | 'custom', resource: any, cleanup: () => void | Promise<void>, description?: string ): string { const id = this.manager.registerResource(type, resource, cleanup, description); this.resourceIds.push(id); return id; } /** * Clean up all resources in this scope */ public async cleanup(): Promise<number> { let cleanedUp = 0; for (const id of this.resourceIds) { const success = await this.manager.cleanup(id); if (success) cleanedUp++; } this.resourceIds = []; return cleanedUp; } /** * Get the number of resources in this scope */ public getResourceCount(): number { return this.resourceIds.length; } } // Global instance for convenience export const resourceManager = ResourceManager.getInstance();