Skip to content

Commit 8bb6a03

Browse files
Copilothotlong
andcommitted
fix: address code review - type-safe app name access, effect cancellation and race condition handling
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent a9401f2 commit 8bb6a03

File tree

1 file changed

+37
-11
lines changed

1 file changed

+37
-11
lines changed

apps/console/src/hooks/useNavigationSync.ts

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)