js-memory-leak-detector
Version:
A comprehensive memory leak detector for web applications with Redux Toolkit support
130 lines • 4.96 kB
JavaScript
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
;