7
* @flow
8
*/
9
10
+import LRU from 'lru-cache';
11
import {
12
convertInspectedElementBackendToFrontend,
13
hydrateHelper,
15
} from 'react-devtools-shared/src/backendAPI';
16
import {fillInPath} from 'react-devtools-shared/src/hydration';
17
18
+import type {LRUCache} from 'react-devtools-shared/src/types';
19
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
20
import type {
21
InspectElementFullData,
27
InspectedElementResponseType,
28
} from 'react-devtools-shared/src/devtools/views/Components/types';
29
28
-// Map an Element in the Store to the most recent copy of its inspected data.
29
-// As updates comes from the backend, inspected data is updated.
30
-// Both this map and the inspected objects in it are mutable.
31
-// They should never be read from directly during render;
32
-// Use a Suspense cache to ensure that transitions work correctly and there is no tearing.
33
-const inspectedElementMap: WeakMap<
34
- Element,
30
+// Maps element ID to inspected data.
31
+// We use an LRU for this rather than a WeakMap because of how the "no-change" optimization works.
32
+// When the frontend polls the backend for an update on the element that's currently inspected,
33
+// the backend will send a "no-change" message if the element hasn't updated (rendered) since the last time it was asked.
34
+// In thid case, the frontend cache should reuse the previous (cached) value.
35
+// Using a WeakMap keyed on Element generally works well for this, since Elements are mutable and stable in the Store.
36
+// This doens't work properly though when component filters are changed,
37
+// because this will cause the Store to dump all roots and re-initialize the tree (recreating the Element objects).
38
+// So instead we key on Element ID (which is stable in this case) and use an LRU for eviction.
39
+const inspectedElementCache: LRUCache<
40
+ number,
41
InspectedElementFrontend,
36
-> = new WeakMap();
42
+> = new LRU({
43
+ max: 25,
44
+});
45
46
type Path = Array<string | number>;
47
74
switch (type) {
75
case 'no-change':
76
// This is a no-op for the purposes of our cache.
69
- inspectedElement = inspectedElementMap.get(element);
77
+ inspectedElement = inspectedElementCache.get(element.id);
78
if (inspectedElement != null) {
79
return [inspectedElement, type];
80
}
73
- break;
81
+
82
+ // We should only encounter this case in the event of a bug.
83
+ throw Error(`Cached data for element "${id}" not found`);
84
85
case 'not-found':
86
// This is effectively a no-op.
87
// If the Element is still in the Store, we can eagerly remove it from the Map.
78
- inspectedElementMap.delete(element);
88
+ inspectedElementCache.remove(element.id);
89
90
throw Error(`Element "${id}" not found`);
91
98
fullData.value,
99
);
100
91
- inspectedElementMap.set(element, inspectedElement);
101
+ inspectedElementCache.set(element.id, inspectedElement);
102
103
return [inspectedElement, type];
104
108
109
// A path has been hydrated.
110
// Merge it with the latest copy we have locally and resolve with the merged value.
101
- inspectedElement = inspectedElementMap.get(element) || null;
111
+ inspectedElement = inspectedElementCache.get(element.id) || null;
112
if (inspectedElement !== null) {
113
// Clone element
114
inspectedElement = {...inspectedElement};
121
hydrateHelper(value, ((path: any): Path)),
122
);
123
114
- inspectedElementMap.set(element, inspectedElement);
124
+ inspectedElementCache.set(element.id, inspectedElement);
125
126
return [inspectedElement, type];
127
}