UNPKG

js-memory-leak-detector

Version:

A comprehensive memory leak detector for web applications with Redux Toolkit support

130 lines 4.96 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ReduxTracker = void 0; class ReduxTracker { constructor() { this.storeSubscriptions = new Map(); this.selectorCalls = new Map(); this.patchReduxStore(); } patchReduxStore() { // Wait for Redux store to be available if (typeof window !== 'undefined') { this.detectStoreFromWindow(); } } detectStoreFromWindow() { // Common ways Redux store is exposed const possibleStores = [ window.__REDUX_STORE__, window.store, window.__STORE__ ]; for (const store of possibleStores) { if (store && typeof store.subscribe === 'function') { this.patchStore(store); break; } } // Also check for React DevTools Redux extension if (window.__REDUX_DEVTOOLS_EXTENSION__) { this.setupDevToolsHook(); } } patchStore(store) { if (this.storeRef) return; // Already patched this.storeRef = store; this.originalStoreSubscribe = store.subscribe; const self = this; store.subscribe = function (listener) { const unsubscribe = self.originalStoreSubscribe.call(this, listener); // Track subscription const subscriptionId = Date.now() + Math.random(); self.storeSubscriptions.set(subscriptionId, { subscriber: listener, created: Date.now(), stack: new Error().stack }); // Return wrapped unsubscribe function return function () { self.storeSubscriptions.delete(subscriptionId); return unsubscribe(); }; }; } setupDevToolsHook() { // Hook into Redux DevTools to detect store creation const originalExtension = window.__REDUX_DEVTOOLS_EXTENSION__; const self = this; window.__REDUX_DEVTOOLS_EXTENSION__ = function (...args) { const enhancer = originalExtension.apply(this, args); return function (createStore) { return function (reducer, preloadedState) { const store = createStore(reducer, preloadedState); self.patchStore(store); return store; }; }; }; } trackSelectorUsage(selectorName) { const current = this.selectorCalls.get(selectorName) || { count: 0, lastCalled: 0 }; this.selectorCalls.set(selectorName, { count: current.count + 1, lastCalled: Date.now() }); } getActiveSubscriptions() { return this.storeSubscriptions.size; } detectLeaks() { const suspects = []; const now = Date.now(); // Check for excessive store subscriptions if (this.storeSubscriptions.size > 50) { suspects.push({ type: 'redux-subscription', severity: this.storeSubscriptions.size > 200 ? 'critical' : 'high', description: `${this.storeSubscriptions.size} active Redux store subscriptions detected`, count: this.storeSubscriptions.size }); } // Check for old subscriptions (component likely unmounted but didn't unsubscribe) let oldSubscriptions = 0; for (const [id, sub] of this.storeSubscriptions) { if (now - sub.created > 10 * 60 * 1000) { // 10 minutes oldSubscriptions++; } } if (oldSubscriptions > 10) { suspects.push({ type: 'redux-subscription', severity: oldSubscriptions > 50 ? 'critical' : 'medium', description: `${oldSubscriptions} Redux subscriptions active for more than 10 minutes`, count: oldSubscriptions }); } // Check for excessive selector calls (potential infinite re-renders) for (const [selector, stats] of this.selectorCalls) { if (stats.count > 1000 && now - stats.lastCalled < 60000) { // 1000 calls in last minute suspects.push({ type: 'redux-selector', severity: stats.count > 5000 ? 'critical' : 'high', description: `Selector "${selector}" called ${stats.count} times recently (potential infinite re-render)`, count: stats.count }); } } return suspects; } cleanup() { if (this.storeRef && this.originalStoreSubscribe) { this.storeRef.subscribe = this.originalStoreSubscribe; } this.storeSubscriptions.clear(); this.selectorCalls.clear(); } } exports.ReduxTracker = ReduxTracker; //# sourceMappingURL=redux-tracker.js.map