1
votes

So, I've started playing with rxjs and I've got a question regarding the best way to keep my observable live after getting an error from a web service call.

Before showing code, here's my current scenario: an angular component which must load an initial list that is paginated and which can also be filtered by changing the item on the combo. In order to solve this with rxjs, I've thought about merging two observables: one handles the select's change event and the other is used for loading more items. Here's the code I'm using:

const filtro$ = this.estadoPedido.valueChanges.pipe(
    distinctUntilChanged(),
    tap(_ => {
        this._paginaAtual = 0;
        this.existemMais = true;
    }),
    startWith(this.estadoPedido.value),
    map(estado => new DadosPesquisa(this._paginaAtual,
        this._elemsPagina,
        estado,
        false))
);

Whenever the select changes, I end up resetting the global page counter (tap operator) and since I want to have an initial load, I'm also using the startWith operator. Finally, I transform the current state into an object that has all the required values to load the values.

I've also got a Subject which is used whenever the load more items button is clicked:

dataRefresh$ = new Subject<DadosPesquisa>();

And these two observables are merged so that I can have a single path for calling my web service:

this.pedidosCarregados$ = merge(filtro$, this.dataRefresh$).pipe(
    tap(() => this.emChamadaRemota = true),
    switchMap(info => forkJoin(
        of(info),
        this._servicoPedidos.obtemPedidos(this._idInstancia,
                                info.paginaAtual,
                                info.elemsPagina,
                                info.estado)
    )),
    shareReplay(),
    tap(([info, pedidos]) => this.existemMais = pedidos.length === info.elemsPagina),
    scan((todosPedidos, info) => !info[0].addPedidosToExisting ?
        info[1] :
        todosPedidos.concat(info[1]), []),
    tap(() => this.emChamadaRemota = false),
    catchError(erro => {
        this.emChamadaRemota = false;
        this.trataErro(erro);
        this.existemMais = false;
        return of([]);
    })
);

Just a quick recap of what I'm trying to do here...tap is used for setting and cleaning a field that controls the wait spinner (emChamadaRemota) and for controlling if the load more button should be shown (existemMais). I'm using the forkJoin inside the switchMap because I need to access the info about the current search across the pipeline. scan is there because loading more items should add the items to the previous loaded page.

Now, I'm also using an interceptor which is responsible for setting the correct headers and handling typical errors (401, 503, etc) with a retry strategy. Here's the code for the intercept method:

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const headers = this.obtemHeaders();
    const requestClonado = req.clone({ headers });
    return next.handle(requestClonado).pipe(
                    retryWhen(this.retryStrategy()),
                    catchError(err => {
                        console.error(err);
                        let msgErro: string;
                        if(err instanceof HttpErrorResponse && this._servicoAutenticacao.trataErroFimSessao(err)) {
                            msgErro = "A sua sessão terminou. Vai ser redirecionado para a página de login" ;
                        }
                        else if(err.status === 503 ) {
                            msgErro = "O servidor não devolveu uma resposta válida (503).";
                        }
                        else {
                            msgErro = err.error && err.error.message ? err.error.message : "Ocorreu um erro no servidor.";
                        }
                        if(err.status !== 503) {
                            this._logger.adicionaInfoExcecao(msgErro).subscribe();
                        }
                        return throwError(msgErro);
                    }
                ));
} 

Now, the problem: if I get an error on the web service call, everything works out well, but my observable will get "killed"...and that makes sense because the operators should catch the error and "unsubscribe" the stream (at least, that's what I understood from some articles I've read).

I've read some articles that say that the solution is to create an internal observable that never throws and that wraps the observable returned from the web service call. Is this the way to go? If so, can I do it at the interceptor level? Or, in case of error, should I simply rebuild my observable chain (but without starting it automatically with the startWith operator)?

1

1 Answers

0
votes

Well, after some tests, the only way I've managed to make it work (without ditching the retry/catch error propagation I have) is to rebuild the pipeline when an exception is thrown. So, I've moved the creation code into a method:

private setupPipeline(runFirstTime = true) {
  const filtro$ = this.estadoPedido.valueChanges.pipe(
                   distinctUntilChanged(),
                   tap(_ => {
                         this._paginaAtual = 0;
                         this.existemMais = true;
                   }),
                   runFirstTime ? startWith(this.estadoPedido.value) : tap(),
                   map(estado => new DadosPesquisa(this._paginaAtual,
                                                  this._elemsPagina,
                                                  estado,
                                                  false))
                   );
  this.pedidosCarregados$ = merge(filtro$, this.dataRefresh$).pipe( 
       //same as before...
      catchError(erro => {
         this.emChamadaRemota = false;
         this.trataErro(erro);
         this.existemMais = false;
         setTimeout(() => this.setupRxjs(false), 100); // reset pipeline
         return of([]);
    })
}

The method is called from within the init method and from withint he catchError operator. I'm sure there's a better way, but recreating the pipeline allowed me to reuse the code almost as is...