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
JavaScript
// 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