@@ -27,20 +27,21 @@ import injectBackendManager from './injectBackendManager';
2727import syncSavedPreferences from './syncSavedPreferences' ;
2828import registerEventsLogger from './registerEventsLogger' ;
2929import getProfilingFlags from './getProfilingFlags' ;
30+ import debounce from './debounce' ;
3031import './requestAnimationFramePolyfill' ;
3132
3233// Try polling for at least 5 seconds, in case if it takes too long to load react
3334const REACT_POLLING_TICK_COOLDOWN = 250 ;
3435const REACT_POLLING_ATTEMPTS_THRESHOLD = 20 ;
3536
3637let reactPollingTimeoutId = null ;
37- function clearReactPollingTimeout ( ) {
38+ export function clearReactPollingTimeout ( ) {
3839 clearTimeout ( reactPollingTimeoutId ) ;
3940 reactPollingTimeoutId = null ;
4041}
4142
42- function executeIfReactHasLoaded ( callback , attempt = 1 ) {
43- reactPollingTimeoutId = null ;
43+ export function executeIfReactHasLoaded ( callback , attempt = 1 ) {
44+ clearReactPollingTimeout ( ) ;
4445
4546 if ( attempt > REACT_POLLING_ATTEMPTS_THRESHOLD ) {
4647 return ;
@@ -81,21 +82,26 @@ function executeIfReactHasLoaded(callback, attempt = 1) {
8182 ) ;
8283}
8384
85+ let lastSubscribedBridgeListener = null ;
86+
8487function createBridge ( ) {
8588 bridge = new Bridge ( {
8689 listen ( fn ) {
87- const listener = message => fn ( message ) ;
90+ const bridgeListener = message => fn ( message ) ;
8891 // Store the reference so that we unsubscribe from the same object.
8992 const portOnMessage = port . onMessage ;
90- portOnMessage . addListener ( listener ) ;
93+ portOnMessage . addListener ( bridgeListener ) ;
94+
95+ lastSubscribedBridgeListener = bridgeListener ;
9196
9297 return ( ) => {
93- portOnMessage . removeListener ( listener ) ;
98+ port ?. onMessage . removeListener ( bridgeListener ) ;
99+ lastSubscribedBridgeListener = null ;
94100 } ;
95101 } ,
96102
97103 send ( event : string , payload : any , transferable ?: Array < any > ) {
98- port. postMessage ( { event, payload} , transferable ) ;
104+ port? .postMessage ( { event, payload} , transferable ) ;
99105 } ,
100106 } ) ;
101107
@@ -469,9 +475,6 @@ function performInTabNavigationCleanup() {
469475 bridge = null ;
470476 render = null ;
471477 root = null ;
472-
473- port ?. disconnect ( ) ;
474- port = null ;
475478}
476479
477480function performFullCleanup ( ) {
@@ -499,23 +502,37 @@ function performFullCleanup() {
499502}
500503
501504function connectExtensionPort ( ) {
505+ if ( port ) {
506+ throw new Error ( 'DevTools port was already connected' ) ;
507+ }
508+
502509 const tabId = chrome . devtools . inspectedWindow . tabId ;
503510 port = chrome . runtime . connect ( {
504511 name : String ( tabId ) ,
505512 } ) ;
506513
514+ // If DevTools port was reconnected and Bridge was already created
515+ // We should subscribe bridge to this port events
516+ // This could happen if service worker dies and all ports are disconnected,
517+ // but later user continues the session and Chrome reconnects all ports
518+ // Bridge object is still in-memory, though
519+ if ( lastSubscribedBridgeListener ) {
520+ port . onMessage . addListener ( lastSubscribedBridgeListener ) ;
521+ }
522+
507523 // This port may be disconnected by Chrome at some point, this callback
508524 // will be executed only if this port was disconnected from the other end
509525 // so, when we call `port.disconnect()` from this script,
510526 // this should not trigger this callback and port reconnection
511- port . onDisconnect . addListener ( connectExtensionPort ) ;
527+ port . onDisconnect . addListener ( ( ) => {
528+ port = null ;
529+ connectExtensionPort ( ) ;
530+ } ) ;
512531}
513532
514533function mountReactDevTools ( ) {
515534 registerEventsLogger ( ) ;
516535
517- connectExtensionPort ( ) ;
518-
519536 createBridgeAndStore ( ) ;
520537
521538 setReactSelectionFromBrowser ( bridge ) ;
@@ -532,7 +549,7 @@ function mountReactDevToolsWhenReactHasLoaded() {
532549 mountReactDevTools ( ) ;
533550 }
534551
535- executeIfReactHasLoaded ( onReactReady ) ;
552+ executeIfReactHasLoaded ( onReactReady , 1 ) ;
536553}
537554
538555let bridge = null ;
@@ -555,11 +572,18 @@ let port = null;
555572// since global values stored on window get reset in this case.
556573chrome . devtools . network . onNavigated . addListener ( syncSavedPreferences ) ;
557574
558- // Cleanup previous page state and remount everything
559- chrome . devtools . network . onNavigated . addListener ( ( ) => {
575+ // In case when multiple navigation events emitted in a short period of time
576+ // This debounced callback primarily used to avoid mounting React DevTools multiple times, which results
577+ // into subscribing to the same events from Bridge and window multiple times
578+ // In this case, we will handle `operations` event twice or more and user will see
579+ // `Cannot add node "1" because a node with that id is already in the Store.`
580+ const debouncedOnNavigatedListener = debounce ( ( ) => {
560581 performInTabNavigationCleanup ( ) ;
561582 mountReactDevToolsWhenReactHasLoaded ( ) ;
562- } ) ;
583+ } , 500 ) ;
584+
585+ // Cleanup previous page state and remount everything
586+ chrome . devtools . network . onNavigated . addListener ( debouncedOnNavigatedListener ) ;
563587
564588// Should be emitted when browser DevTools are closed
565589if ( IS_FIREFOX ) {
@@ -569,5 +593,7 @@ if (IS_FIREFOX) {
569593 window . addEventListener ( 'beforeunload' , performFullCleanup ) ;
570594}
571595
596+ connectExtensionPort ( ) ;
597+
572598syncSavedPreferences ( ) ;
573599mountReactDevToolsWhenReactHasLoaded ( ) ;
0 commit comments