@sirmrmarty/n8n-nodes-tmux-orchestrator
Version:
n8n nodes for orchestrating Claude AI agents through tmux sessions
419 lines (358 loc) • 9.99 kB
text/typescript
/**
* 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();