@@ -399,11 +399,15 @@ export function useNavigationSync(): UseNavigationSyncReturn {
399399 // All-Apps convenience methods
400400 // ------------------------------------------------------------------
401401
402+ /** Safely extract the app name from a metadata entry. */
403+ const getAppName = ( app : unknown ) : string | undefined =>
404+ app && typeof app === 'object' && 'name' in app ? ( app as { name : string } ) . name : undefined ;
405+
402406 /** Add a page nav item to ALL apps that don't already reference it. */
403407 const syncPageCreatedAllApps = useCallback (
404408 async ( pageName : string , label ?: string ) => {
405409 for ( const app of apps ) {
406- const name = ( app as any ) . name ;
410+ const name = getAppName ( app ) ;
407411 if ( ! name ) continue ;
408412 await syncPageCreated ( name , pageName , label ) ;
409413 }
@@ -415,7 +419,7 @@ export function useNavigationSync(): UseNavigationSyncReturn {
415419 const syncDashboardCreatedAllApps = useCallback (
416420 async ( dashboardName : string , label ?: string ) => {
417421 for ( const app of apps ) {
418- const name = ( app as any ) . name ;
422+ const name = getAppName ( app ) ;
419423 if ( ! name ) continue ;
420424 await syncDashboardCreated ( name , dashboardName , label ) ;
421425 }
@@ -427,7 +431,7 @@ export function useNavigationSync(): UseNavigationSyncReturn {
427431 const syncPageDeletedAllApps = useCallback (
428432 async ( pageName : string ) => {
429433 for ( const app of apps ) {
430- const name = ( app as any ) . name ;
434+ const name = getAppName ( app ) ;
431435 if ( ! name ) continue ;
432436 await syncPageDeleted ( name , pageName ) ;
433437 }
@@ -439,7 +443,7 @@ export function useNavigationSync(): UseNavigationSyncReturn {
439443 const syncDashboardDeletedAllApps = useCallback (
440444 async ( dashboardName : string ) => {
441445 for ( const app of apps ) {
442- const name = ( app as any ) . name ;
446+ const name = getAppName ( app ) ;
443447 if ( ! name ) continue ;
444448 await syncDashboardDeleted ( name , dashboardName ) ;
445449 }
@@ -451,7 +455,7 @@ export function useNavigationSync(): UseNavigationSyncReturn {
451455 const syncPageRenamedAllApps = useCallback (
452456 async ( oldName : string , newName : string ) => {
453457 for ( const app of apps ) {
454- const name = ( app as any ) . name ;
458+ const name = getAppName ( app ) ;
455459 if ( ! name ) continue ;
456460 await syncPageRenamed ( name , oldName , newName ) ;
457461 }
@@ -463,7 +467,7 @@ export function useNavigationSync(): UseNavigationSyncReturn {
463467 const syncDashboardRenamedAllApps = useCallback (
464468 async ( oldName : string , newName : string ) => {
465469 for ( const app of apps ) {
466- const name = ( app as any ) . name ;
470+ const name = getAppName ( app ) ;
467471 if ( ! name ) continue ;
468472 await syncDashboardRenamed ( name , oldName , newName ) ;
469473 }
@@ -519,9 +523,16 @@ export function NavigationSyncEffect(): null {
519523 const prevPageNamesRef = useRef < Set < string > | null > ( null ) ;
520524 const prevDashNamesRef = useRef < Set < string > | null > ( null ) ;
521525
522- // Guard against circular refreshes triggered by saveApp → refresh → effect
526+ // Guard against circular refreshes and concurrent sync operations.
527+ // Incremented on each effect invocation; the async closure captures
528+ // its own version and bails out if a newer run has started.
529+ const syncVersionRef = useRef ( 0 ) ;
523530 const syncingRef = useRef ( false ) ;
524531
532+ /** Safely extract the app name from a metadata entry. */
533+ const getAppName = ( app : unknown ) : string | undefined =>
534+ app && typeof app === 'object' && 'name' in app ? ( app as { name : string } ) . name : undefined ;
535+
525536 useEffect ( ( ) => {
526537 if ( syncingRef . current ) return ;
527538
@@ -561,33 +572,48 @@ export function NavigationSyncEffect(): null {
561572 }
562573
563574 // Sync navigation across all apps
575+ const version = ++ syncVersionRef . current ;
564576 syncingRef . current = true ;
565577
578+ let cancelled = false ;
579+
566580 ( async ( ) => {
567581 try {
568582 for ( const app of apps ) {
569- const appName = ( app as any ) . name ;
583+ if ( cancelled || syncVersionRef . current !== version ) break ;
584+ const appName = getAppName ( app ) ;
570585 if ( ! appName ) continue ;
571586
572587 for ( const pageName of addedPages ) {
588+ if ( cancelled ) break ;
573589 await syncPageCreated ( appName , pageName ) ;
574590 }
575591 for ( const pageName of removedPages ) {
592+ if ( cancelled ) break ;
576593 await syncPageDeleted ( appName , pageName ) ;
577594 }
578595 for ( const dashName of addedDash ) {
596+ if ( cancelled ) break ;
579597 await syncDashboardCreated ( appName , dashName ) ;
580598 }
581599 for ( const dashName of removedDash ) {
600+ if ( cancelled ) break ;
582601 await syncDashboardDeleted ( appName , dashName ) ;
583602 }
584603 }
585604 } finally {
586- syncingRef . current = false ;
587- prevPageNamesRef . current = currentPageNames ;
588- prevDashNamesRef . current = currentDashNames ;
605+ // Only update refs if this is still the latest sync version
606+ if ( syncVersionRef . current === version ) {
607+ prevPageNamesRef . current = currentPageNames ;
608+ prevDashNamesRef . current = currentDashNames ;
609+ syncingRef . current = false ;
610+ }
589611 }
590612 } ) ( ) ;
613+
614+ return ( ) => {
615+ cancelled = true ;
616+ } ;
591617 } , [ pages , dashboards , apps , syncPageCreated , syncDashboardCreated , syncPageDeleted , syncDashboardDeleted ] ) ;
592618
593619 return null ;
0 commit comments