@@ -20,6 +20,9 @@ import {
2020 TREE_OPERATION_SET_SUBTREE_MODE ,
2121 TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS ,
2222 TREE_OPERATION_UPDATE_TREE_BASE_DURATION ,
23+ SUSPENSE_TREE_OPERATION_ADD ,
24+ SUSPENSE_TREE_OPERATION_REMOVE ,
25+ SUSPENSE_TREE_OPERATION_REORDER_CHILDREN ,
2326} from '../constants' ;
2427import { ElementTypeRoot } from '../frontend/types' ;
2528import {
@@ -44,6 +47,7 @@ import type {
4447 Element ,
4548 ComponentFilter ,
4649 ElementType ,
50+ SuspenseNode ,
4751} from 'react-devtools-shared/src/frontend/types' ;
4852import type {
4953 FrontendBridge ,
@@ -100,11 +104,12 @@ export default class Store extends EventEmitter<{
100104 hookSettings : [ $ReadOnly < DevToolsHookSettings > ] ,
101105 hostInstanceSelected : [ Element [ 'id' ] ] ,
102106 settingsUpdated : [ $ReadOnly < DevToolsHookSettings > ] ,
103- mutated : [ [ Array < number > , Map < number , number > ] ] ,
107+ mutated : [ [ Array < Element [ 'id' ] > , Map < Element [ 'id' ] , Element [ 'id' ] > ] ] ,
104108 recordChangeDescriptions : [ ] ,
105109 roots : [ ] ,
106110 rootSupportsBasicProfiling : [ ] ,
107111 rootSupportsTimelineProfiling : [ ] ,
112+ suspenseTreeMutated : [ ] ,
108113 supportsNativeStyleEditor : [ ] ,
109114 supportsReloadAndProfile : [ ] ,
110115 unsupportedBridgeProtocolDetected : [ ] ,
@@ -127,16 +132,20 @@ export default class Store extends EventEmitter<{
127132 _componentFilters : Array < ComponentFilter > ;
128133
129134 // Map of ID to number of recorded error and warning message IDs.
130- _errorsAndWarnings : Map < number , { errorCount : number , warningCount : number } > =
131- new Map ( ) ;
135+ _errorsAndWarnings : Map <
136+ Element [ 'id' ] ,
137+ { errorCount : number , warningCount : number } ,
138+ > = new Map ( ) ;
132139
133140 // At least one of the injected renderers contains (DEV only) owner metadata.
134141 _hasOwnerMetadata : boolean = false ;
135142
136143 // Map of ID to (mutable) Element.
137144 // Elements are mutated to avoid excessive cloning during tree updates.
138145 // The InspectedElement Suspense cache also relies on this mutability for its WeakMap usage.
139- _idToElement : Map < number , Element > = new Map ( ) ;
146+ _idToElement : Map < Element [ 'id' ] , Element > = new Map ( ) ;
147+
148+ _idToSuspense : Map < SuspenseNode [ 'id' ] , SuspenseNode > = new Map ( ) ;
140149
141150 // Should the React Native style editor panel be shown?
142151 _isNativeStyleEditorSupported : boolean = false ;
@@ -149,7 +158,7 @@ export default class Store extends EventEmitter<{
149158
150159 // Map of element (id) to the set of elements (ids) it owns.
151160 // This map enables getOwnersListForElement() to avoid traversing the entire tree.
152- _ownersMap : Map < number , Set < number >> = new Map ( ) ;
161+ _ownersMap : Map < Element [ 'id' ] , Set < Element [ 'id' ] >> = new Map ( ) ;
153162
154163 _profilerStore : ProfilerStore ;
155164
@@ -158,15 +167,16 @@ export default class Store extends EventEmitter<{
158167 // Incremented each time the store is mutated.
159168 // This enables a passive effect to detect a mutation between render and commit phase.
160169 _revision : number = 0 ;
170+ _revisionSuspense : number = 0 ;
161171
162172 // This Array must be treated as immutable!
163173 // Passive effects will check it for changes between render and mount.
164- _roots : $ReadOnlyArray < number > = [ ] ;
174+ _roots : $ReadOnlyArray < Element [ 'id' ] > = [ ] ;
165175
166- _rootIDToCapabilities : Map < number , Capabilities > = new Map ( ) ;
176+ _rootIDToCapabilities : Map < Element [ 'id' ] , Capabilities > = new Map ( ) ;
167177
168178 // Renderer ID is needed to support inspection fiber props, state, and hooks.
169- _rootIDToRendererID : Map < number , number > = new Map ( ) ;
179+ _rootIDToRendererID : Map < Element [ 'id' ] , number > = new Map ( ) ;
170180
171181 // These options may be initially set by a configuration option when constructing the Store.
172182 _supportsInspectMatchingDOMElement : boolean = false ;
@@ -439,6 +449,9 @@ export default class Store extends EventEmitter<{
439449 get revision ( ) : number {
440450 return this . _revision ;
441451 }
452+ get revisionSuspense ( ) : number {
453+ return this . _revisionSuspense ;
454+ }
442455
443456 get rootIDToRendererID ( ) : Map < number , number > {
444457 return this . _rootIDToRendererID;
@@ -595,6 +608,16 @@ export default class Store extends EventEmitter<{
595608 return element ;
596609 }
597610
611+ getSuspenseByID ( id : SuspenseNode [ 'id' ] ) : SuspenseNode | null {
612+ const suspense = this . _idToSuspense . get ( id ) ;
613+ if ( suspense === undefined ) {
614+ console . warn ( `No suspense found with id "${ id } "` ) ;
615+ return null ;
616+ }
617+
618+ return suspense ;
619+ }
620+
598621 // Returns a tuple of [id, index]
599622 getElementsWithErrorsAndWarnings ( ) : ErrorAndWarningTuples {
600623 if ( ! this . _shouldShowWarningsAndErrors ) {
@@ -989,6 +1012,7 @@ export default class Store extends EventEmitter<{
9891012
9901013 let haveRootsChanged = false ;
9911014 let haveErrorsOrWarningsChanged = false ;
1015+ let hasSuspenseTreeChanged = false ;
9921016
9931017 // The first two values are always rendererID and rootID
9941018 const rendererID = operations [ 0 ] ;
@@ -1369,7 +1393,7 @@ export default class Store extends EventEmitter<{
13691393 // The profiler UI uses them lazily in order to generate the tree.
13701394 i += 3 ;
13711395 break ;
1372- case TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS :
1396+ case TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS : {
13731397 const id = operations [ i + 1 ] ;
13741398 const errorCount = operations [ i + 2 ] ;
13751399 const warningCount = operations [ i + 3 ] ;
@@ -1383,6 +1407,184 @@ export default class Store extends EventEmitter<{
13831407 }
13841408 haveErrorsOrWarningsChanged = true ;
13851409 break ;
1410+ }
1411+ case SUSPENSE_TREE_OPERATION_ADD : {
1412+ const id = operations [ i + 1 ] ;
1413+ const parentID = operations [ i + 2 ] ;
1414+ const nameStringID = operations [ i + 3 ] ;
1415+ let name = stringTable [ nameStringID ] ;
1416+
1417+ if ( this . _idToSuspense . has ( id ) ) {
1418+ this . _throwAndEmitError (
1419+ Error (
1420+ `Cannot add suspense node "${ id } " because a suspense node with that id is already in the Store.` ,
1421+ ) ,
1422+ ) ;
1423+ }
1424+
1425+ const element = this . _idToElement . get ( id ) ;
1426+ if ( element === undefined ) {
1427+ this . _throwAndEmitError (
1428+ Error (
1429+ `Cannot add suspense node "${ id } " because no matching element was found in the Store.` ,
1430+ ) ,
1431+ ) ;
1432+ } else {
1433+ if ( name === null ) {
1434+ // The boundary isn't explicitly named.
1435+ // Pick a sensible default.
1436+ // TODO: Use key
1437+ const owner = this . _idToElement . get ( element . ownerID ) ;
1438+ if ( owner !== undefined ) {
1439+ // TODO: This is clowny
1440+ name = `${ owner . displayName || 'Unknown' } >?` ;
1441+ }
1442+ }
1443+ }
1444+
1445+ if ( __DEBUG__ ) {
1446+ debug ( 'Suspense Add' , `node ${ id } as child of ${ parentID } ` ) ;
1447+ }
1448+
1449+ if ( parentID !== 0 ) {
1450+ const parentSuspense = this . _idToSuspense . get ( parentID ) ;
1451+ if ( parentSuspense === undefined ) {
1452+ this . _throwAndEmitError (
1453+ Error (
1454+ `Cannot add suspense child "${ id } " to parent suspense "${ parentID } " because parent suspense node was not found in the Store.` ,
1455+ ) ,
1456+ ) ;
1457+
1458+ break ;
1459+ }
1460+
1461+ parentSuspense . children . push ( id ) ;
1462+ }
1463+
1464+ if ( name === null ) {
1465+ name = 'Unknown' ;
1466+ }
1467+
1468+ this . _idToSuspense . set ( id , {
1469+ id,
1470+ parentID,
1471+ children : [ ] ,
1472+ name,
1473+ } ) ;
1474+
1475+ i += 4 ;
1476+
1477+ hasSuspenseTreeChanged = true ;
1478+ break ;
1479+ }
1480+ case SUSPENSE_TREE_OPERATION_REMOVE : {
1481+ const removeLength = operations [ i + 1 ] ;
1482+ i += 2 ;
1483+
1484+ for ( let removeIndex = 0 ; removeIndex < removeLength ; removeIndex ++ ) {
1485+ const id = operations [ i ] ;
1486+ const suspense = this . _idToSuspense . get ( id ) ;
1487+
1488+ if ( suspense === undefined ) {
1489+ this . _throwAndEmitError (
1490+ Error (
1491+ `Cannot remove suspense node "${ id } " because no matching node was found in the Store.` ,
1492+ ) ,
1493+ ) ;
1494+
1495+ break ;
1496+ }
1497+
1498+ i += 1 ;
1499+
1500+ const { children, parentID} = suspense ;
1501+ if ( children . length > 0 ) {
1502+ this . _throwAndEmitError (
1503+ Error ( `Suspense node "${ id } " was removed before its children.` ) ,
1504+ ) ;
1505+ }
1506+
1507+ this . _idToSuspense . delete ( id ) ;
1508+
1509+ let parentSuspense : ?SuspenseNode = null ;
1510+ if ( parentID === 0 ) {
1511+ if ( __DEBUG__ ) {
1512+ debug ( 'Suspense remove' , `node ${ id } root` ) ;
1513+ }
1514+ } else {
1515+ if ( __DEBUG__ ) {
1516+ debug ( 'Suspense Remove' , `node ${ id } from parent ${ parentID } ` ) ;
1517+ }
1518+
1519+ parentSuspense = this . _idToSuspense . get ( parentID ) ;
1520+ if ( parentSuspense === undefined ) {
1521+ this . _throwAndEmitError (
1522+ Error (
1523+ `Cannot remove suspense node "${ id } " from parent "${ parentID } " because no matching node was found in the Store.` ,
1524+ ) ,
1525+ ) ;
1526+
1527+ break ;
1528+ }
1529+
1530+ const index = parentSuspense . children . indexOf ( id ) ;
1531+ parentSuspense . children . splice ( index , 1 ) ;
1532+ }
1533+ }
1534+
1535+ hasSuspenseTreeChanged = true ;
1536+ break ;
1537+ }
1538+ case SUSPENSE_TREE_OPERATION_REORDER_CHILDREN : {
1539+ const id = operations [ i + 1 ] ;
1540+ const numChildren = operations [ i + 2 ] ;
1541+ i += 3 ;
1542+
1543+ const suspense = this . _idToSuspense . get ( id ) ;
1544+ if ( suspense === undefined ) {
1545+ this . _throwAndEmitError (
1546+ Error (
1547+ `Cannot reorder children for suspense node "${ id } " because no matching node was found in the Store.` ,
1548+ ) ,
1549+ ) ;
1550+
1551+ break ;
1552+ }
1553+
1554+ const children = suspense . children ;
1555+ if ( children . length !== numChildren ) {
1556+ this . _throwAndEmitError (
1557+ Error (
1558+ `Suspense children cannot be added or removed during a reorder operation.` ,
1559+ ) ,
1560+ ) ;
1561+ }
1562+
1563+ for ( let j = 0 ; j < numChildren ; j ++ ) {
1564+ const childID = operations [ i + j ] ;
1565+ children [ j ] = childID ;
1566+ if ( __DEV__ ) {
1567+ // This check is more expensive so it's gated by __DEV__.
1568+ const childSuspense = this . _idToSuspense . get ( childID ) ;
1569+ if ( childSuspense == null || childSuspense . parentID !== id ) {
1570+ console . error (
1571+ `Suspense children cannot be added or removed during a reorder operation.` ,
1572+ ) ;
1573+ }
1574+ }
1575+ }
1576+ i += numChildren ;
1577+
1578+ if ( __DEBUG__ ) {
1579+ debug (
1580+ 'Re-order' ,
1581+ `Suspense node ${ id } children ${ children . join ( ',' ) } ` ,
1582+ ) ;
1583+ }
1584+
1585+ hasSuspenseTreeChanged = true ;
1586+ break ;
1587+ }
13861588 default :
13871589 this . _throwAndEmitError (
13881590 new UnsupportedBridgeOperationError (
@@ -1393,6 +1595,9 @@ export default class Store extends EventEmitter<{
13931595 }
13941596
13951597 this . _revision ++ ;
1598+ if ( hasSuspenseTreeChanged ) {
1599+ this . _revisionSuspense ++ ;
1600+ }
13961601
13971602 // Any time the tree changes (e.g. elements added, removed, or reordered) cached indices may be invalid.
13981603 this . _cachedErrorAndWarningTuples = null ;
@@ -1451,6 +1656,10 @@ export default class Store extends EventEmitter<{
14511656 }
14521657 }
14531658
1659+ if ( hasSuspenseTreeChanged ) {
1660+ this . emit ( 'suspenseTreeMutated' ) ;
1661+ }
1662+
14541663 if ( __DEBUG__ ) {
14551664 console . log ( printStore ( this , true ) ) ;
14561665 console . groupEnd ( ) ;
0 commit comments