@samitouri / QOS-React-2 / commits / d87711f8d0

[DevTools] Validate Store operation invariants (#37050)

Builds on #37049 by validating Store operation invariants before mutation. Missing nodes, invalid element types, inconsistent parent-child relationships, and invalid reorder operations now emit and throw explicit errors instead of silently continuing with corrupted state. Adds a canonical-render regression test for invalid child removal.

Ruslan Lesiutin committed Jul 23, 2026 at 10:39 UTC d87711f8d0a45e6950fef15183ad6f9ec827b8b0
2 files changed +272 -118
packages/react-devtools-shared/src/__tests__/store-test.js
+70
@@ -129,6 +129,76 @@ describe('Store', () => {
129 expect(store).toMatchInlineSnapshot(`[root]`);
130 });
131
132 + it('throws when a transition timeline is requested during initial paint', () => {
133 + const errorListener = jest.fn();
134 + store.addListener('error', errorListener);
135 +
136 + expect(() =>
137 + store.getSuspendableDocumentOrderSuspenseTransition(false, 1),
138 + ).toThrow(
139 + 'Cannot get a transition timeline during the initial paint. This is a bug in React DevTools.',
140 + );
141 + expect(errorListener).toHaveBeenCalledTimes(1);
142 +
143 + store.removeListener('error', errorListener);
144 + });
145 +
146 + // @reactVersion >= 18.0
147 + it('throws before removing a node that is not a child of its parent', () => {
148 + function FirstChild() {
149 + return null;
150 + }
151 + function SecondChild() {
152 + return null;
153 + }
154 + function Parent({showFirstChild}) {
155 + return (
156 + <>
157 + {showFirstChild && <FirstChild />}
158 + <SecondChild />
159 + </>
160 + );
161 + }
162 +
163 + act(() => render(<Parent showFirstChild={true} />));
164 +
165 + const parent = store.getElementAtIndex(0);
166 + expect(parent.displayName).toBe('Parent');
167 + const firstChildIndex = parent.children.findIndex(id => {
168 + const child = store.getElementByID(id);
169 + return child !== null && child.displayName === 'FirstChild';
170 + });
171 + expect(firstChildIndex).not.toBe(-1);
172 + const firstChildID = parent.children[firstChildIndex];
173 +
174 + // Corrupt only the frontend relationship. The removal operation below is
175 + // still produced canonically by rendering React.
176 + parent.children.splice(firstChildIndex, 1);
177 +
178 + const errorListener = jest.fn();
179 + store.addListener('error', errorListener);
180 + let caughtError = null;
181 + try {
182 + act(() => render(<Parent showFirstChild={false} />));
183 + } catch (error) {
184 + caughtError = error;
185 + } finally {
186 + // The test Bridge invokes listeners synchronously, so discard the batch
187 + // whose Store listener intentionally threw.
188 + bridge._messageQueue.length = 0;
189 + }
190 +
191 + const expectedMessage =
192 + `Cannot remove node "${firstChildID}" from parent "${parent.id}" ` +
193 + `because it is not a child of the parent.`;
194 + expect(caughtError).toMatchObject({message: expectedMessage});
195 + expect(errorListener).toHaveBeenCalledWith(caughtError);
196 + expect(store.containsElement(firstChildID)).toBe(true);
197 +
198 + parent.children.splice(firstChildIndex, 0, firstChildID);
199 + store.removeListener('error', errorListener);
200 + });
201 +
202 // This test is not the same cause as what's reported on GitHub,
203 // but the resulting behavior (owner mounting after descendant) is the same.
204 // Thec ase below is admittedly contrived and relies on side effects.
packages/react-devtools-shared/src/devtools/store.js
+202 -118
@@ -28,7 +28,20 @@ import {
28 SUSPENSE_TREE_OPERATION_SUSPENDERS,
29 } from '../constants';
30 import {
31 + ElementTypeClass,
32 + ElementTypeContext,
33 + ElementTypeFunction,
34 + ElementTypeForwardRef,
35 + ElementTypeHostComponent,
36 + ElementTypeMemo,
37 + ElementTypeOtherOrUnknown,
38 + ElementTypeProfiler,
39 ElementTypeRoot,
40 + ElementTypeSuspense,
41 + ElementTypeSuspenseList,
42 + ElementTypeTracingMarker,
43 + ElementTypeVirtual,
44 + ElementTypeViewTransition,
45 ElementTypeActivity,
46 ComponentFilterActivitySlice,
47 } from '../frontend/types';
@@ -140,6 +153,32 @@ function isNonZeroRect(rect: Rect) {
153 return rect.width > 0 || rect.height > 0 || rect.x > 0 || rect.y > 0;
154 }
155
156 +function parseElementType(value: number): ElementType | null {
157 + // Cast before switching so Flow checks exhaustiveness while the default rejects unknown bridge values.
158 + const type = value as any as ElementType;
159 + switch (type) {
160 + case ElementTypeClass:
161 + case ElementTypeContext:
162 + case ElementTypeFunction:
163 + case ElementTypeForwardRef:
164 + case ElementTypeHostComponent:
165 + case ElementTypeMemo:
166 + case ElementTypeOtherOrUnknown:
167 + case ElementTypeProfiler:
168 + case ElementTypeRoot:
169 + case ElementTypeSuspense:
170 + case ElementTypeSuspenseList:
171 + case ElementTypeTracingMarker:
172 + case ElementTypeVirtual:
173 + case ElementTypeViewTransition:
174 + case ElementTypeActivity:
175 + return type;
176 + default:
177 + (type) as empty;
178 + return null;
179 + }
180 +}
181 +
182 /**
183 * The store is the single source of truth for updates from the backend.
184 * ContextProviders can subscribe to the Store for specific things they want to provide.
@@ -366,7 +405,7 @@ export default class Store extends EventEmitter<{
405 }
406
407 // This is only used in tests to avoid memory leaks.
369 - assertMapSizeMatchesRootCount(map: Map<any, any>, mapName: string) {
408 + assertMapSizeMatchesRootCount<K, V>(map: Map<K, V>, mapName: string) {
409 const expectedSize = this.roots.length;
410 if (map.size !== expectedSize) {
411 this._throwAndEmitError(
@@ -609,13 +648,11 @@ export default class Store extends EventEmitter<{
648
649 if (root === undefined) {
650 // We should never reach this. This is a bug in the backend renderer.
612 - this._throwAndEmitError(
651 + return this._throwAndEmitError(
652 Error(
653 `Couldn't find root with id "${rootID}": no matching node was found in the Store.`,
654 ),
655 );
617 -
618 - return null;
656 }
657
658 if (root.children.length === 0) {
@@ -630,7 +667,9 @@ export default class Store extends EventEmitter<{
667 }
668
669 if (root === undefined) {
633 - return null;
670 + return this._throwAndEmitError(
671 + Error(`Could not find an element at index "${index}" in the Store.`),
672 + );
673 }
674
675 // Find the element in the tree using the weight of each node...
@@ -640,19 +679,18 @@ export default class Store extends EventEmitter<{
679
680 while (index !== currentWeight) {
681 const numChildren = currentElement.children.length;
682 + let didFindChild = false;
683 for (let i = 0; i < numChildren; i++) {
684 const childID = currentElement.children[i];
685 const child = this._idToElement.get(childID);
686
687 if (child === undefined) {
688 // We should never reach this. This is a bug in the backend renderer.
649 - this._throwAndEmitError(
689 + return this._throwAndEmitError(
690 Error(
691 `Couldn't child element with id "${childID}": no matching node was found in the Store.`,
692 ),
693 );
654 -
655 - return null;
694 }
695
696 const childWeight = child.isCollapsed ? 1 : child.weight;
@@ -660,14 +698,23 @@ export default class Store extends EventEmitter<{
698 if (index <= currentWeight + childWeight) {
699 currentWeight++;
700 currentElement = child;
701 + didFindChild = true;
702 break;
703 } else {
704 currentWeight += childWeight;
705 }
706 }
707 +
708 + if (!didFindChild) {
709 + return this._throwAndEmitError(
710 + Error(
711 + `Could not find an element at index "${index}" because the Store tree weights are invalid.`,
712 + ),
713 + );
714 + }
715 }
716
670 - return currentElement || null;
717 + return currentElement;
718 }
719
720 getElementIDAtIndex(index: number): number | null {
@@ -685,6 +732,26 @@ export default class Store extends EventEmitter<{
732 return element;
733 }
734
735 + _getElementByIDOrThrow(id: Element['id']): Element {
736 + const element = this._idToElement.get(id);
737 + if (element === undefined) {
738 + return this._throwAndEmitError(
739 + Error(
740 + `Could not find element with id "${id}": no matching node was found in the Store.`,
741 + ),
742 + );
743 + }
744 + return element;
745 + }
746 +
747 + _recalculateWeightAcrossRoots(): void {
748 + let weightAcrossRoots = 0;
749 + this._roots.forEach(rootID => {
750 + weightAcrossRoots += this._getElementByIDOrThrow(rootID).weight;
751 + });
752 + this._weightAcrossRoots = weightAcrossRoots;
753 + }
754 +
755 containsSuspense(id: SuspenseNode['id']): boolean {
756 return this._idToSuspense.has(id);
757 }
@@ -893,8 +960,15 @@ export default class Store extends EventEmitter<{
960 let depth = 0;
961 while (parentID > 0) {
962 if (parentID === ownerID || unsortedIDs.has(parentID)) {
896 - // $FlowFixMe[unsafe-addition] addition with possible null/undefined value
897 - depth = depthMap.get(parentID) + 1;
963 + const parentDepth = depthMap.get(parentID);
964 + if (parentDepth === undefined) {
965 + return this._throwAndEmitError(
966 + Error(
967 + `Invalid owners list: owner depth for element "${parentID}" was not found.`,
968 + ),
969 + );
970 + }
971 + depth = parentDepth + 1;
972 depthMap.set(id, depth);
973 break;
974 }
@@ -961,14 +1035,13 @@ export default class Store extends EventEmitter<{
1035 let rootStep: null | SuspenseTimelineStep = null;
1036 for (let i = 0; i < roots.length; i++) {
1037 const rootID = roots[i];
964 - const root = this.getElementByID(rootID);
965 - if (root === null) {
966 - continue;
967 - }
1038 + this._getElementByIDOrThrow(rootID);
1039 const rendererID = this._rootIDToRendererID.get(rootID);
1040 if (rendererID === undefined) {
970 - throw new Error(
971 - 'Failed to find renderer ID for root. This is a bug in React DevTools.',
1041 + return this._throwAndEmitError(
1042 + Error(
1043 + 'Failed to find renderer ID for root. This is a bug in React DevTools.',
1044 + ),
1045 );
1046 }
1047 // TODO: This includes boundaries that can't be suspended due to no support from the renderer.
@@ -1059,9 +1132,12 @@ export default class Store extends EventEmitter<{
1132 ): Array<SuspenseTimelineStep> {
1133 const target: Array<SuspenseTimelineStep> = [];
1134 const focusedTransitionID = this._focusedTransition;
1062 - // $FlowFixMe[invalid-compare]
1063 - if (focusedTransitionID === null) {
1064 - return target;
1135 + if (focusedTransitionID === 0) {
1136 + return this._throwAndEmitError(
1137 + Error(
1138 + 'Cannot get a transition timeline during the initial paint. This is a bug in React DevTools.',
1139 + ),
1140 + );
1141 }
1142
1143 target.push({
@@ -1155,14 +1231,18 @@ export default class Store extends EventEmitter<{
1231 this._focusedTransition,
1232 );
1233 if (focusedTransitionRootID === null) {
1158 - throw new Error(
1159 - 'Failed to find root ID for focused transition. This is a bug in React DevTools.',
1234 + return this._throwAndEmitError(
1235 + Error(
1236 + 'Failed to find root ID for focused transition. This is a bug in React DevTools.',
1237 + ),
1238 );
1239 }
1240 const rendererID = this._rootIDToRendererID.get(focusedTransitionRootID);
1241 if (rendererID === undefined) {
1164 - throw new Error(
1165 - 'Failed to find renderer ID for focused transition root. This is a bug in React DevTools.',
1242 + return this._throwAndEmitError(
1243 + Error(
1244 + 'Failed to find renderer ID for focused transition root. This is a bug in React DevTools.',
1245 + ),
1246 );
1247 }
1248 timeline = this.getSuspendableDocumentOrderSuspenseTransition(
@@ -1313,12 +1393,7 @@ export default class Store extends EventEmitter<{
1393
1394 // Only re-calculate weights and emit an "update" event if the store was mutated.
1395 if (didMutate) {
1316 - let weightAcrossRoots = 0;
1317 - this._roots.forEach(rootID => {
1318 - const {weight} = this.getElementByID(rootID) as any as Element;
1319 - weightAcrossRoots += weight;
1320 - });
1321 - this._weightAcrossRoots = weightAcrossRoots;
1396 + this._recalculateWeightAcrossRoots();
1397
1398 // The Tree context's search reducer expects an explicit list of ids for nodes that were added or removed.
1399 // In this case, we can pass it empty arrays since nodes in a collapsed tree are still there (just hidden).
@@ -1428,13 +1503,22 @@ export default class Store extends EventEmitter<{
1503 switch (operation) {
1504 case TREE_OPERATION_ADD: {
1505 const id = operations[i + 1];
1431 - const type = operations[i + 2] as any as ElementType;
1506 + const rawType = operations[i + 2];
1507 + const type = parseElementType(rawType);
1508 +
1509 + if (type === null) {
1510 + return this._throwAndEmitError(
1511 + Error(
1512 + `Cannot add node "${id}" because "${rawType}" is not a valid element type.`,
1513 + ),
1514 + );
1515 + }
1516
1517 i += 3;
1518
1519 if (this._idToElement.has(id)) {
1520 // We should never reach this. This is a bug in the backend renderer.
1437 - this._throwAndEmitError(
1521 + return this._throwAndEmitError(
1522 Error(
1523 `Cannot add node "${id}" because a node with that id is already in the Store.`,
1524 ),
@@ -1545,13 +1629,11 @@ export default class Store extends EventEmitter<{
1629 const parentElement = this._idToElement.get(parentID);
1630 if (parentElement === undefined) {
1631 // We should never reach this. This is a bug in the backend renderer.
1548 - this._throwAndEmitError(
1632 + return this._throwAndEmitError(
1633 Error(
1634 `Cannot add child "${id}" to parent "${parentID}" because parent node was not found in the Store.`,
1635 ),
1636 );
1553 -
1554 - break;
1637 }
1638
1639 parentElement.children.push(id);
@@ -1622,13 +1704,11 @@ export default class Store extends EventEmitter<{
1704
1705 if (element === undefined) {
1706 // We should never reach this. This is a bug in the backend renderer.
1625 - this._throwAndEmitError(
1707 + return this._throwAndEmitError(
1708 Error(
1709 `Cannot remove node "${id}" because no matching node was found in the Store.`,
1710 ),
1711 );
1630 -
1631 - break;
1712 }
1713
1714 i += 1;
@@ -1636,13 +1716,11 @@ export default class Store extends EventEmitter<{
1716 const {children, ownerID, parentID, weight} = element;
1717 if (children.length > 0) {
1718 // We should never reach this. This is a bug in the backend renderer.
1639 - this._throwAndEmitError(
1719 + return this._throwAndEmitError(
1720 Error(`Node "${id}" was removed before its children.`),
1721 );
1722 }
1723
1644 - this._idToElement.delete(id);
1645 -
1724 let parentElement: ?Element = null;
1725 if (parentID === 0) {
1726 // $FlowFixMe[constant-condition]
@@ -1664,19 +1742,26 @@ export default class Store extends EventEmitter<{
1742 parentElement = this._idToElement.get(parentID);
1743 if (parentElement === undefined) {
1744 // We should never reach this. This is a bug in the backend renderer.
1667 - this._throwAndEmitError(
1745 + return this._throwAndEmitError(
1746 Error(
1747 `Cannot remove node "${id}" from parent "${parentID}" because no matching node was found in the Store.`,
1748 ),
1749 );
1672 -
1673 - break;
1750 }
1751
1752 const index = parentElement.children.indexOf(id);
1753 + if (index === -1) {
1754 + return this._throwAndEmitError(
1755 + Error(
1756 + `Cannot remove node "${id}" from parent "${parentID}" because it is not a child of the parent.`,
1757 + ),
1758 + );
1759 + }
1760 parentElement.children.splice(index, 1);
1761 }
1762
1763 + this._idToElement.delete(id);
1764 +
1765 this._adjustParentTreeWeight(parentElement, -weight);
1766 removedElementIDs.set(id, parentID);
1767
@@ -1704,37 +1789,42 @@ export default class Store extends EventEmitter<{
1789 const element = this._idToElement.get(id);
1790 if (element === undefined) {
1791 // We should never reach this. This is a bug in the backend renderer.
1707 - this._throwAndEmitError(
1792 + return this._throwAndEmitError(
1793 Error(
1794 `Cannot reorder children for node "${id}" because no matching node was found in the Store.`,
1795 ),
1796 );
1712 -
1713 - break;
1797 }
1798
1799 const children = element.children;
1800 if (children.length !== numChildren) {
1801 // We should never reach this. This is a bug in the backend renderer.
1719 - this._throwAndEmitError(
1802 + return this._throwAndEmitError(
1803 Error(
1804 `Children cannot be added or removed during a reorder operation.`,
1805 ),
1806 );
1807 }
1808
1809 + const reorderedChildIDs: Set<Element['id']> = new Set();
1810 for (let j = 0; j < numChildren; j++) {
1811 const childID = operations[i + j];
1728 - children[j] = childID;
1729 - if (__DEV__) {
1730 - // This check is more expensive so it's gated by __DEV__.
1731 - const childElement = this._idToElement.get(childID);
1732 - if (childElement == null || childElement.parentID !== id) {
1733 - console.error(
1812 + const childElement = this._idToElement.get(childID);
1813 + if (
1814 + childElement === undefined ||
1815 + childElement.parentID !== id ||
1816 + reorderedChildIDs.has(childID)
1817 + ) {
1818 + return this._throwAndEmitError(
1819 + Error(
1820 `Children cannot be added or removed during a reorder operation.`,
1735 - );
1736 - }
1821 + ),
1822 + );
1823 }
1824 + reorderedChildIDs.add(childID);
1825 + }
1826 + for (let j = 0; j < numChildren; j++) {
1827 + children[j] = operations[i + j];
1828 }
1829 i += numChildren;
1830
@@ -1829,12 +1919,12 @@ export default class Store extends EventEmitter<{
1919 const parentID = operations[i + 2];
1920 const nameStringID = operations[i + 3];
1921 const isSuspended = operations[i + 4] === 1;
1832 - const numRects = operations[i + 5] as any as number;
1922 + const numRects = operations[i + 5];
1923 let name = stringTable[nameStringID];
1924
1925 if (this._idToSuspense.has(id)) {
1926 // We should never reach this. This is a bug in the backend renderer.
1837 - this._throwAndEmitError(
1927 + return this._throwAndEmitError(
1928 Error(
1929 `Cannot add suspense node "${id}" because a suspense node with that id is already in the Store.`,
1930 ),
@@ -1887,13 +1977,11 @@ export default class Store extends EventEmitter<{
1977 const parentSuspense = this._idToSuspense.get(parentID);
1978 if (parentSuspense === undefined) {
1979 // We should never reach this. This is a bug in the backend renderer.
1890 - this._throwAndEmitError(
1980 + return this._throwAndEmitError(
1981 Error(
1982 `Cannot add suspense child "${id}" to parent suspense "${parentID}" because parent suspense node was not found in the Store.`,
1983 ),
1984 );
1895 -
1896 - break;
1985 }
1986
1987 parentSuspense.children.push(id);
@@ -1924,13 +2012,11 @@ export default class Store extends EventEmitter<{
2012
2013 if (suspense === undefined) {
2014 // We should never reach this. This is a bug in the backend renderer.
1927 - this._throwAndEmitError(
2015 + return this._throwAndEmitError(
2016 Error(
2017 `Cannot remove suspense node "${id}" because no matching node was found in the Store.`,
2018 ),
2019 );
1932 -
1933 - break;
2020 }
2021
2022 i += 1;
@@ -1938,11 +2024,33 @@ export default class Store extends EventEmitter<{
2024 const {children, parentID, rects} = suspense;
2025 if (children.length > 0) {
2026 // We should never reach this. This is a bug in the backend renderer.
1941 - this._throwAndEmitError(
2027 + return this._throwAndEmitError(
2028 Error(`Suspense node "${id}" was removed before its children.`),
2029 );
2030 }
2031
2032 + let parentSuspense: SuspenseNode | null = null;
2033 + let parentIndex = -1;
2034 + if (parentID !== 0) {
2035 + parentSuspense = this._idToSuspense.get(parentID) || null;
2036 + if (parentSuspense === null) {
2037 + return this._throwAndEmitError(
2038 + Error(
2039 + `Cannot remove suspense node "${id}" from parent "${parentID}" because no matching node was found in the Store.`,
2040 + ),
2041 + );
2042 + }
2043 +
2044 + parentIndex = parentSuspense.children.indexOf(id);
2045 + if (parentIndex === -1) {
2046 + return this._throwAndEmitError(
2047 + Error(
2048 + `Cannot remove suspense node "${id}" from parent "${parentID}" because it is not a child of the parent.`,
2049 + ),
2050 + );
2051 + }
2052 + }
2053 +
2054 if (rects !== null && parentID !== 0) {
2055 // Delete all the existing rects from the R-tree
2056 for (let j = 0; j < rects.length; j++) {
@@ -1953,8 +2061,7 @@ export default class Store extends EventEmitter<{
2061 this._idToSuspense.delete(id);
2062 removedSuspenseIDs.set(id, parentID);
2063
1956 - let parentSuspense: ?SuspenseNode = null;
1957 - if (parentID === 0) {
2064 + if (parentSuspense === null) {
2065 // $FlowFixMe[constant-condition]
2066 if (__DEBUG__) {
2067 debug('Suspense remove', `node ${id} root`);
@@ -1965,28 +2072,7 @@ export default class Store extends EventEmitter<{
2072 debug('Suspense Remove', `node ${id} from parent ${parentID}`);
2073 }
2074
1968 - parentSuspense = this._idToSuspense.get(parentID);
1969 - if (parentSuspense === undefined) {
1970 - // We should never reach this. This is a bug in the backend renderer.
1971 - this._throwAndEmitError(
1972 - Error(
1973 - `Cannot remove suspense node "${id}" from parent "${parentID}" because no matching node was found in the Store.`,
1974 - ),
1975 - );
1976 -
1977 - break;
1978 - }
1979 -
1980 - const index = parentSuspense.children.indexOf(id);
1981 - if (index === -1) {
1982 - // We should never reach this. This is a bug in the backend renderer.
1983 - this._throwAndEmitError(
1984 - Error(
1985 - `Cannot remove suspense node "${id}" from parent "${parentID}" because it is not a child of the parent.`,
1986 - ),
1987 - );
1988 - }
1989 - parentSuspense.children.splice(index, 1);
2075 + parentSuspense.children.splice(parentIndex, 1);
2076 }
2077 }
2078
@@ -2001,37 +2087,42 @@ export default class Store extends EventEmitter<{
2087 const suspense = this._idToSuspense.get(id);
2088 if (suspense === undefined) {
2089 // We should never reach this. This is a bug in the backend renderer.
2004 - this._throwAndEmitError(
2090 + return this._throwAndEmitError(
2091 Error(
2092 `Cannot reorder children for suspense node "${id}" because no matching node was found in the Store.`,
2093 ),
2094 );
2009 -
2010 - break;
2095 }
2096
2097 const children = suspense.children;
2098 if (children.length !== numChildren) {
2099 // We should never reach this. This is a bug in the backend renderer.
2016 - this._throwAndEmitError(
2100 + return this._throwAndEmitError(
2101 Error(
2102 `Suspense children cannot be added or removed during a reorder operation.`,
2103 ),
2104 );
2105 }
2106
2107 + const reorderedChildIDs: Set<SuspenseNode['id']> = new Set();
2108 for (let j = 0; j < numChildren; j++) {
2109 const childID = operations[i + j];
2025 - children[j] = childID;
2026 - if (__DEV__) {
2027 - // This check is more expensive so it's gated by __DEV__.
2028 - const childSuspense = this._idToSuspense.get(childID);
2029 - if (childSuspense == null || childSuspense.parentID !== id) {
2030 - console.error(
2110 + const childSuspense = this._idToSuspense.get(childID);
2111 + if (
2112 + childSuspense === undefined ||
2113 + childSuspense.parentID !== id ||
2114 + reorderedChildIDs.has(childID)
2115 + ) {
2116 + return this._throwAndEmitError(
2117 + Error(
2118 `Suspense children cannot be added or removed during a reorder operation.`,
2032 - );
2033 - }
2119 + ),
2120 + );
2121 }
2122 + reorderedChildIDs.add(childID);
2123 + }
2124 + for (let j = 0; j < numChildren; j++) {
2125 + children[j] = operations[i + j];
2126 }
2127 i += numChildren;
2128
@@ -2047,20 +2138,18 @@ export default class Store extends EventEmitter<{
2138 break;
2139 }
2140 case SUSPENSE_TREE_OPERATION_RESIZE: {
2050 - const id = operations[i + 1] as any as number;
2051 - const numRects = operations[i + 2] as any as number;
2141 + const id = operations[i + 1];
2142 + const numRects = operations[i + 2];
2143 i += 3;
2144
2145 const suspense = this._idToSuspense.get(id);
2146 if (suspense === undefined) {
2147 // We should never reach this. This is a bug in the backend renderer.
2057 - this._throwAndEmitError(
2148 + return this._throwAndEmitError(
2149 Error(
2150 `Cannot set rects for suspense node "${id}" because no matching node was found in the Store.`,
2151 ),
2152 );
2062 -
2063 - break;
2153 }
2154
2155 const prevRects = suspense.rects;
@@ -2142,13 +2231,11 @@ export default class Store extends EventEmitter<{
2231
2232 if (suspense === undefined) {
2233 // We should never reach this. This is a bug in the backend renderer.
2145 - this._throwAndEmitError(
2234 + return this._throwAndEmitError(
2235 Error(
2236 `Cannot update suspenders of suspense node "${id}" because no matching node was found in the Store.`,
2237 ),
2238 );
2150 -
2151 - break;
2239 }
2240
2241 // $FlowFixMe[constant-condition]
@@ -2178,7 +2265,7 @@ export default class Store extends EventEmitter<{
2265 break;
2266 }
2267 default:
2181 - this._throwAndEmitError(
2268 + return this._throwAndEmitError(
2269 new UnsupportedBridgeOperationError(
2270 `Unsupported Bridge operation "${operation}"`,
2271 ),
@@ -2283,7 +2370,9 @@ export default class Store extends EventEmitter<{
2370 // the Activities that are descendants of the next Activity slice.
2371 const nextActivitySlice = this._idToElement.get(nextActivitySliceID);
2372 if (nextActivitySlice === undefined) {
2286 - throw new Error('Next Activity slice not found in Store.');
2373 + return this._throwAndEmitError(
2374 + Error('Next Activity slice not found in Store.'),
2375 + );
2376 }
2377
2378 for (let j = 0; j < nextActivitySlice.children.length; j++) {
@@ -2293,12 +2382,7 @@ export default class Store extends EventEmitter<{
2382 }
2383
2384 if (didCollapse) {
2296 - let weightAcrossRoots = 0;
2297 - this._roots.forEach(rootID => {
2298 - const {weight} = this.getElementByID(rootID) as any as Element;
2299 - weightAcrossRoots += weight;
2300 - });
2301 - this._weightAcrossRoots = weightAcrossRoots;
2385 + this._recalculateWeightAcrossRoots();
2386 }
2387 }
2388
@@ -2329,7 +2413,7 @@ export default class Store extends EventEmitter<{
2413 let didMutate = false;
2414 const element = this._idToElement.get(elementID);
2415 if (element === undefined) {
2332 - throw new Error('Element not found in Store.');
2416 + return this._throwAndEmitError(Error('Element not found in Store.'));
2417 }
2418
2419 if (element.type === ElementTypeActivity) {