Search code examples
angularangular-universalangular-transfer-state

TransferState: Who guarantees that data is already stored in state?


I'm doing SSR with TransferState and wondering who guarantees that when we do

http.get(...).subscribe(data => {
  transferState.set(DATA_KEY, data)
})

data will be stored in transferState? Because http.get is async operation and content could be generate and provided to client without this data.


Solution

  • Angular Zone guarantees that all async operations (the calls tracked by zone.js ) to be finished before rendering.

    Let's take a look at

    server.ts

    app.get('*', (req, res) => {
      res.render('index', { req });
    });   
                          ||
                          \/
    app.engine('html', ngExpressEngine({
      bootstrap: AppServerModuleNgFactory,
      providers: [
        provideModuleMap(LAZY_MODULE_MAP)
      ]
    }));
    

    We can see that all regular routes use the Universal engine to render the html.

    res.render method (1) defines default callback. The ngExpressEngine function returns another function with that callback passed as parameter (2). As soon as that callback is triggered express sends the result to the user.

    done = done || function (err, str) {
      if (err) return req.next(err);
      self.send(str);
    };
    

    Now let's see when that callback will be triggered. As mentioned before we need to look at ngExpressEngine function.

     getFactory(moduleOrFactory, compiler)
       .then(factory => {
           return renderModuleFactory(factory, {
              extraProviders
           });
        })
        .then((html: string) => {
          callback(null, html);
        }, (err) => {
          callback(err);
    }); 
    

    It will happen only after the promise(3), returning from renderModuleFactory function, is resolved.

    renderModuleFactory function can be found at @angular/platform-server

    export function renderModuleFactory<T>(
        moduleFactory: NgModuleFactory<T>,
        options: {document?: string, url?: string, extraProviders?: StaticProvider[]}):
        Promise<string> {
      const platform = _getPlatform(platformServer, options);
      return _render(platform, platform.bootstrapModuleFactory(moduleFactory));
    }
    

    You can see above that we actually run Angular application here through platform.bootstrapModuleFactory(moduleFactory) (4)

    Inside _render function(5) application waits for bootstrapping to be finished

    return moduleRefPromise.then((moduleRef) => {
    

    and after that we can see the key for the answer:

    return applicationRef.isStable.pipe((first((isStable: boolean) => isStable)))
            .toPromise()
            .then(() => {
    

    You can see that angular universal looks at ApplicationRef.isStable observable to know when to finish rendering. In simple words isStable on ApplicationRef is triggered when Zone has no microtasks scheduled (7):

    if (!zone.hasPendingMicrotasks) {
      try {
        zone.runOutsideAngular(() => zone.onStable.emit(null));
    

    enter image description here