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
}