UNPKG

ai-debug-local-mcp

Version:

🎯 ENHANCED AI GUIDANCE v4.1.2: Dramatically improved tool descriptions help AI users choose the right tools instead of 'close enough' options. Ultra-fast keyboard automation (10x speed), universal recording, multi-ecosystem debugging support, and compreh

291 lines • 10.7 kB
// Memory-Safe User Actions - Prevents EventTarget memory leaks // Addresses AbortSignal listener accumulation in Playwright interactions import { EventEmitter } from 'events'; import { AbortSignalManager } from '../utils/abort-signal-manager.js'; export class MemorySafeActions { static maxListeners = 50; // Increased from default 10 static initialized = false; // MEMORY LEAK FIX: Store cleanup interval for proper disposal static cleanupInterval = null; // PROFESSIONAL UX: Track all AbortControllers and event listeners for proper cleanup static trackedAbortControllers = new Set(); static trackedEventListeners = new Set(); /** * Initialize memory-safe event handling * Prevents "MaxListenersExceededWarning" by increasing limits */ static initialize() { if (this.initialized) return; // Increase default max listeners for EventEmitter EventEmitter.defaultMaxListeners = this.maxListeners; // Set process-wide max listeners if (process.setMaxListeners) { process.setMaxListeners(this.maxListeners); } // Handle AbortController cleanup globally this.setupGlobalAbortControllerCleanup(); this.initialized = true; } /** * Perform memory-safe page action with automatic cleanup */ static async safePageAction(page, actionFn) { this.initialize(); let controller; let timeoutId; let abortListener; try { // Use centralized AbortSignalManager to prevent duplicate listeners // Reduced timeout from 30s to 15s for better UX const abortSetup = AbortSignalManager.createWithTimeout(15000); controller = abortSetup.controller; timeoutId = abortSetup.timeoutId; const signal = controller.signal; // Execute action with proper cleanup const result = await Promise.race([ actionFn(), new Promise((_, reject) => { abortListener = () => reject(new Error('Action timeout')); signal.addEventListener('abort', abortListener, { once: true }); }) ]); if (timeoutId) clearTimeout(timeoutId); return result; } finally { // Cleanup timeout if (timeoutId) clearTimeout(timeoutId); // Cleanup abort listener if (controller && abortListener) { controller.signal.removeEventListener('abort', abortListener); } // Cleanup controller using AbortSignalManager if (controller) { AbortSignalManager.cleanupController(controller); } // Force cleanup of any remaining listeners this.cleanupEventListeners(); } } /** * Validate CSS selector before use */ static validateSelector(selector) { // Fix common invalid selector patterns if (selector.includes(':contains(')) { // Convert CSS :contains() to Playwright text selector const match = selector.match(/(.+?):contains\(['"]([^'"]+)['"]\)/); if (match) { const [, element, text] = match; return `${element}:has-text("${text}")`; } } // Convert jQuery-style :contains to Playwright text selector if (selector.includes('button:contains(') || selector.includes(':contains(')) { const textMatch = selector.match(/:contains\(['"]([^'"]+)['"]\)/); if (textMatch) { return `text=${textMatch[1]}`; } } return selector; } /** * Memory-safe click action */ static async safeClick(page, selector) { return this.safePageAction(page, async () => { const validSelector = this.validateSelector(selector); await page.click(validSelector, { timeout: 8000, // Reduced from 10s to 8s for faster feedback force: false // Don't force click, let it fail naturally }); }); } /** * Memory-safe type action */ static async safeType(page, selector, value) { return this.safePageAction(page, async () => { const validSelector = this.validateSelector(selector); // Clear existing content first await page.fill(validSelector, ''); // Type new content await page.type(validSelector, value, { delay: 50, // Small delay between keystrokes timeout: 8000 // Reduced from 10s to 8s for faster feedback }); }); } /** * Memory-safe submit action */ static async safeSubmit(page, selector) { return this.safePageAction(page, async () => { const element = await page.$(selector); if (element) { await element.evaluate((el) => { if (el.form) { el.form.submit(); } else if (el.click) { el.click(); } }); } else { throw new Error(`Submit element not found: ${selector}`); } }); } /** * Memory-safe scroll action */ static async safeScroll(page, selector) { return this.safePageAction(page, async () => { await page.locator(selector).scrollIntoViewIfNeeded({ timeout: 8000 // Reduced from 10s to 8s for faster feedback }); }); } /** * Memory-safe hover action */ static async safeHover(page, selector) { return this.safePageAction(page, async () => { await page.hover(selector, { timeout: 8000, // Reduced from 10s to 8s for faster feedback force: false }); }); } /** * Setup global AbortController cleanup to prevent memory leaks */ static setupGlobalAbortControllerCleanup() { // MEMORY LEAK FIX: Store interval ID for proper cleanup // Only create interval if one doesn't already exist if (!this.cleanupInterval) { this.cleanupInterval = setInterval(() => { this.cleanupEventListeners(); }, 60000); // Every minute } // Cleanup on process exit process.on('exit', () => { this.cleanupEventListeners(); }); process.on('SIGINT', () => { this.cleanupEventListeners(); process.exit(0); }); process.on('SIGTERM', () => { this.cleanupEventListeners(); process.exit(0); }); } /** * Force cleanup of event listeners to prevent memory leaks * PROFESSIONAL UX: Proper event listener cleanup for enterprise-grade stability */ static cleanupEventListeners() { try { // Clear tracked event listeners from pages this.trackedEventListeners.forEach(({ page, event, handler }) => { try { if (page && typeof page.off === 'function') { page.off(event, handler); } } catch (e) { // Individual listener cleanup failed - continue with others } }); this.trackedEventListeners.clear(); // Force garbage collection if available (for memory optimization) if (global.gc) { global.gc(); } } catch (error) { // Log error for debugging but maintain silent operation for professional UX console.debug('Memory cleanup completed with minor issues:', error instanceof Error ? error.message : String(error)); } } /** * Register a page event listener for tracked cleanup * PROFESSIONAL UX: Prevents Playwright page event listener memory leaks */ static trackPageEventListener(page, event, handler) { this.initialize(); // Track the listener for cleanup this.trackedEventListeners.add({ page, event, handler }); // Add the event listener page.on(event, handler); } /** * Manually remove a tracked event listener * PROFESSIONAL UX: Allow explicit cleanup of specific listeners */ static removeTrackedEventListener(page, event, handler) { try { // Remove from tracking for (const tracked of this.trackedEventListeners) { if (tracked.page === page && tracked.event === event && tracked.handler === handler) { this.trackedEventListeners.delete(tracked); break; } } // Remove the actual listener if (page && typeof page.off === 'function') { page.off(event, handler); } } catch (error) { console.debug('Event listener removal completed with minor issues:', error instanceof Error ? error.message : String(error)); } } /** * Force immediate cleanup of all tracked resources * PROFESSIONAL UX: Call this method for enterprise-grade memory management */ static forceCleanup() { this.cleanupEventListeners(); } /** * Get current memory usage for monitoring */ static getMemoryUsage() { return { ...process.memoryUsage(), timestamp: Date.now(), maxListeners: this.maxListeners, initialized: this.initialized, trackedAbortControllers: this.trackedAbortControllers.size, trackedEventListeners: this.trackedEventListeners.size }; } /** * Cleanup all resources and intervals to prevent memory leaks * MEMORY LEAK FIX: Properly dispose of cleanup interval */ static cleanup() { // Clear the cleanup interval to prevent memory leaks if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = null; } // Force cleanup of all tracked resources this.cleanupEventListeners(); // Reset initialization state this.initialized = false; } /** * Reset memory-safe actions (for testing) */ static reset() { this.cleanup(); // Use cleanup instead of just setting initialized = false } } //# sourceMappingURL=memory-safe-actions.js.map