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
200 lines • 7.76 kB
JavaScript
// Event Listener Management System
// Comprehensive solution for memory leak prevention in browser environments
/**
* EventListenerManager - Prevents memory leaks by tracking and managing event listeners
*
* Key Features:
* - Tracks all addEventListener calls for automatic cleanup
* - Prevents duplicate listeners
* - Graceful error handling during cleanup
* - Memory leak detection and reporting
* - Backward compatible with standard addEventListener
*/
export class EventListenerManager {
listeners = new Map();
listenerIdCounter = 0;
/**
* Add an event listener with automatic tracking for cleanup
* Prevents duplicate listeners for the same target/event/handler combination
*/
addEventListener(target, event, handler, options) {
if (!target || !event || !handler) {
console.warn('EventListenerManager: Invalid parameters for addEventListener');
return;
}
// Generate unique ID for this listener
const listenerId = this.generateListenerId(target, event, handler);
// Prevent duplicate listeners
if (this.listeners.has(listenerId)) {
console.warn(`EventListenerManager: Duplicate listener prevented for ${event}`);
return;
}
try {
// Add the actual event listener
target.addEventListener(event, handler, options);
// Track the listener for cleanup
this.listeners.set(listenerId, {
target,
event,
handler,
addedAt: new Date(),
id: listenerId
});
}
catch (error) {
console.error('EventListenerManager: Failed to add event listener:', error);
}
}
/**
* Remove a specific event listener
*/
removeEventListener(target, event, handler) {
const listenerId = this.generateListenerId(target, event, handler);
const listener = this.listeners.get(listenerId);
if (!listener) {
return false;
}
try {
target.removeEventListener(event, handler);
this.listeners.delete(listenerId);
return true;
}
catch (error) {
console.error('EventListenerManager: Failed to remove event listener:', error);
// Still remove from tracking even if removal failed
this.listeners.delete(listenerId);
return false;
}
}
/**
* Remove all tracked event listeners
* Essential for preventing memory leaks
*/
cleanup() {
const cleanupErrors = [];
for (const [listenerId, listener] of this.listeners.entries()) {
try {
listener.target.removeEventListener(listener.event, listener.handler);
}
catch (error) {
cleanupErrors.push(error);
console.error(`EventListenerManager: Failed to cleanup listener ${listenerId}:`, error);
}
}
// Clear all tracking regardless of cleanup errors
this.listeners.clear();
if (cleanupErrors.length > 0) {
console.warn(`EventListenerManager: ${cleanupErrors.length} cleanup errors occurred`);
}
}
/**
* Get count of currently tracked listeners
*/
getActiveListenerCount() {
return this.listeners.size;
}
/**
* Get listeners filtered by event type
*/
getListenersByType(eventType) {
return Array.from(this.listeners.values()).filter(listener => listener.event === eventType);
}
/**
* Get detailed information about all tracked listeners
*/
getDetailedListenerInfo() {
return Array.from(this.listeners.values());
}
/**
* Detect potential memory leaks based on listener patterns
*/
detectPotentialLeaks() {
const eventCounts = new Map();
// Group listeners by event type
for (const listener of this.listeners.values()) {
if (!eventCounts.has(listener.event)) {
eventCounts.set(listener.event, []);
}
eventCounts.get(listener.event).push(listener);
}
const potentialLeaks = [];
for (const [event, listeners] of eventCounts.entries()) {
// Consider it a potential leak if:
// 1. More than 10 listeners for the same event type
// 2. Listeners older than 10 minutes
const oldListeners = listeners.filter(l => Date.now() - l.addedAt.getTime() > 10 * 60 * 1000 // 10 minutes
);
if (listeners.length > 10 || oldListeners.length > 0) {
const oldestListener = listeners.reduce((oldest, current) => current.addedAt < oldest.addedAt ? current : oldest);
potentialLeaks.push({
event,
count: listeners.length,
oldestListener: oldestListener.addedAt,
targets: listeners.map(l => this.getTargetDescription(l.target))
});
}
}
return potentialLeaks;
}
/**
* Generate unique ID for listener to prevent duplicates
*/
generateListenerId(target, event, handler) {
// Use a combination of target type, event, and handler reference
const targetId = this.getTargetDescription(target);
const handlerId = typeof handler === 'function'
? handler.toString().slice(0, 50) // Use function signature
: 'object-handler';
// Don't increment counter for duplicate checks - use consistent ID
return `${targetId}:${event}:${handlerId}`;
}
/**
* Get description of event target for debugging
*/
getTargetDescription(target) {
// Check for document/window safely (they might not exist in test environments)
if (typeof document !== 'undefined' && target === document)
return 'document';
if (typeof window !== 'undefined' && target === window)
return 'window';
// Check if Element exists and target is an instance of it (safe for Node.js environments)
if (typeof Element !== 'undefined' && target instanceof Element) {
const tagName = target.tagName?.toLowerCase() || 'element';
const id = target.id ? `#${target.id}` : '';
const className = target.className ? `.${target.className.split(' ').join('.')}` : '';
return `${tagName}${id}${className}`;
}
return target.constructor?.name || 'unknown-target';
}
/**
* Get debugging statistics
*/
getStats() {
const eventTypes = {};
const targetTypes = {};
let oldestListener = null;
for (const listener of this.listeners.values()) {
// Count by event type
eventTypes[listener.event] = (eventTypes[listener.event] || 0) + 1;
// Count by target type
const targetType = this.getTargetDescription(listener.target);
targetTypes[targetType] = (targetTypes[targetType] || 0) + 1;
// Track oldest listener
if (!oldestListener || listener.addedAt < oldestListener) {
oldestListener = listener.addedAt;
}
}
return {
totalListeners: this.listeners.size,
eventTypes,
oldestListener,
targetTypes
};
}
}
/**
* Global instance for easy access across the application
* This can be used to ensure all event listeners are tracked globally
*/
export const globalEventListenerManager = new EventListenerManager();
//# sourceMappingURL=event-listener-manager.js.map