Pesquisa de site

javascript - RXJS Encadear observáveis dependentes sequencialmente, mas obtendo cada emissão para uma barra de progresso


Estou enfrentando um problema e tenho tentado encontrar uma solução usando RxJs, mas não consigo encontrar uma que se encaixe...

  • Eu tenho 3 solicitações REST diferentes, que serão chamadas sequencialmente, e cada uma delas precisa da resposta da anterior como argumento
  • Quero implementar uma barra de progresso, que aumenta à medida que os pedidos têm sucesso

Aqui está o que eu pensei :

  • Vou usar pipes e concatMap() para evitar assinaturas aninhadas e me inscrever em cada requisição quando a anterior estiver concluída.

Considere esta versão muito simplificada. Assuma que cada of representa uma requisição REST bem-sucedida completa (lidaremos com erros mais tarde), e que eu farei trabalhos não mostrados com o parâmetro n...

const request1 = of('success 1').pipe(
  delay(500),
  tap(n => console.log('received ' + n)),
);

const request2 = (n) => of('success 2').pipe(
  delay(1000),
  tap(n => console.log('received ' + n))
);

const request3 = (n) => of('success 3').pipe(
  delay(400),
  tap(n => console.log('received ' + n))
);

request1.pipe(
  concatMap(n => request2(n).pipe(
    concatMap(n => request3(n))
  ))
)

No entanto, quando me inscrevo na última parte do código, receberei apenas a resposta da última requisição, o que é esperado, já que o pipe resolve para isso.

Então, com concatMap(), eu posso encadear minhas chamadas REST dependentes corretamente, mas não consigo acompanhar o progresso.

Embora eu pudesse acompanhar o progresso com bastante facilidade usando inscrições aninhadas, estou me esforçando para evitar isso e usar a melhor forma prática.

Como posso encadear minhas chamadas REST dependentes, mas ainda ser capaz de fazer coisas cada vez que uma chamada tiver sucesso?

Resposta #1

Esta é uma solução generalizada, embora não seja tão simples. Mas ela torna o progresso observável enquanto ainda evita o operador share, que pode introduzir estado inesperado se usado incorretamente.

const chainRequests = (firstRequestFn, ...otherRequestFns) => (
  initialParams
) => {
  return otherRequestFns.reduce(
    (chain, nextRequestFn) =>
      chain.pipe(op.concatMap((response) => nextRequestFn(response))),
    firstRequestFn(initialParams)
  );
};

chainRequests recebe um número variável de funções e retorna uma função que aceita parâmetros iniciais e retorna um observable que concatMaps as funções juntas conforme mostrado manualmente na pergunta. Ele faz isso reduzindo cada função em um valor de acumulação que acontece ser um observable.

Lembre-se, o RxJS nos leva para fora

const chainRequestsWithProgress = (...requestFns) => (initialParams) =>  {
  const progress$ = new Rx.BehaviorSubject(0);
  const wrappedFns = requestFns.map((fn, i) => (...args) =>
    fn(...args).pipe(op.tap(() => progress$.next((i + 1) / requestFns.length)))
  );
  const chain$ = Rx.defer(() => {
    progress$.next(0);
    return chainRequests(...wrappedFns)(initialParams);
  });
  return [chain$, progress$];
};

chainRequestsWithProgress retorna dois observáveis - aquele que eventualmente emite a última resposta, e aquele que emite valores de progresso quando o primeiro observável é assinado. Fazemos isso criando um BehaviorSubject para servir como nosso fluxo de valores de progresso, e envolvendo cada uma de nossas funções de requisição para retornar o mesmo observável que normalmente retornariam, mas também encadeamos com tap para que possa enviar um novo valor de progresso para o BehaviorSubject.

O progresso é zerado a cada inscrição no primeiro observável.

Se você quisesse retornar um único observável que produzisse o estado de progresso, bem como o valor de resultado eventual, você poderia fazer com que chainRequestsWithProgress retornasse em vez disso:

chain$.pipe(
  op.startWith(null),
  op.combineLatest(progress$, (result, progress) => ({ result, progress }))
)

e você terá um observable que emite um objeto representando o progresso em direção ao resultado final, e depois esse resultado em si. Algo para refletir - será que progress$ precisa emitir apenas números?

Aviso

Isso assume que os observáveis de solicitação emitem exatamente um valor.

Resposta #2

A solução mais simples seria ter uma variável de contador de progresso que é atualizada a partir de um toque quando cada resposta retorna.

    let progressCounter = 0;

    request1.pipe(
      tap(_ => progressCounter = 0.33),
      concatMap(n => request2(n).pipe(
        tap(_ => progressCounter = 0.66),
        concatMap(n => request3(n)
          .pipe(tap(_ => progressCounter = 1)))
      ))
    );

Se você quer que o próprio progresso seja observável, então você quer compartilhar os observáveis da solicitação para não fazer solicitações duplicadas e, em seguida, combiná-los para obter o progresso.

Um exemplo de como você pode querer abordar isso pode ser encontrado em: https://www.learnrxjs.io/recipes/progressbar.html