Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Angular Universal: Using ZoneMacroTaskWrapper to make renderModuleFactory wait for async api calls.
import { Injectable } from '@angular/core';
import { Observable, Observer, Subscription } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class AsyncApiCallHelperService {
taskProcessor: MyAsyncTaskProcessor;
constructor() {
this.taskProcessor = new MyAsyncTaskProcessor();
}
doTask<T>(promise: Promise<T>) {
return <Observable<T>> this.taskProcessor.doTask(promise);
}
}
declare const Zone: any;
export abstract class ZoneMacroTaskWrapper<S, R> {
wrap(request: S): Observable<R> {
return new Observable((observer: Observer<R>) => {
let task;
let scheduled = false;
let sub: Subscription|null = null;
let savedResult: any = null;
let savedError: any = null;
// tslint:disable-next-line:no-shadowed-variable
const scheduleTask = (_task: any) => {
task = _task;
scheduled = true;
const delegate = this.delegate(request);
sub = delegate.subscribe(
res => savedResult = res,
err => {
if (!scheduled) {
throw new Error(
'An http observable was completed twice. This shouldn\'t happen, please file a bug.');
}
savedError = err;
scheduled = false;
task.invoke();
},
() => {
if (!scheduled) {
throw new Error(
'An http observable was completed twice. This shouldn\'t happen, please file a bug.');
}
scheduled = false;
task.invoke();
});
};
// tslint:disable-next-line:no-shadowed-variable
const cancelTask = (_task: any) => {
if (!scheduled) {
return;
}
scheduled = false;
if (sub) {
sub.unsubscribe();
sub = null;
}
};
const onComplete = () => {
if (savedError !== null) {
observer.error(savedError);
} else {
observer.next(savedResult);
observer.complete();
}
};
// MockBackend for Http is synchronous, which means that if scheduleTask is by
// scheduleMacroTask, the request will hit MockBackend and the response will be
// sent, causing task.invoke() to be called.
const _task = Zone.current.scheduleMacroTask(
'ZoneMacroTaskWrapper.subscribe', onComplete, {}, () => null, cancelTask);
scheduleTask(_task);
return () => {
if (scheduled && task) {
task.zone.cancelTask(task);
scheduled = false;
}
if (sub) {
sub.unsubscribe();
sub = null;
}
};
});
}
protected abstract delegate(request: S): Observable<R>;
}
export class MyAsyncTaskProcessor extends
ZoneMacroTaskWrapper<Promise<any>, any> {
constructor() { super(); }
// your public task invocation method signature
doTask(request: Promise<any>): Observable<any> {
// call via ZoneMacroTaskWrapper
return this.wrap(request);
}
// delegated raw implementation that will be called by ZoneMacroTaskWrapper
protected delegate(request: Promise<any>): Observable<any> {
return new Observable<any>((observer: Observer<any>) => {
// calling observer.next / complete / error
request
.then(result => {
observer.next(result);
observer.complete();
}).catch(error => observer.error(error));
});
}
}
@mstfash
Copy link

mstfash commented Jan 1, 2019

Hey could you please tell me how does it work and how do I inject it into my application and use it? thanks

@siegrainwong
Copy link

siegrainwong commented Mar 13, 2019

@heavenchains

from this:

  async get(
    url: string,
    query?: any,
    option?: AxiosRequestConfig
  ): Promise<ResponseResult> {
    return await this.handleRequest(Methods.GET, url, null, query, option);
  }

to this:

  async get(
    url: string,
    query?: any,
    option?: AxiosRequestConfig
  ): Promise<ResponseResult> {
    return new Promise<ResponseResult>(resolve => {
      this.processor
        .doTask(this.handleRequest(Methods.GET, url, null, query, option))
        .subscribe(result => {
          resolve(result);
        });
    });
  }

this.processor is the MyAsyncTaskProcessor injected service.

@BruneXX
Copy link

BruneXX commented Dec 23, 2019

Hi Guys is there another way to implement this? I'm already using something like:

/**
   * read
   *
   * @param  id
   * @return
   */
  public read(id: string): Observable<T> {
    return this.httpClient
      .get(`${this.url}/${this.endpoint}/${id}`)
      .pipe(map((data: any) => this.serializer.fromJson(data) as T));
  }

Thats will difficult my implementation..

@tsherdiwala
Copy link

tsherdiwala commented Dec 30, 2019

It works however, I could see a long deal in loading and sometimes the page time out.

@guilhermehtk
Copy link

guilhermehtk commented Jan 6, 2020

Tks, you saved my day!

@guilhermehtk
Copy link

guilhermehtk commented Jan 6, 2020

@BruneXX

Maybe this will work

  public read(id: string): Observable<T> {
    return this.processor
        .doTask(this.httpClient
      .get(`${this.url}/${this.endpoint}/${id}`)
      .pipe(map((data: any) => this.serializer.fromJson(data) as T)));
  }

Assuming that AsyncApiCallHelperService has been injected as processor into the constructor.

@vroussel35
Copy link

vroussel35 commented Aug 1, 2020

Hi all,

We have just migrated to Angular 9 and it seems the MyAsyncTaskProcessor implementation does not work anymore: we are facing timeout error. It's like the task is always pending and for our SSR implementation, it means our page is never rendered (or to be accurate, the page is rendered after 60 seconds).

We are using the exact same code when testing Angular 8 and Angular 9.

Has anyone of you already faced this issue after the migration to Angular 9 (or Angular 10)?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment