Skip to main content
Glama
resourceGuard.ts8.65 kB
/** * @fileOverview: Central resource registry that tracks and auto-disposes resources to prevent memory leaks * @module: ResourceGuard * @keyFunctions: * - register(): Register a resource for auto-disposal * - dispose(): Manually dispose of a specific resource * - disposeAll(): Dispose all resources and shut down * - startCleanupTimer(): Periodic cleanup of expired resources * @dependencies: * - logger: Logging utilities for resource tracking * - Node.js process handlers for graceful shutdown * @context: Prevents memory leaks from timers, watchers, processes, streams, and other resources by providing centralized lifecycle management */ import { logger } from '../utils/logger'; export interface Resource { id: string; type: 'timer' | 'watcher' | 'process' | 'stream' | 'worker'; createdAt: Date; metadata?: Record<string, any>; dispose: () => Promise<void> | void; } /** * Central resource registry that tracks and auto-disposes resources. * Prevents memory leaks from timers, watchers, and other resources. */ export class ResourceGuard { private resources = new Map<string, Resource>(); private cleanupTimer?: NodeJS.Timeout; private maxAge: number = 30 * 60 * 1000; // 30 minutes private disposed = false; private static handlersInstalled = false; constructor() { this.startCleanupTimer(); this.setupProcessHandlers(); } /** * Register a resource for auto-disposal. */ register(resource: Omit<Resource, 'createdAt'>): string { if (this.disposed) { throw new Error('ResourceGuard has been disposed'); } const fullResource: Resource = { ...resource, createdAt: new Date(), }; this.resources.set(resource.id, fullResource); logger.debug(`Registered ${resource.type} resource: ${resource.id}`); return resource.id; } /** * Manually dispose of a specific resource. */ async dispose(resourceId: string): Promise<boolean> { const resource = this.resources.get(resourceId); if (!resource) { return false; } try { await resource.dispose(); this.resources.delete(resourceId); logger.debug(`Disposed ${resource.type} resource: ${resourceId}`); return true; } catch (error) { logger.error(`Error disposing resource ${resourceId}:`, { error, resourceType: resource.type, }); // Remove from registry even if disposal failed to prevent retry loops this.resources.delete(resourceId); return false; } } /** * Dispose all resources and shut down. */ async disposeAll(permanent: boolean = false): Promise<void> { // If we're already in a disposed state and asked again, just return if (this.disposed && permanent) { return; } this.disposed = true; if (this.cleanupTimer) { clearInterval(this.cleanupTimer); this.cleanupTimer = undefined; } const resourceIds = Array.from(this.resources.keys()); logger.info(`Disposing all resources: ${resourceIds.length} items`); const disposePromises = resourceIds.map(id => this.dispose(id)); await Promise.allSettled(disposePromises); this.resources.clear(); logger.info('ResourceGuard disposed'); // For tests and reusable environments, allow re-initialization by default if (!permanent) { this.disposed = false; this.startCleanupTimer(); } } /** * Get resource statistics. */ getStats(): { total: number; byType: Record<string, number>; oldestResource?: { id: string; type: string; age: number }; } { const byType: Record<string, number> = {}; let oldestResource: { id: string; type: string; age: number } | undefined; for (const resource of this.resources.values()) { byType[resource.type] = (byType[resource.type] || 0) + 1; const age = Date.now() - resource.createdAt.getTime(); if (!oldestResource || age > oldestResource.age) { oldestResource = { id: resource.id, type: resource.type, age, }; } } return { total: this.resources.size, byType, oldestResource, }; } /** * Create a managed timer that will be auto-disposed. */ createTimer(callback: () => void, delay: number, metadata?: Record<string, any>): string { const id = `timer_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const timer = setTimeout(() => { try { callback(); } catch (error) { logger.error(`Error in managed timer ${id}:`, { error }); } // Auto-remove after execution this.resources.delete(id); }, delay); this.register({ id, type: 'timer', metadata: { delay, ...metadata }, dispose: () => { clearTimeout(timer); }, }); return id; } /** * Create a managed interval that will be auto-disposed. */ createInterval(callback: () => void, interval: number, metadata?: Record<string, any>): string { const id = `interval_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const timer = setInterval(() => { try { callback(); } catch (error) { logger.error(`Error in managed interval ${id}:`, { error }); } }, interval); this.register({ id, type: 'timer', metadata: { interval, ...metadata }, dispose: () => { clearInterval(timer); }, }); return id; } /** * Create a managed file watcher that will be auto-disposed. */ createWatcher( path: string, callback: (event: string, filename?: string) => void, metadata?: Record<string, any> ): string { const fs = require('fs'); const id = `watcher_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const watcher = fs.watch(path, (event: string, filename?: string) => { try { callback(event, filename); } catch (error) { logger.error(`Error in managed watcher ${id}:`, { error, path, event }); } }); watcher.on('error', (error: Error) => { logger.error(`Watcher error for ${id}:`, { error, path }); }); this.register({ id, type: 'watcher', metadata: { path, ...metadata }, dispose: () => { watcher.close(); }, }); return id; } /** * Create a managed AbortController that will be auto-disposed. */ createAbortController(metadata?: Record<string, any>): { id: string; controller: AbortController; } { const id = `abort_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const controller = new AbortController(); this.register({ id, type: 'stream', metadata, dispose: () => { if (!controller.signal.aborted) { controller.abort(); } }, }); return { id, controller }; } /** * Start automatic cleanup of old resources. */ private startCleanupTimer(): void { this.cleanupTimer = setInterval( () => { this.cleanupOldResources().catch(error => { logger.error('Error during resource cleanup:', { error }); }); }, 5 * 60 * 1000 ); // Check every 5 minutes } /** * Clean up resources older than maxAge. */ private async cleanupOldResources(): Promise<void> { const now = Date.now(); const oldResources: string[] = []; for (const [id, resource] of this.resources) { if (now - resource.createdAt.getTime() > this.maxAge) { oldResources.push(id); } } if (oldResources.length > 0) { logger.info(`Cleaning up ${oldResources.length} old resources`); for (const id of oldResources) { await this.dispose(id); } } } /** * Set up process handlers for graceful shutdown. */ private setupProcessHandlers(): void { // Install process handlers only once per process to avoid listener leaks in tests if (ResourceGuard.handlersInstalled) { return; } ResourceGuard.handlersInstalled = true; const cleanup = () => { logger.info('Process exit detected, disposing resources...'); // Permanent disposal on process signals/exits this.disposeAll(true).catch(error => { logger.error('Error during process cleanup:', { error }); }); }; // Use once to avoid accumulating listeners across re-instantiations in test runs process.once('SIGINT', cleanup); process.once('SIGTERM', cleanup); process.once('exit', cleanup); } } // Singleton instance export const resourceGuard = new ResourceGuard();

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sbarron/AmbianceMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server