Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Server does not wait till http call completes before rendering - angular 4 server side rendering

I have gone ahead and implemented angular universal and able to render static part of html via server side rendering. Issue that I face is, API calls are being made and server is rendering the html without waiting for the http call to complete. So, part where my template is dependent on data obtained from api call is not being rendered on server.

Further Info:

I use authentication in node server which serve the index html only if the user is authenticated and sets the cookie on response.

Whenever I make an API call from angular, I also send the cookie as header as the dependent services also validates user with the token. For server side rendering, as the cookie will not be available at server level, I have successfully injected request and pick the cookie for the API call. So, API calls are successful but server is not waiting to render till the promise resolves.

Steps that I have tried with no success:

I have changed my zone version as suggested in this comment https://github.com/angular/universal-starter/issues/181#issuecomment-250177280

Please let me know if any further info is required.

Directing me to a angular universal boilerplate which has http calls involved would help me.

like image 642
Chan15 Avatar asked Nov 07 '17 07:11

Chan15


2 Answers

I had a few issues/concerns with the previous solutions. Here's my solution

  • Works with Promises and Observables
  • Has options for Observables to determine when the task will be finished (eg Completion/Error, First Emit, Other)
  • Has option to warn when a task is taking too long to complete
  • Angular UDK doesn't seem to respect tasks that were initiated outside of the component (eg, by NGXS). This provides an awaitMacroTasks() that may be called from the component to fix it.

Gist

/// <reference types="zone.js" />
import { Inject, Injectable, InjectionToken, OnDestroy, Optional } from "@angular/core";
import { BehaviorSubject, Observable, of, Subject, Subscription } from "rxjs";
import { finalize, switchMap, takeUntil, takeWhile, tap } from "rxjs/operators";

export const MACRO_TASK_WRAPPER_OPTIONS = new InjectionToken<MacroTaskWrapperOptions>("MacroTaskWrapperOptions");

export interface MacroTaskWrapperOptions {
  wrapMacroTaskTooLongWarningThreshold?: number;
}

/*
* These utilities help Angular Universal know when
* the page is done loading by wrapping
* Promises and Observables in ZoneJS Macro Tasks.
*
* See: https://gist.github.com/sparebytes/e2bc438e3cfca7f6687f1d61287f8d72
* See: https://github.com/angular/angular/issues/20520
* See: https://stackoverflow.com/a/54345373/787757
*
* Usage:
*
  ```ts
  @Injectable
  class MyService {
    constructor(private macroTaskWrapper: MacroTaskWrapperService) {}

    doSomething(): Observable<any> {
      return this.macroTaskWrapper.wrapMacroTask("MyService.doSomething", getMyData())
    }
  }

  @Component
  class MyComponent {
    constructor(private macroTaskWrapper: MacroTaskWrapperService) {}

    ngOnInit() {
      // You can use wrapMacroTask here
      this.macroTaskWrapper.wrapMacroTask("MyComponent.ngOnInit", getMyData())

      // If any tasks have started outside of the component use this:
      this.macroTaskWrapper.awaitMacroTasks("MyComponent.ngOnInit");
    }
  }
  ```
*
*/
@Injectable({ providedIn: "root" })
export class MacroTaskWrapperService implements OnDestroy {
  /** Override this value to change the warning time */
  wrapMacroTaskTooLongWarningThreshold: number;

  constructor(@Inject(MACRO_TASK_WRAPPER_OPTIONS) @Optional() options?: MacroTaskWrapperOptions) {
    this.wrapMacroTaskTooLongWarningThreshold =
      options && options.wrapMacroTaskTooLongWarningThreshold != null ? options.wrapMacroTaskTooLongWarningThreshold : 10000;
  }

  ngOnDestroy() {
    this.macroTaskCount.next(0);
    this.macroTaskCount.complete();
  }

  /**
   * Useful for waiting for tasks that started outside of a Component
   *
   * awaitMacroTasks$().subscribe()
   **/
  awaitMacroTasks$(label: string, stackTrace?: string): Observable<number> {
    return this._wrapMacroTaskObservable(
      "__awaitMacroTasks__" + label,
      of(null)
        // .pipe(delay(1))
        .pipe(switchMap(() => this.macroTaskCount))
        .pipe(takeWhile(v => v > 0)),
      null,
      "complete",
      false,
      stackTrace,
    );
  }

  /**
   * Useful for waiting for tasks that started outside of a Component
   *
   * awaitMacroTasks()
   **/
  awaitMacroTasks(label: string, stackTrace?: string): Subscription {
    // return _awaitMacroTasksLogged();
    return this.awaitMacroTasks$(label, stackTrace).subscribe();
  }

  awaitMacroTasksLogged(label: string, stackTrace?: string): Subscription {
    console.error("MACRO START");
    return this.awaitMacroTasks$(label, stackTrace).subscribe(() => {}, () => {}, () => console.error("MACRO DONE"));
  }

  /**
   * Starts a Macro Task for a promise or an observable
   */
  wrapMacroTask<T>(
    label: string,
    request: Promise<T>,
    warnIfTakingTooLongThreshold?: number | null,
    isDoneOn?: IWaitForObservableIsDoneOn<T> | null,
    stackTrace?: string | null,
  ): Promise<T>;
  wrapMacroTask<T>(
    label: string,
    request: Observable<T>,
    warnIfTakingTooLongThreshold?: number | null,
    isDoneOn?: IWaitForObservableIsDoneOn<T> | null,
    stackTrace?: string | null,
  ): Observable<T>;
  wrapMacroTask<T>(
    /** Label the task for debugging purposes */
    label: string,
    /** The observable or promise to watch */
    request: Promise<T> | Observable<T>,
    /** Warn us if the request takes too long. Set to 0 to disable */
    warnIfTakingTooLongThreshold?: number | null,
    /** When do we know the request is done */
    isDoneOn?: IWaitForObservableIsDoneOn<T> | null,
    /** Stack trace to log if the task takes too long */
    stackTrace?: string | null,
  ): Promise<T> | Observable<T> {
    if (request instanceof Promise) {
      return this.wrapMacroTaskPromise(label, request, warnIfTakingTooLongThreshold, stackTrace);
    } else if (request instanceof Observable) {
      return this.wrapMacroTaskObservable(label, request, warnIfTakingTooLongThreshold, isDoneOn, stackTrace);
    }

    // Backup type check
    if ("then" in request && typeof (request as any).then === "function") {
      return this.wrapMacroTaskPromise(label, request, warnIfTakingTooLongThreshold, stackTrace);
    } else {
      return this.wrapMacroTaskObservable(label, request as Observable<T>, warnIfTakingTooLongThreshold, isDoneOn, stackTrace);
    }
  }

  /**
   * Starts a Macro Task for a promise
   */
  async wrapMacroTaskPromise<T>(
    /** Label the task for debugging purposes */
    label: string,
    /** The Promise to watch */
    request: Promise<T>,
    /** Warn us if the request takes too long. Set to 0 to disable */
    warnIfTakingTooLongThreshold?: number | null,
    /** Stack trace to log if the task takes too long */
    stackTrace?: string | null,
  ): Promise<T> {
    // Initialize warnIfTakingTooLongThreshold
    if (typeof warnIfTakingTooLongThreshold !== "number") {
      warnIfTakingTooLongThreshold = this.wrapMacroTaskTooLongWarningThreshold;
    }

    // Start timer for warning
    let hasTakenTooLong = false;
    let takingTooLongTimeout: any = null;
    if (warnIfTakingTooLongThreshold! > 0 && takingTooLongTimeout == null) {
      takingTooLongTimeout = setTimeout(() => {
        hasTakenTooLong = true;
        clearTimeout(takingTooLongTimeout);
        takingTooLongTimeout = null;
        console.warn(
          `wrapMacroTaskPromise: Promise is taking too long to complete. Longer than ${warnIfTakingTooLongThreshold}ms.`,
        );
        console.warn("Task Label: ", label);
        if (stackTrace) {
          console.warn("Task Stack Trace: ", stackTrace);
        }
      }, warnIfTakingTooLongThreshold!);
    }

    // Start the task
    const task: MacroTask = Zone.current.scheduleMacroTask("wrapMacroTaskPromise", () => {}, {}, () => {}, () => {});
    this.macroTaskStarted();

    // Prepare function for ending the task
    const endTask = () => {
      task.invoke();
      this.macroTaskEnded();

      // Kill the warning timer
      if (takingTooLongTimeout != null) {
        clearTimeout(takingTooLongTimeout);
        takingTooLongTimeout = null;
      }

      if (hasTakenTooLong) {
        console.warn("Long Running Macro Task is Finally Complete: ", label);
      }
    };

    // Await the promise
    try {
      const result = await request;
      endTask();
      return result;
    } catch (ex) {
      endTask();
      throw ex;
    }
  }

  /**
   * Starts a Macro Task for an observable
   */
  wrapMacroTaskObservable<T>(
    /** Label the task for debugging purposes */
    label: string,
    /** The observable to watch */
    request: Observable<T>,
    /** Warn us if the request takes too long. Set to 0 to disable */
    warnIfTakingTooLongThreshold?: number | null,
    /** When do we know the request is done */
    isDoneOn?: IWaitForObservableIsDoneOn<T> | null,
    /** Stack trace to log if the task takes too long */
    stackTrace?: string | null,
  ): Observable<T> {
    return this._wrapMacroTaskObservable(label, request, warnIfTakingTooLongThreshold, isDoneOn, true, stackTrace);
  }

  protected _wrapMacroTaskObservable<T>(
    label: string,
    request: Observable<T>,
    warnIfTakingTooLongThreshold?: number | null,
    isDoneOn?: IWaitForObservableIsDoneOn<T> | null,
    isCounted: boolean = true,
    stackTrace?: string | null,
  ): Observable<T> {
    return of(null).pipe(
      switchMap(() => {
        let counts = 0;

        // Determine emitPredicate
        let emitPredicate: (d: T) => boolean;
        if (isDoneOn == null || isDoneOn === "complete") {
          emitPredicate = alwaysFalse;
        } else if (isDoneOn === "first-emit") {
          emitPredicate = makeEmitCountPredicate(1);
        } else if ("emitCount" in isDoneOn) {
          emitPredicate = makeEmitCountPredicate(isDoneOn.emitCount);
        } else if ("emitPredicate" in isDoneOn) {
          emitPredicate = isDoneOn.emitPredicate;
        } else {
          console.warn("wrapMacroTaskObservable: Invalid isDoneOn value given. Defaulting to 'complete'.", isDoneOn);
          emitPredicate = alwaysFalse;
        }

        // Initialize warnIfTakingTooLongThreshold
        if (typeof warnIfTakingTooLongThreshold !== "number") {
          warnIfTakingTooLongThreshold = this.wrapMacroTaskTooLongWarningThreshold;
        }

        /** When task is null it means it hasn't been scheduled */
        let task: MacroTask | null = null;
        let takingTooLongTimeout: any = null;
        let hasTakenTooLong = false;

        /** Function to call when we have determined the request is complete */
        const endTask = () => {
          if (task != null) {
            task.invoke();
            task = null;
            if (hasTakenTooLong) {
              console.warn("Long Running Macro Task is Finally Complete: ", label);
            }
          }

          this.macroTaskEnded(counts);
          counts = 0;

          // Kill the warning timer
          if (takingTooLongTimeout != null) {
            clearTimeout(takingTooLongTimeout);
            takingTooLongTimeout = null;
          }
        };

        /** Used if the task is cancelled */
        const unsubSubject = new Subject();
        function unsub() {
          unsubSubject.next();
          unsubSubject.complete();
        }

        return of(null)
          .pipe(
            tap(() => {
              // Start the task if one hasn't started yet
              if (task == null) {
                task = Zone.current.scheduleMacroTask("wrapMacroTaskObservable", () => {}, {}, () => {}, unsub);
              }
              if (isCounted) {
                this.macroTaskStarted();
                counts++;
              }

              // Start timer for warning
              if (warnIfTakingTooLongThreshold! > 0 && takingTooLongTimeout == null) {
                takingTooLongTimeout = setTimeout(() => {
                  hasTakenTooLong = true;
                  clearTimeout(takingTooLongTimeout);
                  takingTooLongTimeout = null;
                  console.warn(
                    `wrapMacroTaskObservable: Observable is taking too long to complete. Longer than ${warnIfTakingTooLongThreshold}ms.`,
                  );
                  console.warn("Task Label: ", label);
                  if (stackTrace) {
                    console.warn("Task Stack Trace: ", stackTrace);
                  }
                }, warnIfTakingTooLongThreshold!);
              }
            }),
          )
          .pipe(switchMap(() => request.pipe(takeUntil(unsubSubject))))
          .pipe(
            tap(v => {
              if (task != null) {
                if (emitPredicate(v)) {
                  endTask();
                }
              }
            }),
          )
          .pipe(
            finalize(() => {
              endTask();
              unsubSubject.complete();
            }),
          );
      }),
    );
  }

  protected macroTaskCount = new BehaviorSubject(0);

  protected macroTaskStarted(counts: number = 1) {
    const nextTaskCount = this.macroTaskCount.value + counts;
    this.macroTaskCount.next(nextTaskCount);
    // console.log("Macro Task Count + ", counts, " = ", nextTaskCount);
  }
  protected macroTaskEnded(counts: number = 1) {
    const nextTaskCount = this.macroTaskCount.value - counts;
    this.macroTaskCount.next(nextTaskCount);
    // console.log("Macro Task Count - ", counts, " = ", nextTaskCount);
  }
}

export type IWaitForObservableIsDoneOn<T = any> =
  | "complete"
  | "first-emit"
  | { emitCount: number }
  | { emitPredicate: (d: T) => boolean };

// Utilities:

function makeEmitCountPredicate(emitCount: number) {
  let count = 0;
  return () => {
    count++;
    return count >= emitCount;
  };
}

function alwaysFalse() {
  return false;
}
like image 112
sparebytes Avatar answered Sep 29 '22 19:09

sparebytes


I just used Zone directly:

Declare the Zone variable in your component:

declare const Zone: any;

Create a macro task.

const t = Zone.current.scheduleMacroTask (
  i.reference, () => {}, {}, () => {}, () => {}
);

Do your http async call. In the response callback / promise let the macro task know its done:

t.invoke();

The above is the simplest form of the solution. You would obviously need to handle errors and timeouts.

like image 33
Khurram Ali Avatar answered Sep 29 '22 19:09

Khurram Ali