diff --git a/src/app/core/http/cache.interceptor.ts b/src/app/core/http/cache.interceptor.ts index e5c8ed2b0..911cc6291 100644 --- a/src/app/core/http/cache.interceptor.ts +++ b/src/app/core/http/cache.interceptor.ts @@ -59,7 +59,10 @@ export class CacheInterceptor implements HttpInterceptor { '/notes', // The ALM maturity breakdown counts deposits and loans as of the date asked and reads the bank's // own bucket edges from the query; a cached copy would show another date's or another setting's figures. - '/almreturn' + '/almreturn', + // The Approvals badge: a stream that stays open for minutes and a count that moves with every request raised or + // actioned. A copy of either is of no use, and the stream must never be held by anything but its own caller. + '/approvals/counts' ]; private isCacheable(url: string): boolean { @@ -93,18 +96,22 @@ export class CacheInterceptor implements HttpInterceptor { // Create new response to avoid side-effects subscriber.next(new HttpResponse(cachedData as Object)); subscriber.complete(); - } else { - next.handle(request).subscribe( - (event) => { - if (environment.httpCacheEnabled && event instanceof HttpResponse) { - this.httpCacheService.setCacheData(request.urlWithParams, event); - } - subscriber.next(event); - }, - (error) => subscriber.error(error), - () => subscriber.complete() - ); + return undefined; } + const inner = next.handle(request).subscribe( + (event) => { + if (environment.httpCacheEnabled && event instanceof HttpResponse) { + this.httpCacheService.setCacheData(request.urlWithParams, event); + } + subscriber.next(event); + }, + (error) => subscriber.error(error), + () => subscriber.complete() + ); + // The request is cancelled when its caller walks away (a page is left, a screen is closed). The inner request has to + // be cancelled with it: left to run, it kept its connection open. A browser holds about six per site, so a few + // streams that were let go but never closed filled them and every later request waited in the queue. + return () => inner.unsubscribe(); }); } } diff --git a/src/app/core/route/route-reusable-strategy.ts b/src/app/core/route/route-reusable-strategy.ts index 7eb61b946..708aeaf7c 100644 --- a/src/app/core/route/route-reusable-strategy.ts +++ b/src/app/core/route/route-reusable-strategy.ts @@ -9,6 +9,7 @@ /** Angular Imports */ import { ActivatedRouteSnapshot, DetachedRouteHandle, RouteReuseStrategy } from '@angular/router'; import { Injectable } from '@angular/core'; +import { ShellComponent } from '../shell/shell.component'; /** * A route strategy allowing for explicit route reuse. @@ -31,7 +32,22 @@ export class RouteReusableStrategy extends RouteReuseStrategy { return null; } + /** + * Every feature module registers its own copy of the shell route (Route.withShell), so two pages in different modules + * sit under two different shell routes. Treated as different, a click from one module into another destroyed the + * shell and built a new one: the menu bar vanished until it had loaded again, and the shell's own calls (menu access, + * notifications, business date, the approval stream) went out again on every such click. The shell is the same + * component whichever module it wraps, so it is kept: only the page inside it changes, until a hard refresh. + */ public shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean { - return future.routeConfig === curr.routeConfig || future.data.reuse; + return ( + future.routeConfig === curr.routeConfig || + future.data.reuse || + (RouteReusableStrategy.isShell(future) && RouteReusableStrategy.isShell(curr)) + ); + } + + private static isShell(route: ActivatedRouteSnapshot): boolean { + return route.routeConfig?.component === ShellComponent; } } diff --git a/src/app/core/shell/shell.component.ts b/src/app/core/shell/shell.component.ts index 49383469e..19776d78b 100644 --- a/src/app/core/shell/shell.component.ts +++ b/src/app/core/shell/shell.component.ts @@ -81,8 +81,9 @@ export class ShellComponent implements OnInit, OnDestroy { .pipe(map((result) => result.matches)); /** Sets the initial state of sidenav as collapsed. Not collapsed if false. */ sidenavCollapsed = true; - /** Progress bar mode. */ - progressBarMode: string; + /** Progress bar mode. It starts from what is running right now: the template shows the bar for anything that is not + * 'none', so an empty start left it running until some request happened to finish. */ + progressBarMode: string = this.progressBarService.getRequestsRunning() > 0 ? 'indeterminate' : 'none'; /** Subscription to progress bar. */ progressBar$: Subscription; /** Hides nav/sidebar for full-screen cashier routes. */