@samitouri / QOS-React-2 / commits / 8830ba890c

Fix more Suspense traversal bugs

Dan Abramov committed Apr 19, 2019 at 01:09 UTC 8830ba890cdfc27970f1183fec41f8c2025d5977
3 files changed +241 -163
src/backend/renderer.js
+102 -38
@@ -574,13 +574,21 @@ export function attach(
574 }
575
576 let pendingOperations: Array<number> = [];
577 + let pendingRealUnmountedIDs: Array<number> = [];
578 + let pendingSimulatedUnmountedIDs: Array<number> = [];
579 let pendingOperationsQueue: Array<Uint32Array> | null = [];
580
581 + // We keep track of which Fibers have been reported as unmounted by React
582 + // during this commit phase so that we don't try to "hide" them or their
583 + // children when Suspense flips to fallback. These Fibers won't have IDs.
584 + let fibersUnmountedInThisCommitPhase: WeakSet<Fiber> = new WeakSet();
585 +
586 + // TODO: we could make this layer DEV-only and write directly to pendingOperations.
587 let nextOperation: Array<number> = [];
588 function beginNextOperation(size: number): void {
589 nextOperation.length = size;
590 }
583 - function endNextOperation(addToStartOfQueue: boolean): void {
591 + function endNextOperation(): void {
592 if (__DEV__) {
593 for (let i = 0; i < nextOperation.length; i++) {
594 if (!Number.isInteger(nextOperation[i])) {
@@ -591,20 +599,16 @@ export function attach(
599 }
600 }
601 }
594 -
595 - if (addToStartOfQueue) {
596 - pendingOperations.splice.apply(
597 - pendingOperations,
598 - [0, 0].concat(nextOperation)
599 - );
600 - } else {
601 - pendingOperations.push.apply(pendingOperations, nextOperation);
602 - }
602 + pendingOperations.push.apply(pendingOperations, nextOperation);
603 nextOperation.length = 0;
604 }
605
606 function flushPendingEvents(root: Object): void {
607 - if (pendingOperations.length === 0) {
607 + if (
608 + pendingOperations.length === 0 &&
609 + pendingRealUnmountedIDs.length === 0 &&
610 + pendingSimulatedUnmountedIDs.length === 0
611 + ) {
612 // If we're currently profiling, send an "operations" method even if there are no mutations to the tree.
613 // The frontend needs this no-op info to know how to reconstruct the tree for each commit,
614 // even if a particular commit didn't change the shape of the tree.
@@ -613,18 +617,48 @@ export function attach(
617 }
618 }
619
620 + const ops = new Uint32Array(
621 + // Identify which renderer this update is coming from.
622 + 2 + // [rendererID, rootFiberID]
623 + // All unmounts are batched in a single message.
624 + 2 + // [TREE_OPERATION_REMOVE, removedIDLength]
625 + pendingRealUnmountedIDs.length +
626 + pendingSimulatedUnmountedIDs.length +
627 + // Regular operations
628 + pendingOperations.length
629 + );
630 +
631 // Identify which renderer this update is coming from.
632 // This enables roots to be mapped to renderers,
633 // Which in turn enables fiber props, states, and hooks to be inspected.
619 - beginNextOperation(2);
620 - nextOperation[0] = rendererID;
621 - nextOperation[1] = getFiberID(getPrimaryFiber(root.current));
622 - endNextOperation(true);
634 + let i = 0;
635 + ops[i++] = rendererID;
636 + ops[i++] = getFiberID(getPrimaryFiber(root.current));
637 +
638 + // All unmounts except roots are batched in a single message.
639 + ops[i++] = TREE_OPERATION_REMOVE;
640 + // The first number is how many unmounted IDs we're gonna send.
641 + ops[i++] =
642 + pendingRealUnmountedIDs.length + pendingSimulatedUnmountedIDs.length;
643 + // Fill in the real unmounts in the reverse order.
644 + // They were inserted parents-first by React, but we want children-first.
645 + // So we traverse our array backwards.
646 + for (let j = pendingRealUnmountedIDs.length - 1; j >= 0; j--) {
647 + ops[i++] = pendingRealUnmountedIDs[j];
648 + }
649 + // Fill in the simulated unmounts (hidden Suspense subtrees) in their order.
650 + // (We want children to go before parents.)
651 + // They go *after* the real unmounts because we know for sure they won't be
652 + // children of already pushed "real" IDs. If they were, we wouldn't be able
653 + // to discover them during the traversal, as they would have been deleted.
654 + ops.set(pendingSimulatedUnmountedIDs, i);
655 + i += pendingSimulatedUnmountedIDs.length;
656 + // Fill in the rest of the operations.
657 + ops.set(pendingOperations, i);
658
659 // Let the frontend know about tree operations.
660 // The first value in this array will identify which root it corresponds to,
661 // so we do no longer need to dispatch a separate root-committed event.
627 - const ops = Uint32Array.from(pendingOperations);
662 if (pendingOperationsQueue !== null) {
663 // Until the frontend has been connected, store the tree operations.
664 // This will let us avoid walking the tree later when the frontend connects,
@@ -635,7 +669,10 @@ export function attach(
669 hook.emit('operations', ops);
670 }
671
638 - pendingOperations = [];
672 + pendingOperations.length = 0;
673 + pendingRealUnmountedIDs.length = 0;
674 + pendingSimulatedUnmountedIDs.length = 0;
675 + fibersUnmountedInThisCommitPhase = new WeakSet();
676 }
677
678 function recordMount(fiber: Fiber, parentFiber: Fiber | null) {
@@ -657,7 +694,7 @@ export function attach(
694 nextOperation[2] = ElementTypeRoot;
695 nextOperation[3] = isProfilingSupported ? 1 : 0;
696 nextOperation[4] = hasOwnerMetadata ? 1 : 0;
660 - endNextOperation(false);
697 + endNextOperation();
698 } else {
699 const { displayName, key, type } = getDataForFiber(fiber);
700 const { _debugOwner } = fiber;
@@ -702,7 +739,7 @@ export function attach(
739 nextOperation[6 + encodedDisplayNameSize + 1 + i] = encodedKey[i];
740 }
741 }
705 - endNextOperation(false);
742 + endNextOperation();
743 }
744
745 if (isProfiling) {
@@ -714,7 +751,7 @@ export function attach(
751 nextOperation[0] = TREE_OPERATION_UPDATE_TREE_BASE_DURATION;
752 nextOperation[1] = id;
753 nextOperation[2] = treeBaseDuration;
717 - endNextOperation(false);
754 + endNextOperation();
755
756 const { actualDuration } = fiber;
757 if (actualDuration > 0) {
@@ -729,7 +766,7 @@ export function attach(
766 }
767 }
768
732 - function recordUnmount(fiber: Fiber) {
769 + function recordUnmount(fiber: Fiber, isSimulated: boolean) {
770 const isRoot = fiber.tag === HostRoot;
771 const primaryFiber = getPrimaryFiber(fiber);
772 if (!fiberToIDMap.has(primaryFiber)) {
@@ -744,19 +781,22 @@ export function attach(
781 }
782 const id = getFiberID(primaryFiber);
783 if (isRoot) {
747 - beginNextOperation(2);
784 + // Removing a root needs to happen at the end
785 + // so we don't batch it with other unmounts.
786 + beginNextOperation(3);
787 nextOperation[0] = TREE_OPERATION_REMOVE;
749 - nextOperation[1] = id;
750 - endNextOperation(false);
788 + nextOperation[1] = 1; // Remove one item
789 + nextOperation[2] = id;
790 + endNextOperation();
791 } else if (!shouldFilterFiber(fiber)) {
752 - beginNextOperation(2);
753 - nextOperation[0] = TREE_OPERATION_REMOVE;
754 - nextOperation[1] = id;
755 - // Non-root fibers are deleted during the commit phase.
756 - // They are deleted in the parent-first order. However
757 - // DevTools currently expects deletions to be child-first.
758 - // This is why we prepend the delete operation to the queue.
759 - endNextOperation(true);
792 + // To maintain child-first ordering,
793 + // we'll push it into one of these queues,
794 + // and later arrange them in the correct order.
795 + if (isSimulated) {
796 + pendingSimulatedUnmountedIDs.push(id);
797 + } else {
798 + pendingRealUnmountedIDs.push(id);
799 + }
800 }
801 fiberToIDMap.delete(primaryFiber);
802 idToFiberMap.delete(id);
@@ -816,14 +856,35 @@ export function attach(
856 }
857 }
858
859 + // We use this to simulate unmounting for Suspense trees
860 + // when we switch from primary to fallback.
861 function unmountFiberChildrenRecursively(fiber: Fiber) {
862 if (__DEBUG__) {
863 debug('unmountFiberChildrenRecursively()', fiber);
864 }
865 +
866 + // We might meet a nested Suspense on our way.
867 + const isTimedOutSuspense =
868 + fiber.tag === ReactTypeOfWork.SuspenseComponent &&
869 + fiber.memoizedState !== null;
870 +
871 let child = fiber.child;
872 + if (isTimedOutSuspense) {
873 + // If it's showing fallback tree, let's traverse it instead.
874 + const primaryChildFragment = fiber.child;
875 + const fallbackChildFragment = primaryChildFragment.sibling;
876 + // Skip over to the real Fiber child.
877 + child = fallbackChildFragment.child;
878 + }
879 +
880 while (child !== null) {
825 - recordUnmount(child);
826 - unmountFiberChildrenRecursively(child);
881 + // Record simulated unmounts children-first.
882 + // We might find real committed unmounts along the way--skip them.
883 + // Otherwise we would send duplicated messages for the same IDs.
884 + if (!fibersUnmountedInThisCommitPhase.has(child)) {
885 + unmountFiberChildrenRecursively(child);
886 + recordUnmount(child, true);
887 + }
888 child = child.sibling;
889 }
890 }
@@ -844,7 +905,7 @@ export function attach(
905 nextOperation[0] = TREE_OPERATION_UPDATE_TREE_BASE_DURATION;
906 nextOperation[1] = getFiberID(getPrimaryFiber(fiber));
907 nextOperation[2] = treeBaseDuration;
847 - endNextOperation(false);
908 + endNextOperation();
909 }
910
911 if (haveProfilerTimesChanged(fiber.alternate, fiber)) {
@@ -883,7 +944,7 @@ export function attach(
944 for (let i = 0; i < nextChildren.length; i++) {
945 nextOperation[3 + i] = nextChildren[i];
946 }
886 - endNextOperation(false);
947 + endNextOperation();
948 }
949
950 function findReorderedChildrenRecursively(
@@ -1091,10 +1152,13 @@ export function attach(
1152 }
1153
1154 function handleCommitFiberUnmount(fiber) {
1155 + // Remeber this is a real deletion so we don't
1156 + // go down this tree when hiding Suspense nodes.
1157 + fibersUnmountedInThisCommitPhase.add(fiber);
1158 // This is not recursive.
1159 // We can't traverse fibers after unmounting so instead
1160 // we rely on React telling us about each unmount.
1097 - recordUnmount(fiber);
1161 + recordUnmount(fiber, false);
1162 }
1163
1164 function handleCommitFiberRoot(root) {
@@ -1134,7 +1198,7 @@ export function attach(
1198 updateFiberRecursively(current, alternate, null);
1199 } else if (wasMounted && !isMounted) {
1200 // Unmount an existing root.
1137 - recordUnmount(current);
1201 + recordUnmount(current, false);
1202 }
1203 } else {
1204 // Mount a new root.
src/devtools/store.js
+98 -86
@@ -543,6 +543,33 @@ export default class Store extends EventEmitter {
543 }
544 };
545
546 + _adjustParentTreeWeight = (
547 + parentElement: Element | null,
548 + weightDelta: number
549 + ) => {
550 + let isInsideCollapsedSubTree = false;
551 +
552 + while (parentElement != null) {
553 + parentElement.weight += weightDelta;
554 +
555 + // Additions and deletions within a collapsed subtree should not bubble beyond the collapsed parent.
556 + // Their weight will bubble up when the parent is expanded.
557 + if (parentElement.isCollapsed) {
558 + isInsideCollapsedSubTree = true;
559 + break;
560 + }
561 +
562 + parentElement = ((this._idToElement.get(
563 + parentElement.parentID
564 + ): any): Element);
565 + }
566 +
567 + // Additions and deletions within a collapsed subtree should not affect the overall number of elements.
568 + if (!isInsideCollapsedSubTree) {
569 + this._weightAcrossRoots += weightDelta;
570 + }
571 + };
572 +
573 onBridgeOperations = (operations: Uint32Array) => {
574 if (!(operations instanceof Uint32Array)) {
575 // $FlowFixMe TODO HACK Temporary workaround for the fact that Chrome is not transferring the typed array.
@@ -579,20 +606,11 @@ export default class Store extends EventEmitter {
606
607 let i = 2;
608 while (i < operations.length) {
582 - let id: number = ((null: any): number);
583 - let element: Element = ((null: any): Element);
584 - let ownerID: number = 0;
585 - let parentID: number = ((null: any): number);
586 - let parentElement: Element = ((null: any): Element);
587 - let type: ElementType = ((null: any): ElementType);
588 - let weightDelta: number = 0;
589 -
609 const operation = operations[i];
591 -
610 switch (operation) {
593 - case TREE_OPERATION_ADD:
594 - id = ((operations[i + 1]: any): number);
595 - type = ((operations[i + 2]: any): ElementType);
611 + case TREE_OPERATION_ADD: {
612 + const id = ((operations[i + 1]: any): number);
613 + const type = ((operations[i + 2]: any): ElementType);
614
615 i = i + 3;
616
@@ -602,6 +620,8 @@ export default class Store extends EventEmitter {
620 );
621 }
622
623 + let ownerID: number = 0;
624 + let parentID: number = ((null: any): number);
625 if (type === ElementTypeRoot) {
626 if (__DEBUG__) {
627 debug('Add', `new root node ${id}`);
@@ -672,7 +692,9 @@ export default class Store extends EventEmitter {
692 );
693 }
694
675 - parentElement = ((this._idToElement.get(parentID): any): Element);
695 + const parentElement = ((this._idToElement.get(
696 + parentID
697 + ): any): Element);
698 parentElement.children = parentElement.children.concat(id);
699
700 const element: Element = {
@@ -690,74 +712,83 @@ export default class Store extends EventEmitter {
712
713 this._idToElement.set(id, element);
714
715 + // TODO: don't recreate this on every iteration.
716 const oldAddedElementIDs = addedElementIDs;
717 addedElementIDs = new Uint32Array(addedElementIDs.length + 1);
718 addedElementIDs.set(oldAddedElementIDs);
719 addedElementIDs[oldAddedElementIDs.length] = id;
720
698 - weightDelta = 1;
721 + this._adjustParentTreeWeight(parentElement, 1);
722 }
723 break;
724 + }
725 case TREE_OPERATION_REMOVE: {
702 - id = ((operations[i + 1]: any): number);
703 -
704 - if (!this._idToElement.has(id)) {
705 - throw Error(
706 - `Cannot remove node ${id} because no matching node was found in the Store.`
707 - );
708 - }
709 -
726 + const removeLength = ((operations[i + 1]: any): number);
727 i = i + 2;
728
712 - element = ((this._idToElement.get(id): any): Element);
713 - parentID = element.parentID;
714 - weightDelta = -element.weight;
715 -
716 - if (element.children.length > 0) {
717 - throw new Error(
718 - 'Fiber ' +
719 - id +
720 - ' was removed before its children. ' +
721 - 'This is a bug in React DevTools.'
722 - );
723 - }
724 -
725 - this._idToElement.delete(id);
729 + for (let removeIndex = 0; removeIndex < removeLength; removeIndex++) {
730 + const id = ((operations[i]: any): number);
731
727 - if (parentID === 0) {
728 - if (__DEBUG__) {
729 - debug('Remove', `node ${id} root`);
732 + if (!this._idToElement.has(id)) {
733 + throw Error(
734 + `Cannot remove node ${id} because no matching node was found in the Store.`
735 + );
736 }
737
732 - this._roots = this._roots.filter(rootID => rootID !== id);
733 - this._rootIDToRendererID.delete(id);
734 - this._rootIDToCapabilities.delete(id);
738 + i = i + 1;
739
736 - haveRootsChanged = true;
737 - } else {
738 - if (__DEBUG__) {
739 - debug('Remove', `node ${id} from parent ${parentID}`);
740 + const element = ((this._idToElement.get(id): any): Element);
741 + if (element.children.length > 0) {
742 + throw new Error(
743 + 'Fiber ' +
744 + id +
745 + ' was removed before its children. ' +
746 + 'This is a bug in React DevTools.'
747 + );
748 }
741 - parentElement = ((this._idToElement.get(parentID): any): Element);
742 - if (parentElement === undefined) {
743 - throw Error(
744 - `Cannot remove node ${id} from parent ${parentID} because no matching node was found in the Store.`
749 +
750 + this._idToElement.delete(id);
751 +
752 + const parentID = element.parentID;
753 + let parentElement = null;
754 + if (parentID === 0) {
755 + if (__DEBUG__) {
756 + debug('Remove', `fiber ${id} root`);
757 + }
758 +
759 + this._roots = this._roots.filter(rootID => rootID !== id);
760 + this._rootIDToRendererID.delete(id);
761 + this._rootIDToCapabilities.delete(id);
762 +
763 + haveRootsChanged = true;
764 + } else {
765 + if (__DEBUG__) {
766 + debug('Remove', `fiber ${id} from parent ${parentID}`);
767 + }
768 + parentElement = ((this._idToElement.get(parentID): any): Element);
769 + if (parentElement === undefined) {
770 + throw Error(
771 + `Cannot remove node ${id} from parent ${parentID} because no matching node was found in the Store.`
772 + );
773 + }
774 + parentElement.children = parentElement.children.filter(
775 + childID => childID !== id
776 );
777 }
747 - parentElement.children = parentElement.children.filter(
748 - childID => childID !== id
749 - );
750 - }
778
752 - // Track removed items so search results can be updated
753 - const oldRemovedElementIDs = removedElementIDs;
754 - removedElementIDs = new Uint32Array(removedElementIDs.length + 1);
755 - removedElementIDs.set(oldRemovedElementIDs);
756 - removedElementIDs[oldRemovedElementIDs.length] = id;
779 + this._adjustParentTreeWeight(parentElement, -element.weight);
780 +
781 + // Track removed items so search results can be updated
782 + // TODO: no need to recreate this in a loop.
783 + const oldRemovedElementIDs = removedElementIDs;
784 + removedElementIDs = new Uint32Array(removedElementIDs.length + 1);
785 + removedElementIDs.set(oldRemovedElementIDs);
786 + removedElementIDs[oldRemovedElementIDs.length] = id;
787 + }
788 break;
789 }
759 - case TREE_OPERATION_RESET_CHILDREN:
760 - id = ((operations[i + 1]: any): number);
790 + case TREE_OPERATION_RESET_CHILDREN: {
791 + const id = ((operations[i + 1]: any): number);
792 const numChildren = ((operations[i + 2]: any): number);
793 const children = ((operations.slice(
794 i + 3,
@@ -776,7 +807,7 @@ export default class Store extends EventEmitter {
807 );
808 }
809
779 - element = ((this._idToElement.get(id): any): Element);
810 + const element = ((this._idToElement.get(id): any): Element);
811 const prevChildren = element.children;
812 element.children = Array.from(children);
813 if (element.children.length !== prevChildren.length) {
@@ -799,10 +830,13 @@ export default class Store extends EventEmitter {
830 });
831
832 element.weight = nextWeight;
802 -
803 - weightDelta = nextWeight - prevWeight;
833 + // TODO: passing null here is suspicious, but it's existing behavior.
834 + // It is suspicious because either we shouldn't need a delta at all--
835 + // or we should apply it to all parents, and not just this item.
836 + this._adjustParentTreeWeight(null, nextWeight - prevWeight);
837 }
838 break;
839 + }
840 case TREE_OPERATION_UPDATE_TREE_BASE_DURATION:
841 // Base duration updates are only sent while profiling is in progress.
842 // We can ignore them at this point.
@@ -812,28 +846,6 @@ export default class Store extends EventEmitter {
846 default:
847 throw Error(`Unsupported Bridge operation ${operation}`);
848 }
815 -
816 - let isInsideCollapsedSubTree = false;
817 -
818 - while (parentElement != null) {
819 - parentElement.weight += weightDelta;
820 -
821 - // Additions and deletions within a collapsed subtree should not bubble beyond the collapsed parent.
822 - // Their weight will bubble up when the parent is expanded.
823 - if (parentElement.isCollapsed) {
824 - isInsideCollapsedSubTree = true;
825 - break;
826 - }
827 -
828 - parentElement = ((this._idToElement.get(
829 - parentElement.parentID
830 - ): any): Element);
831 - }
832 -
833 - // Additions and deletions within a collapsed subtree should not affect the overall number of elements.
834 - if (!isInsideCollapsedSubTree) {
835 - this._weightAcrossRoots += weightDelta;
836 - }
849 }
850
851 this._revision++;
src/devtools/views/Profiler/CommitTreeBuilder.js
+41 -39
@@ -168,18 +168,12 @@ function updateTree(
168
169 let i = 2;
170 while (i < operations.length) {
171 - let id: number = ((null: any): number);
172 - let node: Node = ((null: any): Node);
173 - let parentID: number = ((null: any): number);
174 - let parentNode: Node = ((null: any): Node);
175 - let type: ElementType = ((null: any): ElementType);
176 -
171 const operation = operations[i];
172
173 switch (operation) {
174 case TREE_OPERATION_ADD:
181 - id = ((operations[i + 1]: any): number);
182 - type = ((operations[i + 2]: any): ElementType);
175 + const id = ((operations[i + 1]: any): number);
176 + const type = ((operations[i + 2]: any): ElementType);
177
178 i = i + 3;
179
@@ -210,7 +204,7 @@ function updateTree(
204
205 nodes.set(id, node);
206 } else {
213 - parentID = ((operations[i]: any): number);
207 + const parentID = ((operations[i]: any): number);
208 i++;
209
210 i++; // ownerID
@@ -240,7 +234,7 @@ function updateTree(
234 );
235 }
236
243 - parentNode = getClonedNode(parentID);
237 + const parentNode = getClonedNode(parentID);
238 parentNode.children = parentNode.children.concat(id);
239
240 const node: Node = {
@@ -254,40 +248,46 @@ function updateTree(
248
249 nodes.set(id, node);
250 }
257 - break;
258 - case TREE_OPERATION_REMOVE:
259 - id = ((operations[i + 1]: any): number);
251
252 + break;
253 + case TREE_OPERATION_REMOVE: {
254 + const removeLength = ((operations[i + 1]: any): number);
255 i = i + 2;
256
263 - if (!nodes.has(id)) {
264 - throw new Error(
265 - 'Commit tree does not contain fiber ' +
266 - id +
267 - '. This is a bug in React DevTools.'
268 - );
269 - }
257 + for (let removeIndex = 0; removeIndex < removeLength; removeIndex++) {
258 + const id = ((operations[i]: any): number);
259 + i = i + 1;
260 +
261 + if (!nodes.has(id)) {
262 + throw new Error(
263 + 'Commit tree does not contain fiber ' +
264 + id +
265 + '. This is a bug in React DevTools.'
266 + );
267 + }
268
271 - node = getClonedNode(id);
272 - parentID = node.parentID;
269 + const node = getClonedNode(id);
270 + const parentID = node.parentID;
271
274 - nodes.delete(id);
272 + nodes.delete(id);
273
276 - parentNode = getClonedNode(parentID);
277 - if (parentNode == null) {
278 - // No-op
279 - } else {
280 - if (__DEBUG__) {
281 - debug('Remove', `fiber ${id} from parent ${parentID}`);
282 - }
274 + const parentNode = getClonedNode(parentID);
275 + if (parentNode == null) {
276 + // No-op
277 + } else {
278 + if (__DEBUG__) {
279 + debug('Remove', `fiber ${id} from parent ${parentID}`);
280 + }
281
284 - parentNode.children = parentNode.children.filter(
285 - childID => childID !== id
286 - );
282 + parentNode.children = parentNode.children.filter(
283 + childID => childID !== id
284 + );
285 + }
286 }
287 break;
289 - case TREE_OPERATION_RESET_CHILDREN:
290 - id = ((operations[i + 1]: any): number);
288 + }
289 + case TREE_OPERATION_RESET_CHILDREN: {
290 + const id = ((operations[i + 1]: any): number);
291 const numChildren = ((operations[i + 2]: any): number);
292 const children = ((operations.slice(
293 i + 3,
@@ -300,14 +300,15 @@ function updateTree(
300 debug('Re-order', `fiber ${id} children ${children.join(',')}`);
301 }
302
303 - node = getClonedNode(id);
303 + const node = getClonedNode(id);
304 node.children = Array.from(children);
305
306 break;
307 - case TREE_OPERATION_UPDATE_TREE_BASE_DURATION:
308 - id = operations[i + 1];
307 + }
308 + case TREE_OPERATION_UPDATE_TREE_BASE_DURATION: {
309 + const id = operations[i + 1];
310
310 - node = getClonedNode(id);
311 + const node = getClonedNode(id);
312 node.treeBaseDuration = operations[i + 2] / 1000; // Convert microseconds back to milliseconds;
313
314 if (__DEBUG__) {
@@ -319,6 +320,7 @@ function updateTree(
320
321 i = i + 3;
322 break;
323 + }
324 default:
325 throw Error(`Unsupported Bridge operation ${operation}`);
326 }