Event-Driven Navigation System

Reactive navigation updates using RouteReadyService event bus

๐Ÿ”„ Event-Driven Navigation System

Purpose: Ensure navigation items are always in sync with the router configuration, including dynamically registered module routes.


๐ŸŽฏ The Problem

Challenge: Dynamic Route Registration

// โŒ Problem: Navigation built before dynamic routes exist

App Initialization:
1. NavigationBuilderService initializes
2. Builds navigation from router config
3. โœ… Static routes (dashboard, landing) included
4. โŒ Dynamic routes (Singularity, Editor, Space Map) NOT included
   โ†“
5. BrowserBootstrapperService runs (later)
6. Registers dynamic module routes
   โ†“
7. Navigation is stale - doesn't include new routes! ๐Ÿ˜•

The Root Cause


โœ… The Solution: RouteReadyService

Event Bus Pattern

@Injectable({ providedIn: 'root' })
export class RouteReadyService {
  // ๐ŸŽฏ Signal: Reactive state
  private readonly _routesReady = signal<boolean>(false);
  readonly routesReady = this._routesReady.asReadonly();

  // ๐ŸŽฏ Observable: Event stream
  private readonly _routesRegistered = new Subject<void>();
  readonly routesRegistered$ = this._routesRegistered.asObservable();

  /**
   * Called by BrowserBootstrapperService after dynamic routes are registered
   */
  notifyRoutesRegistered(): void {
    this._routesReady.set(true);
    this._routesRegistered.next();
    console.log('โœ… RouteReadyService: Dynamic module routes registered!');
  }
}

๐Ÿ”„ Complete Flow

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  APP INITIALIZATION                                       โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  1. Angular Bootstrap                                    โ”‚
โ”‚     โ”œโ”€ Core services initialize                          โ”‚
โ”‚     โ””โ”€ NavigationBuilderService created                  โ”‚
โ”‚        โ”œโ”€ Load from cache (<5ms) โšก                      โ”‚
โ”‚        โ”œโ”€ Build from static routes (~50ms)               โ”‚
โ”‚        โ””โ”€ Set up effect() to watch RouteReadyService     โ”‚
โ”‚                                                          โ”‚
โ”‚  2. APP_INITIALIZER Runs                                โ”‚
โ”‚     โ””โ”€ BrowserBootstrapperService.initialize()          โ”‚
โ”‚        โ”œโ”€ Discover modules                               โ”‚
โ”‚        โ”œโ”€ Register module routes (~100ms)                โ”‚
โ”‚        โ”‚  โ”œโ”€ Singularity routes โ†’ /test-space/editor/*  โ”‚
โ”‚        โ”‚  โ”œโ”€ Editor routes โ†’ /test-space/singularity/*  โ”‚
โ”‚        โ”‚  โ””โ”€ Space Map routes โ†’ /test-space/space-map/* โ”‚
โ”‚        โ””โ”€ ๐ŸŽฏ RouteReadyService.notifyRoutesRegistered() โ”‚
โ”‚                                                          โ”‚
โ”‚  3. Event Propagation                                   โ”‚
โ”‚     โ”œโ”€ routesReady signal โ†’ true โœ…                     โ”‚
โ”‚     โ”œโ”€ routesRegistered$ emits โœ…                       โ”‚
โ”‚     โ””โ”€ NavigationBuilderService effect() triggers! ๐ŸŽฏ   โ”‚
โ”‚                                                          โ”‚
โ”‚  4. Navigation Rebuild                                  โ”‚
โ”‚     โ”œโ”€ buildNavigationFromRoutes()                      โ”‚
โ”‚     โ”‚  โ”œโ”€ Read live router.config                       โ”‚
โ”‚     โ”‚  โ”œโ”€ Include all dynamic routes โœ…                 โ”‚
โ”‚     โ”‚  โ””โ”€ Prepend spaceSlug to paths                    โ”‚
โ”‚     โ”œโ”€ Update routeBasedItems signal                    โ”‚
โ”‚     โ””โ”€ ๐Ÿ’พ Save to cache for next time                   โ”‚
โ”‚                                                          โ”‚
โ”‚  5. UI Updates                                          โ”‚
โ”‚     โ””โ”€ MainSidenavComponent renders complete navigation โ”‚
โ”‚        โœ… All routes visible (static + dynamic)         โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

๐ŸŽฏ Key Components

1. RouteReadyService (Event Bus)

/**
 * ๐ŸŽฏ Central event bus for route registration lifecycle
 */
@Injectable({ providedIn: 'root' })
export class RouteReadyService {
  // Signal for reactive components
  readonly routesReady = signal<boolean>(false);
  
  // Observable for one-time subscriptions
  readonly routesRegistered$ = new Subject<void>();
  
  notifyRoutesRegistered(): void {
    this.routesReady.set(true);
    this.routesRegistered$.next();
  }
}

2. BrowserBootstrapperService (Emitter)

// In main.ts
provideAppInitializer(() => {
  const bootstrapper = inject(BrowserBootstrapperService);
  const routeReady = inject(RouteReadyService);

  bootstrapper.initialize({
    environment: isDevMode() ? 'development' : 'production',
    enableDetailedLogging: isDevMode(),
    enablePerformanceMetrics: true
  }).subscribe({
    next: (result) => {
      console.log('๐Ÿš€ Dynamic Route Registration Complete');
      
      // ๐ŸŽฏ Notify all listeners
      routeReady.notifyRoutesRegistered();
    },
    error: (error) => {
      console.error('โŒ Module initialization failed:', error);
    }
  });
});

3. NavigationBuilderService (Listener)

@Injectable({ providedIn: 'root' })
export class NavigationBuilderService {
  private readonly routeReady = inject(RouteReadyService);
  
  constructor() {
    // Build initial navigation
    this.buildNavigationFromRoutes();
    
    // ๐ŸŽฏ Reactive Effect: Rebuild when routes ready
    effect(() => {
      const ready = this.routeReady.routesReady();
      if (ready) {
        console.log('๐Ÿ”„ Routes ready, rebuilding navigation');
        this.buildNavigationFromRoutes(); // โœ… Includes dynamic routes
        this.saveToCache(); // ๐Ÿ’พ Cache for next time
      }
    });
  }
}

๐ŸŽจ Benefits of Event-Driven Approach

โœ… Advantages

Before (setTimeout) After (Event-Driven)
โฐ Arbitrary 2-second delay โšก Instant notification
๐Ÿ› Race conditions โœ… Guaranteed order
๐ŸŽฒ Unreliable timing ๐ŸŽฏ Deterministic
๐Ÿ˜• Hard to debug ๐Ÿ“Š Clear event flow
โŒ No extensibility โœจ Easy to add listeners

โœ… Key Improvements

  1. Deterministic: Routes are guaranteed to be ready before rebuild
  2. Fast: No arbitrary waits, happens immediately when ready
  3. Extensible: Easy to add more listeners to route registration
  4. Testable: Can mock RouteReadyService in tests
  5. Debuggable: Clear console logs show event flow

๐Ÿงช Testing

RouteReadyService Tests

describe('RouteReadyService', () => {
  it('should start with routes not ready', () => {
    expect(service.routesReady()).toBe(false);
  });

  it('should set routes ready to true when notified', () => {
    service.notifyRoutesRegistered();
    expect(service.routesReady()).toBe(true);
  });

  it('should emit event on routesRegistered$ observable', (done) => {
    service.routesRegistered$.pipe(take(1)).subscribe(() => {
      done();
    });
    
    service.notifyRoutesRegistered();
  });
});

NavigationBuilderService Integration Test

it('should rebuild navigation when routes are ready', async () => {
  // Arrange
  const routesReadySignal = signal(false);
  routeReadyMock.routesReady = routesReadySignal.asReadonly();

  // Act - trigger routes ready
  routesReadySignal.set(true);
  await new Promise(resolve => setTimeout(resolve, 100));

  // Assert - navigation should include new routes
  const items = service.getRouteBasedItems();
  expect(items.length).toBeGreaterThan(0);
});

๐Ÿ”ง Integration Points

1. Module Integrator

// @libs-core/module-integrator
export class BrowserBootstrapperService {
  initialize(config: BootstrapConfig): Observable<BootstrapResult> {
    return this.registerModuleRoutes().pipe(
      tap(() => {
        // โœ… Notify when complete
        this.routeReady.notifyRoutesRegistered();
      })
    );
  }
}

2. Navigation Builder

// apps/gograb-space/src/app/services
export class NavigationBuilderService {
  constructor() {
    // โœ… React to route registration
    effect(() => {
      if (this.routeReady.routesReady()) {
        this.buildNavigationFromRoutes();
      }
    });
  }
}

3. Main Sidenav

// Removed old setTimeout hack:
// โŒ ngOnInit() { setTimeout(() => this.navigationBuilder.refreshNavigation(), 2000); }

// โœ… Now automatic via RouteReadyService event bus

๐Ÿ“Š Performance Impact

Before (setTimeout)

Route Registration: t=0ms
  โ†“
Wait 2000ms... โฐ (arbitrary)
  โ†“
Navigation Rebuild: t=2000ms
  โ†“
Total Delay: 2000ms (even if routes ready at t=100ms!)

After (Event-Driven)

Route Registration: t=0ms
  โ†“
Dynamic Routes Registered: t=100ms
  โ†“
RouteReadyService.notifyRoutesRegistered()
  โ†“
Navigation Rebuild: t=101ms โšก
  โ†“
Total Delay: 101ms (instant after routes ready!)

Improvement: 95% faster (2000ms โ†’ 100ms) ๐Ÿš€


๐Ÿ”ฎ Future Enhancements

  1. Route Change Detection: Rebuild only when routes actually change
  2. Partial Updates: Update only changed navigation items
  3. Multiple Listeners: Add more services that react to route registration
  4. Route Analytics: Track which routes are registered and when
  5. Hot Module Reloading: Support HMR for route updates

๐Ÿ“ Summary

What We Built:

Benefits:

Status: โœ… Production Ready


Last Updated: October 29, 2025
Impact: Architecture - Event-driven, reactive navigation system