Files
basicswap/basicswap/static/js/modules/cleanup-manager.js
2025-10-10 11:08:23 +02:00

509 lines
18 KiB
JavaScript

const CleanupManager = (function() {
const state = {
eventListeners: [],
timeouts: [],
intervals: [],
animationFrames: [],
resources: new Map(),
debug: false,
memoryOptimizationInterval: null
};
function log(message, ...args) {
if (state.debug) {
console.log(`[CleanupManager] ${message}`, ...args);
}
}
const publicAPI = {
addListener: function(element, type, handler, options = false) {
if (!element) {
log('Warning: Attempted to add listener to null/undefined element');
return handler;
}
element.addEventListener(type, handler, options);
state.eventListeners.push({ element, type, handler, options });
log(`Added ${type} listener to`, element);
return handler;
},
setTimeout: function(callback, delay) {
const id = window.setTimeout(callback, delay);
state.timeouts.push(id);
log(`Created timeout ${id} with ${delay}ms delay`);
return id;
},
setInterval: function(callback, delay) {
const id = window.setInterval(callback, delay);
state.intervals.push(id);
log(`Created interval ${id} with ${delay}ms delay`);
return id;
},
requestAnimationFrame: function(callback) {
const id = window.requestAnimationFrame(callback);
state.animationFrames.push(id);
log(`Requested animation frame ${id}`);
return id;
},
registerResource: function(type, resource, cleanupFn) {
const id = `${type}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
state.resources.set(id, { resource, cleanupFn });
log(`Registered custom resource ${id} of type ${type}`);
return id;
},
unregisterResource: function(id) {
const resourceInfo = state.resources.get(id);
if (resourceInfo) {
try {
resourceInfo.cleanupFn(resourceInfo.resource);
state.resources.delete(id);
log(`Unregistered and cleaned up resource ${id}`);
return true;
} catch (error) {
console.error(`[CleanupManager] Error cleaning up resource ${id}:`, error);
return false;
}
}
log(`Resource ${id} not found`);
return false;
},
clearTimeout: function(id) {
const index = state.timeouts.indexOf(id);
if (index !== -1) {
window.clearTimeout(id);
state.timeouts.splice(index, 1);
log(`Cleared timeout ${id}`);
}
},
clearInterval: function(id) {
const index = state.intervals.indexOf(id);
if (index !== -1) {
window.clearInterval(id);
state.intervals.splice(index, 1);
log(`Cleared interval ${id}`);
}
},
cancelAnimationFrame: function(id) {
const index = state.animationFrames.indexOf(id);
if (index !== -1) {
window.cancelAnimationFrame(id);
state.animationFrames.splice(index, 1);
log(`Cancelled animation frame ${id}`);
}
},
removeListener: function(element, type, handler, options = false) {
if (!element) return;
try {
element.removeEventListener(type, handler, options);
log(`Removed ${type} listener from`, element);
} catch (error) {
console.error(`[CleanupManager] Error removing event listener:`, error);
}
state.eventListeners = state.eventListeners.filter(
listener => !(listener.element === element &&
listener.type === type &&
listener.handler === handler)
);
},
removeListenersByElement: function(element) {
if (!element) return;
const listenersToRemove = state.eventListeners.filter(
listener => listener.element === element
);
listenersToRemove.forEach(({ element, type, handler, options }) => {
try {
element.removeEventListener(type, handler, options);
log(`Removed ${type} listener from`, element);
} catch (error) {
console.error(`[CleanupManager] Error removing event listener:`, error);
}
});
state.eventListeners = state.eventListeners.filter(
listener => listener.element !== element
);
},
clearAllTimeouts: function() {
state.timeouts.forEach(id => {
window.clearTimeout(id);
});
const count = state.timeouts.length;
state.timeouts = [];
log(`Cleared all timeouts (${count})`);
},
clearAllIntervals: function() {
state.intervals.forEach(id => {
window.clearInterval(id);
});
const count = state.intervals.length;
state.intervals = [];
log(`Cleared all intervals (${count})`);
},
clearAllAnimationFrames: function() {
state.animationFrames.forEach(id => {
window.cancelAnimationFrame(id);
});
const count = state.animationFrames.length;
state.animationFrames = [];
log(`Cancelled all animation frames (${count})`);
},
clearAllResources: function() {
let successCount = 0;
let errorCount = 0;
state.resources.forEach((resourceInfo, id) => {
try {
resourceInfo.cleanupFn(resourceInfo.resource);
successCount++;
} catch (error) {
console.error(`[CleanupManager] Error cleaning up resource ${id}:`, error);
errorCount++;
}
});
state.resources.clear();
log(`Cleared all custom resources (${successCount} success, ${errorCount} errors)`);
},
clearAllListeners: function() {
state.eventListeners.forEach(({ element, type, handler, options }) => {
if (element) {
try {
element.removeEventListener(type, handler, options);
} catch (error) {
console.error(`[CleanupManager] Error removing event listener:`, error);
}
}
});
const count = state.eventListeners.length;
state.eventListeners = [];
log(`Removed all event listeners (${count})`);
},
clearAll: function() {
const counts = {
listeners: state.eventListeners.length,
timeouts: state.timeouts.length,
intervals: state.intervals.length,
animationFrames: state.animationFrames.length,
resources: state.resources.size
};
this.clearAllListeners();
this.clearAllTimeouts();
this.clearAllIntervals();
this.clearAllAnimationFrames();
this.clearAllResources();
log(`All resources cleaned up:`, counts);
return counts;
},
getResourceCounts: function() {
return {
listeners: state.eventListeners.length,
timeouts: state.timeouts.length,
intervals: state.intervals.length,
animationFrames: state.animationFrames.length,
resources: state.resources.size,
total: state.eventListeners.length +
state.timeouts.length +
state.intervals.length +
state.animationFrames.length +
state.resources.size
};
},
setupMemoryOptimization: function(options = {}) {
const memoryCheckInterval = options.interval || 2 * 60 * 1000;
const maxCacheSize = options.maxCacheSize || 100;
const maxDataSize = options.maxDataSize || 1000;
if (state.memoryOptimizationInterval) {
this.clearInterval(state.memoryOptimizationInterval);
}
this.addListener(document, 'visibilitychange', () => {
if (document.hidden) {
log('Tab hidden - running memory optimization');
this.optimizeMemory({
maxCacheSize: maxCacheSize,
maxDataSize: maxDataSize
});
} else if (window.TooltipManager) {
window.TooltipManager.cleanup();
}
});
state.memoryOptimizationInterval = this.setInterval(() => {
if (document.hidden) {
log('Periodic memory optimization');
this.optimizeMemory({
maxCacheSize: maxCacheSize,
maxDataSize: maxDataSize
});
}
}, memoryCheckInterval);
log('Memory optimization setup complete');
return state.memoryOptimizationInterval;
},
optimizeMemory: function(options = {}) {
log('Running memory optimization');
if (window.TooltipManager && typeof window.TooltipManager.cleanup === 'function') {
window.TooltipManager.cleanup();
}
if (window.IdentityManager && typeof window.IdentityManager.limitCacheSize === 'function') {
window.IdentityManager.limitCacheSize(options.maxCacheSize || 100);
}
this.cleanupOrphanedResources();
if (window.gc) {
try {
window.gc();
log('Forced garbage collection');
} catch (e) {
}
}
document.dispatchEvent(new CustomEvent('memoryOptimized', {
detail: {
timestamp: Date.now(),
maxDataSize: options.maxDataSize || 1000
}
}));
log('Memory optimization complete');
},
cleanupOrphanedResources: function() {
let removedListeners = 0;
const validListeners = [];
for (let i = 0; i < state.eventListeners.length; i++) {
const listener = state.eventListeners[i];
if (!listener.element) {
removedListeners++;
continue;
}
try {
const isDetached = !(listener.element instanceof Node) ||
!document.body.contains(listener.element) ||
(listener.element.classList && listener.element.classList.contains('hidden')) ||
(listener.element.style && listener.element.style.display === 'none');
if (isDetached) {
try {
if (listener.element instanceof Node) {
listener.element.removeEventListener(listener.type, listener.handler, listener.options);
}
removedListeners++;
} catch (e) {
}
} else {
validListeners.push(listener);
}
} catch (e) {
log(`Error checking listener (removing): ${e.message}`);
removedListeners++;
}
}
if (removedListeners > 0) {
state.eventListeners = validListeners;
log(`Removed ${removedListeners} event listeners for detached/hidden elements`);
}
let removedResources = 0;
const resourcesForRemoval = [];
state.resources.forEach((info, id) => {
const resource = info.resource;
try {
if (resource instanceof Element && !document.body.contains(resource)) {
resourcesForRemoval.push(id);
}
if (resource && resource.element) {
if (resource.element instanceof Node && !document.body.contains(resource.element)) {
resourcesForRemoval.push(id);
}
}
} catch (e) {
log(`Error checking resource ${id}: ${e.message}`);
}
});
resourcesForRemoval.forEach(id => {
this.unregisterResource(id);
removedResources++;
});
if (removedResources > 0) {
log(`Removed ${removedResources} orphaned resources`);
}
if (window.TooltipManager) {
if (typeof window.TooltipManager.cleanupOrphanedTooltips === 'function') {
try {
window.TooltipManager.cleanupOrphanedTooltips();
} catch (e) {
if (typeof window.TooltipManager.cleanup === 'function') {
try {
window.TooltipManager.cleanup();
} catch (err) {
log(`Error cleaning up tooltips: ${err.message}`);
}
}
}
} else if (typeof window.TooltipManager.cleanup === 'function') {
try {
window.TooltipManager.cleanup();
} catch (e) {
log(`Error cleaning up tooltips: ${e.message}`);
}
}
}
try {
this.cleanupTooltipDOM();
} catch (e) {
log(`Error in cleanupTooltipDOM: ${e.message}`);
}
},
cleanupTooltipDOM: function() {
let removedElements = 0;
try {
const tooltipSelectors = [
'[role="tooltip"]',
'[id^="tooltip-"]',
'.tippy-box',
'[data-tippy-root]'
];
tooltipSelectors.forEach(selector => {
try {
const elements = document.querySelectorAll(selector);
elements.forEach(element => {
try {
if (!(element instanceof Element)) return;
const isDetached = !element.parentElement ||
!document.body.contains(element.parentElement) ||
element.classList.contains('hidden') ||
element.style.display === 'none' ||
element.style.visibility === 'hidden';
if (isDetached) {
try {
element.remove();
removedElements++;
} catch (e) {
}
}
} catch (err) {
}
});
} catch (err) {
log(`Error querying for ${selector}: ${err.message}`);
}
});
} catch (e) {
log(`Error in tooltip DOM cleanup: ${e.message}`);
}
if (removedElements > 0) {
log(`Removed ${removedElements} detached tooltip elements`);
}
},
setDebugMode: function(enabled) {
state.debug = Boolean(enabled);
log(`Debug mode ${state.debug ? 'enabled' : 'disabled'}`);
return state.debug;
},
dispose: function() {
this.clearAll();
log('CleanupManager disposed');
},
initialize: function(options = {}) {
if (options.debug !== undefined) {
this.setDebugMode(options.debug);
}
if (typeof window !== 'undefined' && !options.noAutoCleanup) {
this.addListener(window, 'beforeunload', () => {
this.clearAll();
});
}
if (typeof window !== 'undefined' && !options.noMemoryOptimization) {
this.setupMemoryOptimization(options.memoryOptions || {});
}
log('CleanupManager initialized');
return this;
}
};
return publicAPI;
})();
if (typeof module !== 'undefined' && module.exports) {
module.exports = CleanupManager;
}
if (typeof window !== 'undefined') {
window.CleanupManager = CleanupManager;
}
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
if (document.readyState === 'complete' || document.readyState === 'interactive') {
CleanupManager.initialize({ debug: false });
} else {
document.addEventListener('DOMContentLoaded', () => {
CleanupManager.initialize({ debug: false });
}, { once: true });
}
}