mirror of
https://github.com/basicswap/basicswap.git
synced 2025-11-05 18:38:09 +01:00
509 lines
18 KiB
JavaScript
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 });
|
|
}
|
|
}
|