@memlab/lens 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +63 -0
  2. package/dist/index.js +112 -0
  3. package/explainer.md +54 -0
  4. package/package.json +34 -0
  5. package/playwright.config.ts +21 -0
  6. package/src/config/config.ts +32 -0
  7. package/src/core/dom-observer.ts +189 -0
  8. package/src/core/event-listener-tracker.ts +171 -0
  9. package/src/core/react-fiber-analysis.ts +123 -0
  10. package/src/core/react-memory-scan.ts +366 -0
  11. package/src/core/types.ts +180 -0
  12. package/src/core/valid-component-name.ts +17 -0
  13. package/src/extensions/basic-extension.ts +41 -0
  14. package/src/extensions/dom-visualization-extension.ts +42 -0
  15. package/src/index.ts +16 -0
  16. package/src/memlens.lib.js.flow +75 -0
  17. package/src/memlens.lib.ts +22 -0
  18. package/src/memlens.run.ts +21 -0
  19. package/src/tests/bundle/lib.bundle.test.ts +31 -0
  20. package/src/tests/bundle/run.bundle.start.head.test.ts +48 -0
  21. package/src/tests/bundle/run.bundle.start.test.ts +48 -0
  22. package/src/tests/bundle/run.bundle.test.ts +51 -0
  23. package/src/tests/fiber/dev.fiber.complex.dev.test.ts +92 -0
  24. package/src/tests/fiber/dev.fiber.complex.list.dev.test.ts +118 -0
  25. package/src/tests/fiber/dev.fiber.complex.prod.test.ts +92 -0
  26. package/src/tests/fiber/dev.fiber.name.dev.test.ts +77 -0
  27. package/src/tests/fiber/dev.fiber.name.prod.test.ts +79 -0
  28. package/src/tests/lib/babel.prod.js +4 -0
  29. package/src/tests/lib/react-dom-v18.dev.js +29926 -0
  30. package/src/tests/lib/react-dom-v18.prod.js +269 -0
  31. package/src/tests/lib/react-v18.dev.js +3345 -0
  32. package/src/tests/lib/react-v18.prod.js +33 -0
  33. package/src/tests/manual/playwright-open-manual.js +40 -0
  34. package/src/tests/manual/todo-list/todo-with-run.bundle.html +80 -0
  35. package/src/tests/utils/test-utils.ts +28 -0
  36. package/src/utils/intersection-observer.ts +65 -0
  37. package/src/utils/react-fiber-utils.ts +212 -0
  38. package/src/utils/utils.ts +201 -0
  39. package/src/utils/weak-ref-utils.ts +308 -0
  40. package/src/visual/components/component-stack-panel.ts +85 -0
  41. package/src/visual/components/control-widget.ts +96 -0
  42. package/src/visual/components/overlay-rectangle.ts +167 -0
  43. package/src/visual/components/status-text.ts +53 -0
  44. package/src/visual/components/toggle-button.ts +57 -0
  45. package/src/visual/components/visual-overlay.ts +19 -0
  46. package/src/visual/dom-element-visualizer-interactive.ts +358 -0
  47. package/src/visual/dom-element-visualizer.ts +130 -0
  48. package/src/visual/visual-utils.ts +89 -0
  49. package/tsconfig.json +18 -0
  50. package/webpack.config.js +105 -0
package/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # MemLens
2
+
3
+ ## What is this?
4
+
5
+ MemLens is a debugging tool that helps identify memory leaks in React applications. It tracks:
6
+
7
+ - Detached DOM elements that are no longer connected to the document but still held in memory
8
+ - Unmounted React Fiber nodes that haven't been properly cleaned up
9
+ - Memory usage patterns and growth over time
10
+
11
+ The tool provides:
12
+
13
+ 1. Real-time memory monitoring through the browser console
14
+ 2. Visual representation of problematic DOM elements and React components
15
+ 3. Memory usage statistics and trends
16
+
17
+ This can help developers identify:
18
+ - Components that aren't properly cleaned up
19
+
20
+ ## How to Build?
21
+
22
+ ```bash
23
+ webpack
24
+ ```
25
+
26
+ ## How to Test?
27
+
28
+ 1. Install Playwright
29
+
30
+ ```bash
31
+ npx playwright install
32
+ npx playwright install-deps
33
+ ```
34
+
35
+ 2. Run the test
36
+
37
+ ```bash
38
+ npm run test:e2e
39
+ ```
40
+
41
+ 3. Test manually by copying and pasting the content of `dist/run.bundle.js`
42
+ into web console
43
+
44
+
45
+ ## TODO List
46
+ * Note that document.querySelectorAll('*') only captures DOM elements on the DOM tree
47
+ * So the DOM tree scanning and component name analysis must be done in a frequent interval (every 10s or so)
48
+ * Extensible framework for tracking additional metadata
49
+ * Auto diffing and counting detached Element and unmounted Fiber nodes
50
+ * being able to summarize the leaked components that was not reported (this can reduce the report overhead)
51
+ * Improve the DOM visualizer - only visualize the common ancestors of the detached DOM elements and unmounted fiber nodes
52
+ * Improving the scanning efficiency so that the overhead is minimal in production environment
53
+ * Improve the fiber tree traversal efficiency (there are redundant traversals right now)
54
+ * Not only keep track of detached DOM elements, but also keep track of unmounted fiber nodes in WeakMap and WeakSet
55
+ * DOM visualizer is leaking canvas DOM elements after each scan
56
+ * Monitor event listener leaks?
57
+ * Real-time memory usage graphs
58
+ * Real-time component count and other react memory scan obtained stats graphs
59
+ * Component re-render heat maps
60
+ * Interactive component tree navigation
61
+ * Browser extension integration
62
+ * centralized config file
63
+ * centralized exception handling
package/dist/index.js ADDED
@@ -0,0 +1,112 @@
1
+ /******/ (() => { // webpackBootstrap
2
+ /******/ "use strict";
3
+ /******/ var __webpack_modules__ = ({
4
+
5
+ /***/ 3:
6
+ /***/ ((module) => {
7
+
8
+ module.exports = require("path");
9
+
10
+ /***/ }),
11
+
12
+ /***/ 383:
13
+ /***/ ((module) => {
14
+
15
+ module.exports = require("fs");
16
+
17
+ /***/ }),
18
+
19
+ /***/ 783:
20
+ /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
21
+
22
+
23
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
24
+ if (k2 === undefined) k2 = k;
25
+ var desc = Object.getOwnPropertyDescriptor(m, k);
26
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
27
+ desc = { enumerable: true, get: function() { return m[k]; } };
28
+ }
29
+ Object.defineProperty(o, k2, desc);
30
+ }) : (function(o, m, k, k2) {
31
+ if (k2 === undefined) k2 = k;
32
+ o[k2] = m[k];
33
+ }));
34
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
35
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
36
+ }) : function(o, v) {
37
+ o["default"] = v;
38
+ });
39
+ var __importStar = (this && this.__importStar) || (function () {
40
+ var ownKeys = function(o) {
41
+ ownKeys = Object.getOwnPropertyNames || function (o) {
42
+ var ar = [];
43
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
44
+ return ar;
45
+ };
46
+ return ownKeys(o);
47
+ };
48
+ return function (mod) {
49
+ if (mod && mod.__esModule) return mod;
50
+ var result = {};
51
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
52
+ __setModuleDefault(result, mod);
53
+ return result;
54
+ };
55
+ })();
56
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
57
+ exports.getBundleContent = getBundleContent;
58
+ /**
59
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
60
+ *
61
+ * This source code is licensed under the MIT license found in the
62
+ * LICENSE file in the root directory of this source tree.
63
+ *
64
+ * @format
65
+ * @oncall memory_lab
66
+ */
67
+ const fs = __importStar(__webpack_require__(383));
68
+ const path = __importStar(__webpack_require__(3));
69
+ function getBundleContent() {
70
+ const bundlePath = path.join(__dirname, 'memlens.run.bundle.min.js');
71
+ return fs.readFileSync(bundlePath, 'utf-8');
72
+ }
73
+
74
+
75
+ /***/ })
76
+
77
+ /******/ });
78
+ /************************************************************************/
79
+ /******/ // The module cache
80
+ /******/ var __webpack_module_cache__ = {};
81
+ /******/
82
+ /******/ // The require function
83
+ /******/ function __webpack_require__(moduleId) {
84
+ /******/ // Check if module is in cache
85
+ /******/ var cachedModule = __webpack_module_cache__[moduleId];
86
+ /******/ if (cachedModule !== undefined) {
87
+ /******/ return cachedModule.exports;
88
+ /******/ }
89
+ /******/ // Create a new module (and put it into the cache)
90
+ /******/ var module = __webpack_module_cache__[moduleId] = {
91
+ /******/ // no module.id needed
92
+ /******/ // no module.loaded needed
93
+ /******/ exports: {}
94
+ /******/ };
95
+ /******/
96
+ /******/ // Execute the module function
97
+ /******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);
98
+ /******/
99
+ /******/ // Return the exports of the module
100
+ /******/ return module.exports;
101
+ /******/ }
102
+ /******/
103
+ /************************************************************************/
104
+ /******/
105
+ /******/ // startup
106
+ /******/ // Load entry module and return exports
107
+ /******/ // This entry module is referenced by other modules so it can't be inlined
108
+ /******/ var __webpack_exports__ = __webpack_require__(783);
109
+ /******/ module.exports = __webpack_exports__;
110
+ /******/
111
+ /******/ })()
112
+ ;
package/explainer.md ADDED
@@ -0,0 +1,54 @@
1
+
2
+
3
+ ### Purpose and Core Functionality
4
+ This project is a specialized production memory diagnostic collection and local dev debugging tool designed to detect and visualize memory leaks in React applications, with a particular focus on identifying detached components that should have been garbage collected but remain in memory.
5
+
6
+ ### How It Works
7
+
8
+ #### Detection System
9
+ The module operates through multiple layers of monitoring:
10
+ * **DOM Scanning**: Continuously scans the Document Object Model to create a comprehensive map of all elements present in the application.
11
+ * **React Fiber Analysis**: Specifically tracks React's internal fiber tree structure, which represents the component hierarchy and their relationships.
12
+ * **Memory Leak Detection**: Identifies components that are no longer attached to the main DOM tree but still persist in memory, indicating potential memory leaks.
13
+
14
+ #### Monitoring Process
15
+ The system employs a dual-monitoring approach:
16
+ * **Periodic Scanning**: Runs regular scans at configurable intervals to check for detached components.
17
+ * **Real-time Observation**: Uses mutation observers to detect DOM changes as they happen, ensuring immediate detection of potential issues.
18
+
19
+ #### Visualization System
20
+ The visualization component provides real-time feedback through:
21
+ * **Overlay Layer**: Creates a transparent canvas overlay that sits above the application.
22
+ * **Visual Indicators**: Draws highlighting boxes around problematic components.
23
+ * **Component Information**: Displays relevant information about detected issues, including component names and their locations.
24
+
25
+ ### Key Features
26
+
27
+ #### Memory Safety
28
+ - Implements memory-safe tracking mechanisms using weak references
29
+ - Avoids creating memory leaks while detecting them
30
+ - Ensures the debugging tool itself doesn't impact application performance
31
+
32
+ #### Non-Intrusive Design
33
+ - Operates without interfering with the application's normal functionality
34
+ - Uses a transparent overlay that doesn't block user interactions
35
+ - Can be easily enabled or disabled as needed
36
+
37
+ #### Developer Tools Integration
38
+ - Provides detailed debugging information in development mode
39
+ - Offers subscription capabilities for external monitoring tools
40
+ - Maintains detailed statistics about detected issues
41
+
42
+ ### Use Cases
43
+
44
+ * **Production Performance Monitoring**
45
+ - Monitoring application memory usage patterns
46
+ - Detecting gradual memory leaks in long-running applications
47
+ - Identifying patterns in component detachment
48
+
49
+ * **Development Debugging**
50
+ - Identifying component cleanup issues during development
51
+ - Tracking down memory leaks in complex component hierarchies
52
+ - Visualizing component lifecycle issues
53
+
54
+ This tool serves as an essential debugging aid for React developers, helping them maintain efficient and leak-free applications while providing valuable insights into component behavior and memory management patterns.
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@memlab/lens",
3
+ "version": "1.0.0",
4
+ "license": "MIT",
5
+ "description": "MemLens is a tool for inspecting memory leaks in browser",
6
+ "author": "Liang Gong <lgong@meta.com>",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
12
+ "scripts": {
13
+ "build": "webpack",
14
+ "rebuild": "webpack",
15
+ "reset-build": "rm package-lock.json; rm -rf node_modules; npm i; webpack",
16
+ "test": "playwright test --output=out",
17
+ "test:manual": "node src/tests/manual/playwright-open-manual.js todo-list/todo-with-run.bundle.html",
18
+ "test:e2e": "playwright test --output=out",
19
+ "test:rebuild": "webpack && playwright test --output=out",
20
+ "build-pkg": "webpack",
21
+ "publish-patch": "npm publish",
22
+ "clean-pkg": "rm -rf ./dist && rm -rf ./node_modules && rm -f ./tsconfig.tsbuildinfo && rm -rf out"
23
+ },
24
+ "devDependencies": {
25
+ "@playwright/test": "^1.49.1",
26
+ "@types/react": "^18.3.12",
27
+ "@types/react-reconciler": "^0.28.8",
28
+ "react-reconciler": "^0.31.0",
29
+ "ts-loader": "^9.5.1",
30
+ "typescript": "^5.7.2",
31
+ "webpack": "^5.97.1",
32
+ "webpack-cli": "^5.1.4"
33
+ }
34
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @format
8
+ * @oncall memory_lab
9
+ */
10
+ import process from 'process';
11
+ import {PlaywrightTestConfig} from '@playwright/test';
12
+
13
+ const config: PlaywrightTestConfig = {
14
+ testDir: './src/tests',
15
+ use: {
16
+ // Serve files from the root directory
17
+ baseURL: `file://${process.cwd()}`,
18
+ },
19
+ };
20
+
21
+ export default config;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @format
8
+ * @oncall memory_lab
9
+ */
10
+
11
+ import {AnyValue, Config} from '../core/types';
12
+
13
+ // Performance Configuration
14
+ export const performanceConfig = {
15
+ scanIntervalMs: 1000,
16
+ maxComponentStackDepth: 100,
17
+ memoryMeasurementIntervalMs: 5000,
18
+ };
19
+
20
+ // Feature Flags
21
+ export const featureFlags = {
22
+ enableMutationObserver: true,
23
+ enableMemoryTracking: true,
24
+ enableComponentStack: true,
25
+ enableConsoleLogs: (window as AnyValue)?.TEST_MEMORY_SCAN,
26
+ };
27
+
28
+ // overall Config
29
+ export const config: Config = {
30
+ performance: performanceConfig,
31
+ features: featureFlags,
32
+ };
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @format
8
+ * @oncall memory_lab
9
+ */
10
+ import {isWeakAPINative} from '../utils/weak-ref-utils';
11
+ import {isVisualizerElement} from '../visual/visual-utils';
12
+ import {DOMElementStats, DOMObserveCallback, Optional} from './types';
13
+
14
+ // if false, we only capture the top-most element of a removed subtree
15
+ const CAPTURE_ALL_REMOVED_ELEMENTS = true;
16
+ const IS_WEAK_REF_NATIVE = isWeakAPINative();
17
+ const IS_MUTATION_OBSERVER_SUPPORTED = window.MutationObserver !== undefined;
18
+
19
+ export class DOMObserver {
20
+ #elementCount: number;
21
+ #detachedElementCount: number;
22
+ #mutationObserver: Optional<MutationObserver>;
23
+ #trackedElements: Array<WeakRef<Element>>;
24
+ #trackedElementSet: WeakSet<Element>;
25
+ #consolidateInterval: Optional<number>;
26
+ #observeCallback: Array<DOMObserveCallback>;
27
+
28
+ constructor() {
29
+ this.#elementCount = 0;
30
+ this.#detachedElementCount = 0;
31
+ this.#trackedElements = [];
32
+ this.#trackedElementSet = new WeakSet();
33
+ this.#observeCallback = [];
34
+ this.startMonitoring();
35
+ }
36
+
37
+ register(callback: DOMObserveCallback): void {
38
+ this.#observeCallback.push(callback);
39
+ }
40
+
41
+ startMonitoring(): void {
42
+ if (!IS_WEAK_REF_NATIVE || !IS_MUTATION_OBSERVER_SUPPORTED) {
43
+ return;
44
+ }
45
+ if (this.#mutationObserver != null) {
46
+ return;
47
+ }
48
+ this.#mutationObserver = new MutationObserver(mutationsList => {
49
+ let newlyAdded: Array<WeakRef<Element>> = [];
50
+ const updateCallback = (node: Node) => {
51
+ if (node == null) {
52
+ return;
53
+ }
54
+ if (node.nodeType != Node.ELEMENT_NODE) {
55
+ return;
56
+ }
57
+ const element = node as Element;
58
+ if (isVisualizerElement(element)) {
59
+ return;
60
+ }
61
+ if (CAPTURE_ALL_REMOVED_ELEMENTS) {
62
+ const diff = this.#gatherAllElementsInRemovedSubtree(element);
63
+ newlyAdded = [...newlyAdded, ...diff];
64
+ } else if (!this.#trackedElementSet.has(element)) {
65
+ const ref = new WeakRef(element);
66
+ this.#trackedElements.push(ref);
67
+ this.#trackedElementSet.add(element);
68
+ newlyAdded.push(ref);
69
+ }
70
+ };
71
+
72
+ mutationsList.forEach(mutation => {
73
+ mutation.addedNodes.forEach(updateCallback);
74
+ mutation.removedNodes.forEach(updateCallback);
75
+ });
76
+
77
+ this.#observeCallback.forEach(cb => cb(newlyAdded));
78
+ });
79
+
80
+ const waitForBodyAndObserve = () => {
81
+ if (document.body) {
82
+ // observe changes in DOM tree
83
+ this.#mutationObserver?.observe(document.body, {
84
+ childList: true, // Detect direct additions/removals
85
+ subtree: true, // Observe all descendants
86
+ });
87
+ } else {
88
+ setTimeout(waitForBodyAndObserve, 0);
89
+ }
90
+ };
91
+
92
+ waitForBodyAndObserve();
93
+
94
+ // starts consolidating removedElements weak references;
95
+ this.#consolidateInterval = window.setInterval(() => {
96
+ this.#consolidateElementRefs();
97
+ }, 3000);
98
+ }
99
+
100
+ stopMonitoring(): void {
101
+ if (this.#mutationObserver != null) {
102
+ this.#mutationObserver.disconnect();
103
+ this.#mutationObserver = null;
104
+ }
105
+ if (this.#consolidateInterval != null) {
106
+ window.clearInterval(this.#consolidateInterval);
107
+ this.#consolidateInterval = null;
108
+ }
109
+ // TODO: clean up memory
110
+ }
111
+
112
+ #consolidateElementRefs(): void {
113
+ const consolidatedList = [];
114
+ const trackedElements = new Set<Element>();
115
+ for (const ref of this.#trackedElements) {
116
+ const element = ref.deref();
117
+ if (element != null && !trackedElements.has(element)) {
118
+ consolidatedList.push(ref);
119
+ trackedElements.add(element);
120
+ }
121
+ }
122
+ this.#trackedElements = consolidatedList;
123
+ }
124
+
125
+ #getTotalDOMElementCount(): number {
126
+ return (this.#elementCount =
127
+ document?.getElementsByTagName('*')?.length ?? 0);
128
+ }
129
+
130
+ #getDetachedElementCount(): number {
131
+ let count = 0;
132
+ for (const ref of this.#trackedElements) {
133
+ const element = ref.deref();
134
+ if (element && element.isConnected === false) {
135
+ ++count;
136
+ }
137
+ }
138
+ return (this.#detachedElementCount = count);
139
+ }
140
+
141
+ #gatherAllElementsInRemovedSubtree(node: Node): Array<WeakRef<Element>> {
142
+ const queue = [node];
143
+ const visited = new Set<Node>();
144
+ const newlyAdded: Array<WeakRef<Element>> = [];
145
+ while (queue.length > 0) {
146
+ const current = queue.pop();
147
+ if (current == null || visited.has(current)) {
148
+ continue;
149
+ }
150
+ if (current?.nodeType !== Node.ELEMENT_NODE) {
151
+ continue;
152
+ }
153
+ const element = current as Element;
154
+ if (isVisualizerElement(element)) {
155
+ continue;
156
+ }
157
+ visited.add(element);
158
+ if (!this.#trackedElementSet.has(element)) {
159
+ const ref = new WeakRef(element);
160
+ this.#trackedElements.push(ref);
161
+ this.#trackedElementSet.add(element);
162
+ newlyAdded.push(ref);
163
+ }
164
+ const list = element.childNodes;
165
+ for (let i = 0; i < list.length; ++i) {
166
+ queue.push(list[i]);
167
+ }
168
+ }
169
+ return newlyAdded;
170
+ }
171
+
172
+ getDOMElements(): Array<WeakRef<Element>> {
173
+ return [...this.#trackedElements];
174
+ }
175
+
176
+ getStats(): DOMElementStats {
177
+ try {
178
+ this.#elementCount = this.#getTotalDOMElementCount();
179
+ this.#detachedElementCount = this.#getDetachedElementCount();
180
+ } catch (ex) {
181
+ // do nothing
182
+ }
183
+
184
+ return {
185
+ elements: this.#elementCount,
186
+ detachedElements: this.#detachedElementCount,
187
+ };
188
+ }
189
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @format
8
+ * @oncall memory_lab
9
+ */
10
+ import type {Fiber} from 'react-reconciler';
11
+ import {getFiberNodeFromElement} from '../utils/react-fiber-utils';
12
+ import {WeakMapPlus} from '../utils/weak-ref-utils';
13
+
14
+ type EventListenerEntry = {
15
+ type: string;
16
+ cb: WeakRef<EventListenerOrEventListenerObject>;
17
+ options?: boolean | AddEventListenerOptions;
18
+ fiber?: WeakRef<Fiber>;
19
+ };
20
+
21
+ type DetachedListenerGroup = {
22
+ type: string;
23
+ count: number;
24
+ entries: WeakRef<EventListenerEntry>[];
25
+ };
26
+
27
+ export class EventListenerTracker {
28
+ private static instance: EventListenerTracker | null = null;
29
+ #listenerMap: WeakMapPlus<EventTarget, EventListenerEntry[]>;
30
+ #detachedListeners: Map<string, DetachedListenerGroup[]>;
31
+ #originalAddEventListener: typeof EventTarget.prototype.addEventListener;
32
+ #originalRemoveEventListener: typeof EventTarget.prototype.removeEventListener;
33
+
34
+ private constructor() {
35
+ this.#listenerMap = new WeakMapPlus({fallback: 'noop', cleanupMs: 100});
36
+ this.#detachedListeners = new Map();
37
+ this.#originalAddEventListener = EventTarget.prototype.addEventListener;
38
+ this.#originalRemoveEventListener =
39
+ EventTarget.prototype.removeEventListener;
40
+ this.#patchEventListeners();
41
+ }
42
+
43
+ static getInstance(): EventListenerTracker {
44
+ if (!EventListenerTracker.instance) {
45
+ EventListenerTracker.instance = new EventListenerTracker();
46
+ }
47
+ return EventListenerTracker.instance;
48
+ }
49
+
50
+ #patchEventListeners(): void {
51
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
52
+ const self = this;
53
+ EventTarget.prototype.addEventListener = function (
54
+ type: string,
55
+ listener: EventListenerOrEventListenerObject,
56
+ options?: boolean | AddEventListenerOptions,
57
+ ) {
58
+ self.#originalAddEventListener.call(this, type, listener, options);
59
+ if (this instanceof Element) {
60
+ const fiber = getFiberNodeFromElement(this);
61
+ const entry: EventListenerEntry = {
62
+ type,
63
+ cb: new WeakRef(listener),
64
+ options,
65
+ fiber: fiber ? new WeakRef(fiber) : undefined,
66
+ };
67
+ const listeners = self.#listenerMap.get(this) ?? [];
68
+ listeners.push(entry);
69
+ self.#listenerMap.set(this, listeners);
70
+ }
71
+ };
72
+
73
+ EventTarget.prototype.removeEventListener = function (
74
+ type: string,
75
+ listener: EventListenerOrEventListenerObject,
76
+ options?: boolean | EventListenerOptions,
77
+ ) {
78
+ self.#originalRemoveEventListener.call(this, type, listener, options);
79
+ if (this instanceof Element) {
80
+ const listeners = self.#listenerMap.get(this);
81
+ if (listeners) {
82
+ const index = listeners.findIndex(
83
+ entry =>
84
+ entry.type === type &&
85
+ entry.cb.deref() === listener &&
86
+ entry.options === options,
87
+ );
88
+ if (index !== -1) {
89
+ listeners.splice(index, 1);
90
+ }
91
+ if (listeners.length === 0) {
92
+ self.#listenerMap.delete(this);
93
+ } else {
94
+ self.#listenerMap.set(this, listeners);
95
+ }
96
+ }
97
+ }
98
+ };
99
+ }
100
+
101
+ #unpatchEventListeners(): void {
102
+ EventTarget.prototype.addEventListener = this.#originalAddEventListener;
103
+ EventTarget.prototype.removeEventListener =
104
+ this.#originalRemoveEventListener;
105
+ }
106
+
107
+ addListener(
108
+ el: EventTarget,
109
+ type: string,
110
+ cb: EventListenerOrEventListenerObject,
111
+ options?: boolean | AddEventListenerOptions,
112
+ ): void {
113
+ el.addEventListener(type, cb, options);
114
+ }
115
+
116
+ removeListener(
117
+ el: EventTarget,
118
+ type: string,
119
+ cb: EventListenerOrEventListenerObject,
120
+ options?: boolean | AddEventListenerOptions,
121
+ ): void {
122
+ el.removeEventListener(type, cb, options);
123
+ }
124
+
125
+ scan(
126
+ getComponentName: (elRef: WeakRef<Element>) => string,
127
+ ): Map<string, DetachedListenerGroup[]> {
128
+ const detachedListeners = new Map<string, DetachedListenerGroup[]>();
129
+
130
+ for (const [el, listeners] of this.#listenerMap.entries()) {
131
+ if (el instanceof Element && !el.isConnected) {
132
+ for (const listener of listeners) {
133
+ // Skip if the callback has been garbage collected
134
+ if (!listener.cb.deref()) continue;
135
+
136
+ const componentName = getComponentName(new WeakRef(el));
137
+ if (!detachedListeners.has(componentName)) {
138
+ detachedListeners.set(componentName, []);
139
+ }
140
+
141
+ const groups = detachedListeners.get(componentName);
142
+ let group = groups?.find(g => g.type === listener.type);
143
+ if (!group) {
144
+ group = {
145
+ type: listener.type,
146
+ count: 0,
147
+ entries: [],
148
+ };
149
+ groups?.push(group);
150
+ }
151
+ group.count++;
152
+ group.entries.push(new WeakRef(listener));
153
+ }
154
+ }
155
+ }
156
+
157
+ this.#detachedListeners = detachedListeners;
158
+ return detachedListeners;
159
+ }
160
+
161
+ getDetachedListeners(): Map<string, DetachedListenerGroup[]> {
162
+ return this.#detachedListeners;
163
+ }
164
+
165
+ destroy(): void {
166
+ this.#unpatchEventListeners();
167
+ this.#listenerMap.destroy();
168
+ this.#detachedListeners.clear();
169
+ EventListenerTracker.instance = null;
170
+ }
171
+ }