angularangular-universalangular-transfer-state

Angular Universal Transfer State not working as expected


I have an issue where data from my API is not being shown in the view source of my project. I did some research and came across the TransferState so I created a class:

import { Injectable } from '@angular/core';
import { TransferState } from '@angular/platform-browser';
import { Observable, from } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { tap } from 'rxjs/operators';

@Injectable({
  providedIn: 'root',
})
export class TransferHttpService {
  constructor(
    private transferHttp: TransferState,
    private httpClient: HttpClient
  ) {}
  get(url, options?): Observable<any> {
    return this.getData(url, options, () => {
      return this.httpClient.get(url, options);
    });
  }
  post(url, body, options?): Observable<any> {
    return this.getData(url, options, () => {
      return this.httpClient.post(url, body, options);
    });
  }
  delete(url, options?): Observable<any> {
    return this.getData(url, options, () => {
      return this.httpClient.delete(url, options);
    });
  }
  put(url, body, options?): Observable<any> {
    return this.getData(url, options, () => {
      return this.httpClient.put(url, body, options);
    });
  }
  getData(url, options, callback: () => Observable<any>): Observable<any> {
    const optionsString = options ? JSON.stringify(options) : '';
    let key = `${url + optionsString}`;
    try {
      return this.resolveData(key);
    } catch (e) {
      console.log('In catch', key);
      return callback().pipe(
        tap((data) => {
          console.log('cache set', key);
          this.setCache(key, data);
        })
      );
    }
  }
  resolveData(key) {
    let resultData: any;
    if (this.hasKey(key)) {
      resultData = this.getFromCache(key);
      console.log('got cache', key);
    } else {
      throw new Error();
    }
    return from(Promise.resolve(resultData));
  }
  setCache(key, value) {
    this.transferHttp.set(key, value);
  }
  getFromCache(key) {
    return this.transferHttp.get(key, null); // null set as default value
  }
  hasKey(key) {
    return this.transferHttp.hasKey(key);
  }
}

And in any service where I was using HttpClient, I now replaced with TransferHttpService instead. If you look at the service, you can see a few console logs. This is useful because when the data is grabbed from the cache, I can see it in View Page Source but if it has to insert it into the cache, then it's not in the page source.

When I first tested it, all seemed to be fine. I got this:

enter image description here

In my project console I could see this:

enter image description here

Which is great. But it doesn't seem to work all the time. If I refresh the page, sometimes it works, most of the time it doesn't:

enter image description here

In this case, the navigation and footer are both cached, but the pages isn't which means it isn't in the View Page Source.

Sometimes it's much worse, where all of the items are not cached:

enter image description here

And in that case, nothing is in the View Page Source.

What can I do to make sure the requests are always cached? Do I need to create a resolver or is it something more simple?


Solution

  • This was caused by two issues. The first was because I was using contentfuls SDK and out of the box it uses HttpClient, which actually needed overriding to use the TransferHttpService (see here)

    The second issue was the TransferHttpService itself. I changed it to this:

    import { Injectable, Inject, PLATFORM_ID } from '@angular/core';
    import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
    import {
      TransferState,
      StateKey,
      makeStateKey,
    } from '@angular/platform-browser';
    import { Observable, from } from 'rxjs';
    import { tap } from 'rxjs/operators';
    import { isPlatformBrowser, isPlatformServer } from '@angular/common';
    
    @Injectable({ providedIn: 'root' })
    export class TransferHttpService {
      constructor(
        protected transferState: TransferState,
        private httpClient: HttpClient,
        @Inject(PLATFORM_ID) private platformId: Object
      ) {}
    
      request<T>(
        method: string,
        uri: string | Request,
        options?: {
          body?: any;
          headers?:
            | HttpHeaders
            | {
                [header: string]: string | string[];
              };
          reportProgress?: boolean;
          observe?: 'response';
          params?:
            | HttpParams
            | {
                [param: string]: string | string[];
              };
          responseType?: 'json';
          withCredentials?: boolean;
        }
      ): Observable<T> {
        // tslint:disable-next-line:no-shadowed-variable
        return this.getData<T>(
          method,
          uri,
          options,
          (method: string, url: string, options: any) => {
            return this.httpClient.request<T>(method, url, options);
          }
        );
      }
    
      /**
       * Performs a request with `get` http method.
       */
      get<T>(
        url: string,
        options?: {
          headers?:
            | HttpHeaders
            | {
                [header: string]: string | string[];
              };
          observe?: 'response';
          params?:
            | HttpParams
            | {
                [param: string]: string | string[];
              };
          reportProgress?: boolean;
          responseType?: 'json';
          withCredentials?: boolean;
        }
      ): Observable<T> {
        // tslint:disable-next-line:no-shadowed-variable
        return this.getData<T>(
          'get',
          url,
          options,
          (_method: string, url: string, options: any) => {
            return this.httpClient.get<T>(url, options);
          }
        );
      }
    
      /**
       * Performs a request with `post` http method.
       */
      post<T>(
        url: string,
        body: any,
        options?: {
          headers?:
            | HttpHeaders
            | {
                [header: string]: string | string[];
              };
          observe?: 'response';
          params?:
            | HttpParams
            | {
                [param: string]: string | string[];
              };
          reportProgress?: boolean;
          responseType?: 'json';
          withCredentials?: boolean;
        }
      ): Observable<T> {
        // tslint:disable-next-line:no-shadowed-variable
        return this.getPostData<T>(
          'post',
          url,
          body,
          options,
          // tslint:disable-next-line:no-shadowed-variable
          (_method: string, url: string, body: any, options: any) => {
            return this.httpClient.post<T>(url, body, options);
          }
        );
      }
    
      /**
       * Performs a request with `put` http method.
       */
      put<T>(
        url: string,
        _body: any,
        options?: {
          headers?:
            | HttpHeaders
            | {
                [header: string]: string | string[];
              };
          observe?: 'body';
          params?:
            | HttpParams
            | {
                [param: string]: string | string[];
              };
          reportProgress?: boolean;
          responseType?: 'json';
          withCredentials?: boolean;
        }
      ): Observable<T> {
        // tslint:disable-next-line:no-shadowed-variable
        return this.getPostData<T>(
          'put',
          url,
          _body,
          options,
          (_method: string, url: string, _body: any, options: any) => {
            return this.httpClient.put<T>(url, _body, options);
          }
        );
      }
    
      /**
       * Performs a request with `delete` http method.
       */
      delete<T>(
        url: string,
        options?: {
          headers?:
            | HttpHeaders
            | {
                [header: string]: string | string[];
              };
          observe?: 'response';
          params?:
            | HttpParams
            | {
                [param: string]: string | string[];
              };
          reportProgress?: boolean;
          responseType?: 'json';
          withCredentials?: boolean;
        }
      ): Observable<T> {
        // tslint:disable-next-line:no-shadowed-variable
        return this.getData<T>(
          'delete',
          url,
          options,
          (_method: string, url: string, options: any) => {
            return this.httpClient.delete<T>(url, options);
          }
        );
      }
    
      /**
       * Performs a request with `patch` http method.
       */
      patch<T>(
        url: string,
        body: any,
        options?: {
          headers?:
            | HttpHeaders
            | {
                [header: string]: string | string[];
              };
          observe?: 'response';
          params?:
            | HttpParams
            | {
                [param: string]: string | string[];
              };
          reportProgress?: boolean;
          responseType?: 'json';
          withCredentials?: boolean;
        }
      ): Observable<T> {
        // tslint:disable-next-line:no-shadowed-variable
        return this.getPostData<T>(
          'patch',
          url,
          body,
          options,
          // tslint:disable-next-line:no-shadowed-variable
          (
            _method: string,
            url: string,
            body: any,
            options: any
          ): Observable<any> => {
            return this.httpClient.patch<T>(url, body, options);
          }
        );
      }
    
      /**
       * Performs a request with `head` http method.
       */
      head<T>(
        url: string,
        options?: {
          headers?:
            | HttpHeaders
            | {
                [header: string]: string | string[];
              };
          observe?: 'response';
          params?:
            | HttpParams
            | {
                [param: string]: string | string[];
              };
          reportProgress?: boolean;
          responseType?: 'json';
          withCredentials?: boolean;
        }
      ): Observable<T> {
        // tslint:disable-next-line:no-shadowed-variable
        return this.getData<T>(
          'head',
          url,
          options,
          (_method: string, url: string, options: any) => {
            return this.httpClient.head<T>(url, options);
          }
        );
      }
    
      /**
       * Performs a request with `options` http method.
       */
      options<T>(
        url: string,
        options?: {
          headers?:
            | HttpHeaders
            | {
                [header: string]: string | string[];
              };
          observe?: 'response';
          params?:
            | HttpParams
            | {
                [param: string]: string | string[];
              };
          reportProgress?: boolean;
          responseType?: 'json';
          withCredentials?: boolean;
        }
      ): Observable<T> {
        // tslint:disable-next-line:no-shadowed-variable
        return this.getData<T>(
          'options',
          url,
          options,
          // tslint:disable-next-line:no-shadowed-variable
          (_method: string, url: string, options: any) => {
            return this.httpClient.options<T>(url, options);
          }
        );
      }
    
      // tslint:disable-next-line:max-line-length
      getData<T>(
        method: string,
        uri: string | Request,
        options: any,
        callback: (
          method: string,
          uri: string | Request,
          options: any
        ) => Observable<any>
      ): Observable<T> {
        let url = uri;
    
        if (typeof uri !== 'string') {
          url = uri.url;
        }
    
        const tempKey = url + (options ? JSON.stringify(options) : '');
        const key = makeStateKey<T>(tempKey);
        try {
          return this.resolveData<T>(key);
        } catch (e) {
          //console.log('in catch', key);
          return callback(method, uri, options).pipe(
            tap((data: T) => {
              if (isPlatformBrowser(this.platformId)) {
                // Client only code.
                // nothing;
              }
              if (isPlatformServer(this.platformId)) {
                //console.log('set cache', key);
                this.setCache<T>(key, data);
              }
            })
          );
        }
      }
    
      private getPostData<T>(
        _method: string,
        uri: string | Request,
        body: any,
        options: any,
        callback: (
          method: string,
          uri: string | Request,
          body: any,
          options: any
        ) => Observable<any>
      ): Observable<T> {
        let url = uri;
    
        if (typeof uri !== 'string') {
          url = uri.url;
        }
    
        const tempKey =
          url +
          (body ? JSON.stringify(body) : '') +
          (options ? JSON.stringify(options) : '');
        const key = makeStateKey<T>(tempKey);
    
        try {
          return this.resolveData<T>(key);
        } catch (e) {
          return callback(_method, uri, body, options).pipe(
            tap((data: T) => {
              if (isPlatformBrowser(this.platformId)) {
                // Client only code.
                // nothing;
              }
              if (isPlatformServer(this.platformId)) {
                this.setCache<T>(key, data);
              }
            })
          );
        }
      }
    
      private resolveData<T>(key: StateKey<T>): Observable<T> {
        const data = this.getFromCache<T>(key);
    
        if (!data) {
          throw new Error();
        }
    
        if (isPlatformBrowser(this.platformId)) {
          //console.log('get cache', key);
          // Client only code.
          this.transferState.remove(key);
        }
        if (isPlatformServer(this.platformId)) {
          //console.log('we are the server');
          // Server only code.
        }
    
        return from(Promise.resolve<T>(data));
      }
    
      private setCache<T>(key: StateKey<T>, data: T): void {
        return this.transferState.set<T>(key, data);
      }
    
      private getFromCache<T>(key: StateKey<T>): T {
        return this.transferState.get<T>(key, null);
      }
    }
    

    Which is a much better version and now everything works