Fix module loading to ensure each module is loaded only once by pgammans · Pull Request #40389 · angular/angular · GitHub
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions goldens/size-tracking/aio-payloads.json
4 changes: 2 additions & 2 deletions goldens/size-tracking/integration-payloads.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"master": {
"uncompressed": {
"runtime-es2015": 2285,
"main-es2015": 241202,
"main-es2015": 241843,
"polyfills-es2015": 36709,
"5-es2015": 745
}
Expand All @@ -66,4 +66,4 @@
}
}
}
}
}
11 changes: 6 additions & 5 deletions packages/router/src/apply_redirects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,11 +280,12 @@ class ApplyRedirects {
segments: UrlSegment[], outlet: string): Observable<UrlSegmentGroup> {
if (route.path === '**') {
if (route.loadChildren) {
return this.configLoader.load(ngModule.injector, route)
.pipe(map((cfg: LoadedRouterConfig) => {
route._loadedConfig = cfg;
return new UrlSegmentGroup(segments, {});
}));
const loaded$ = route._loadedConfig ? of(route._loadedConfig) :
this.configLoader.load(ngModule.injector, route);
return loaded$.pipe(map((cfg: LoadedRouterConfig) => {
route._loadedConfig = cfg;
return new UrlSegmentGroup(segments, {});
}));
}

return of(new UrlSegmentGroup(segments, {}));
Expand Down
5 changes: 5 additions & 0 deletions packages/router/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,11 @@ export interface Route {
* @internal
*/
_loadedConfig?: LoadedRouterConfig;
/**
* Filled for routes with `loadChildren` during load
* @internal
*/
_loader$?: Observable<LoadedRouterConfig>;
}

export class LoadedRouterConfig {
Expand Down
50 changes: 31 additions & 19 deletions packages/router/src/router_config_loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
*/

import {Compiler, InjectFlags, InjectionToken, Injector, NgModuleFactory, NgModuleFactoryLoader} from '@angular/core';
import {from, Observable, of} from 'rxjs';
import {map, mergeMap} from 'rxjs/operators';
import {ConnectableObservable, from, Observable, of, Subject} from 'rxjs';
import {catchError, map, mergeMap, refCount, tap} from 'rxjs/operators';

import {LoadChildren, LoadedRouterConfig, Route} from './config';
import {flatten, wrapIntoObservable} from './utils/collection';
Expand All @@ -28,27 +28,39 @@ export class RouterConfigLoader {
private onLoadEndListener?: (r: Route) => void) {}

load(parentInjector: Injector, route: Route): Observable<LoadedRouterConfig> {
if (route._loader$) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that this is no longer a replay, does it make sense to return the route._loadedConfig instead if it exists or check if the route._loader$ is already complete? The callers of load here prevent this from being the case, but it might still be a good safeguard. What do you think?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes we need to return route._loadedConfig but as route._loadedConfig will only exist once route._loader$ is complete.
Currently it is the callers of load need to set route._loadedConfig themselves. which is a bit odd and and addressed in #36654

The callers are (possibly more)

  • RouterPreloader.preloadConfig
  • the pipeline of the routeGuard from ApplyRedirects.getChildConfig
  • the pipeline of the ApplyRedirects.matchSegmentAgainstRoute

The first two have checks for route._loadedConfig and use that if available so load will not get called in these cases. The later is probably should be updated to but atm the check in load would prevent duplicate loads

private matchSegmentAgainstRoute(
      ngModule: NgModuleRef<any>, rawSegmentGroup: UrlSegmentGroup, route: Route,
      segments: UrlSegment[], outlet: string): Observable<UrlSegmentGroup> {
    if (route.path === '**') {
      if (route.loadChildren) {
+         const loaded$ = route._loadedConfig ? of(route._loadedConfig) :
                                          this.configLoader.load(ngModule.injector, route);
-         return this.configLoader.load(ngModule.injector, route)
-              .pipe(map((cfg: LoadedRouterConfig) => {
+        return loaded.pipe(map((cfg: LoadedRouterConfig) => {
              route._loadedConfig = cfg;
              return new UrlSegmentGroup(segments, {});
            }));
      }

      return of(new UrlSegmentGroup(segments, {}));
    }
    ...

Lastly in the delay between create and compete of route._loader$ we need to use the same observable if load is called multiple times. Which it can be as, in fact we have test that do this.
ie there can be a call from multiple of the load callers. In the test we have one from the call RouterPreloader.preloadConfig and on from the call through ApplyRedirects.getChildConfig

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I absolutely agree that it's odd for the callers of load to set _loadedConfig rather than having RouteConfigLoader and I like the direction of #36654.

Since RouterConfigLoader isn't public API and neither are the callers of load, I'll leave it to you to decide which approach to take to ensure we aren't returning the already completed _loader observable:

  • Handle it in the load function in the same manner the callers are doing by checking for _loadedConfig
  • Update ApplyRedirects to not call load if _loadedConfig already exists.

We can then consolidate everything in #36654 or in another PR that omits the RouteConfigReady feature and only improves the DRY-ness (would be easier to land, as it's not a public api change).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@atscott Ideally I think we should put the test route._loadedConfig inside load, and move it setting to part of the load, but apply_redirects use the status of route._loadedConfig to avoid duplicating calls to the load guard.

Thus we'd need some another call to allow us prevent calling the guard twice. ie would probably need to be change to public API of Route!

So i propose: (basically what we have.)

  • load : Allows overlapping calls and will only load once.
  • load : decide what to do if the previous load is closed / finished and load called
    • throw
    • reload the module
    • just return the current loaded module, but dose this also sent event etc again?
  • leave it up to the callers of load to manage checking of route._loadedConfig

Once landed look at improving setting of route._loadedConfig to be part of the loads. observable pipe (new PR based on #36654)

I added a check to ApplyRedirects.matchSegmentAgainstRoute and a test using this code path.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pgammans - 👍 Agreed.

I think maybe you forgot to push up your changes to ApplyRedirects.matchSegmentAgainstRoute? After that's done, I'll run a final sanity check suite of internal tests and this should be good to go! Thanks for the hard work!

return route._loader$;
}

if (this.onLoadStartListener) {
this.onLoadStartListener(route);
}

const moduleFactory$ = this.loadModuleFactory(route.loadChildren!);

return moduleFactory$.pipe(map((factory: NgModuleFactory<any>) => {
if (this.onLoadEndListener) {
this.onLoadEndListener(route);
}

const module = factory.create(parentInjector);

// When loading a module that doesn't provide `RouterModule.forChild()` preloader will get
// stuck in an infinite loop. The child module's Injector will look to its parent `Injector`
// when it doesn't find any ROUTES so it will return routes for it's parent module instead.
return new LoadedRouterConfig(
flatten(module.injector.get(ROUTES, undefined, InjectFlags.Self | InjectFlags.Optional))
.map(standardizeConfig),
module);
}));
const loadRunner = moduleFactory$.pipe(
map((factory: NgModuleFactory<any>) => {
if (this.onLoadEndListener) {
this.onLoadEndListener(route);
}
const module = factory.create(parentInjector);
// When loading a module that doesn't provide `RouterModule.forChild()` preloader
// will get stuck in an infinite loop. The child module's Injector will look to
// its parent `Injector` when it doesn't find any ROUTES so it will return routes
// for it's parent module instead.
return new LoadedRouterConfig(
flatten(
module.injector.get(ROUTES, undefined, InjectFlags.Self | InjectFlags.Optional))
.map(standardizeConfig),
module);
}),
catchError((err) => {
route._loader$ = undefined;
throw err;
}),
);
// Use custom ConnectableObservable as share in runners pipe increasing the bundle size too much
route._loader$ = new ConnectableObservable(loadRunner, () => new Subject<LoadedRouterConfig>())
.pipe(refCount());
return route._loader$;
}

private loadModuleFactory(loadChildren: LoadChildren): Observable<NgModuleFactory<any>> {
Expand Down
3 changes: 2 additions & 1 deletion packages/router/src/router_preloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,8 @@ export class RouterPreloader implements OnDestroy {

private preloadConfig(ngModule: NgModuleRef<any>, route: Route): Observable<void> {
return this.preloadingStrategy.preload(route, () => {
const loaded$ = this.loader.load(ngModule.injector, route);
const loaded$ = route._loadedConfig ? of(route._loadedConfig) :
this.loader.load(ngModule.injector, route);
return loaded$.pipe(mergeMap((config: LoadedRouterConfig) => {
route._loadedConfig = config;
return this.processRoutes(config.module, config.routes);
Expand Down
39 changes: 39 additions & 0 deletions packages/router/test/apply_redirects.spec.ts
Loading