web-dev-qa-db-fra.com

RxJs: interroger jusqu'à ce que l'intervalle soit fait ou que les données correctes soient reçues

Comment exécuter le scénario suivant dans le navigateur avec RxJs:

  • soumettre les données à la file d'attente pour traitement
  • récupérer l'identifiant du travail
  • interroger un autre point de terminaison toutes les 1s jusqu'à ce que le résultat soit disponible ou que 60 secondes se soient écoulées (puis échouer)

Solution intermédiaire que j'ai trouvée:

 Rx.Observable
    .fromPromise(submitJobToQueue(jobData))
    .flatMap(jobQueueData => 
      Rx.Observable
            .interval(1000)
            .delay(5000)
            .map(_ => jobQueueData.jobId)
            .take(55)
    )
    .flatMap(jobId => Rx.Observable.fromPromise(pollQueueForResult(jobId)))
    .filter(result => result.completed)
    .subscribe(
      result => console.log('Result', result),
      error =>  console.log('Error', error)
    );
  1. Existe-t-il un moyen sans variables intermédiaires pour arrêter le chronomètre une fois que les données arrivent ou qu'une erreur se produit? Je peux maintenant introduire de nouveaux observables puis utiliser takeUntil
  2. L'utilisation de flatMap ici est-elle sémantiquement correcte? Peut-être que tout cela devrait être réécrit et non enchaîné avec flatMap?
21
gerasalus

En partant du haut, vous avez la promesse de devenir un observable. Une fois que cela donne une valeur, vous voulez passer un appel une fois par seconde jusqu'à ce que vous receviez une certaine réponse (succès) ou jusqu'à ce qu'un certain laps de temps se soit écoulé. Nous pouvons mapper chaque partie de cette explication sur une méthode Rx:

"Une fois que cela donne une valeur" = map/flatMap (flatMap dans ce cas parce que ce qui vient ensuite sera également observable, et nous devons les aplatir)

"une fois par seconde" = interval

"recevoir une certaine réponse" = filter

"ou" = amb

"un certain temps s'est écoulé" = timer

De là, nous pouvons le reconstituer comme suit:

Rx.Observable
  .fromPromise(submitJobToQueue(jobData))
  .flatMap(jobQueueData =>
    Rx.Observable.interval(1000)
      .flatMap(() => pollQueueForResult(jobQueueData.jobId))
      .filter(x => x.completed)
      .take(1)
      .map(() => 'Completed')
      .amb(
        Rx.Observable.timer(60000)
          .flatMap(() => Rx.Observable.throw(new Error('Timeout')))
      )
  )
  .subscribe(
    x => console.log('Result', x),
    x => console.log('Error', x)
  )
;

Une fois que nous avons obtenu notre résultat initial, nous projetons cela dans une course entre deux observables, une qui produira une valeur lorsqu'elle recevra une réponse réussie, et une qui produira une valeur quand un certain laps de temps se sera écoulé. Le deuxième flatMap existe parce que .throw N'est pas présent sur les instances observables, et la méthode sur Rx.Observable Renvoie un observable qui doit également être aplati.

Il s'avère que le combo amb/timer peut en fait être remplacé par timeout, comme ceci:

Rx.Observable
  .fromPromise(submitJobToQueue(jobData))
  .flatMap(jobQueueData =>
    Rx.Observable.interval(1000)
      .flatMap(() => pollQueueForResult(jobQueueData.jobId))
      .filter(x => x.completed)
      .take(1)
      .map(() => 'Completed')
      .timeout(60000, Rx.Observable.throw(new Error('Timeout')))
  )
  .subscribe(
    x => console.log('Result', x),
    x => console.log('Error', x)
  )
;

J'ai omis le .delay Que vous aviez dans votre échantillon car il n'était pas décrit dans la logique souhaitée, mais il pourrait être adapté trivialement à cette solution.

Donc, pour répondre directement à vos questions:

  1. Dans le code ci-dessus, il n'est pas nécessaire d'arrêter quoi que ce soit manuellement, car le interval sera supprimé au moment où le nombre d'abonnés tombe à zéro, ce qui se produit lorsque le take(1) ou le amb/timeout se termine.
  2. Oui, les deux utilisations de votre original étaient valides, car dans les deux cas, vous projetiez chaque élément d'un observable dans un nouvel observable et vouliez aplatir l'observable résultant des observables en un observable régulier.

Voici le jsbin J'ai jeté ensemble pour tester la solution (vous pouvez Tweak la valeur retournée dans pollQueueForResult pour obtenir le succès/timeout souhaité; les temps ont été divisés par 10 pour des raisons de rapidité essai).

30
Matt Burnell

Une petite optimisation à l'excellente réponse de @ matt-burnell. Vous pouvez remplacer les opérateurs filtre et take par l'opérateur first comme suit

Rx.Observable
  .fromPromise(submitJobToQueue(jobData))
  .flatMap(jobQueueData =>
    Rx.Observable.interval(1000)
      .flatMap(() => pollQueueForResult(jobQueueData.jobId))
      .first(x => x.completed)
      .map(() => 'Completed')
      .timeout(60000, Rx.Observable.throw(new Error('Timeout')))

  )
  .subscribe(
    x => console.log('Result', x),
    x => console.log('Error', x)
  );

De plus, pour les personnes qui ne le savent pas, l'opérateur flatMap est un alias pour mergeMap dans RxJS 5.0.

8
Joe King

Pas votre question, mais j'avais besoin des mêmes fonctionnalités

import { takeWhileInclusive } from 'rxjs-take-while-inclusive'
import { of, interval, race, throwError } from 'rxjs'
import { catchError, timeout, mergeMap, delay, switchMapTo } from 'rxjs/operators'

const defaultMaxWaitTimeMilliseconds = 5 * 1000

function isAsyncThingSatisfied(result) {
  return true
}

export function doAsyncThingSeveralTimesWithTimeout(
  doAsyncThingReturnsPromise,
  maxWaitTimeMilliseconds = defaultMaxWaitTimeMilliseconds,
  checkEveryMilliseconds = 500,
) {
  const subject$ = race(
    interval(checkEveryMilliseconds).pipe(
      mergeMap(() => doAsyncThingReturnsPromise()),
      takeWhileInclusive(result => isAsyncThingSatisfied(result)),
    ),
    of(null).pipe(
      delay(maxWaitTimeMilliseconds),
      switchMapTo(throwError('doAsyncThingSeveralTimesWithTimeout timeout'))
    )
  )

  return subject$.toPromise(Promise) // will return first result satistieble result of doAsyncThingReturnsPromise or throw error on timeout
}

Exemple

// mailhogWaitForNEmails
import { takeWhileInclusive } from 'rxjs-take-while-inclusive'
import { of, interval, race, throwError } from 'rxjs'
import { catchError, timeout, mergeMap, delay, switchMap } from 'rxjs/operators'

const defaultMaxWaitTimeMilliseconds = 5 * 1000

export function mailhogWaitForNEmails(
  mailhogClient,
  numberOfExpectedEmails,
  maxWaitTimeMilliseconds = defaultMaxWaitTimeMilliseconds,
  checkEveryMilliseconds = 500,
) {
  let tries = 0

  const mails$ = race(
    interval(checkEveryMilliseconds).pipe(
      mergeMap(() => mailhogClient.getAll()),
      takeWhileInclusive(mails => {
        tries += 1
        return mails.total < numberOfExpectedEmails
      }),
    ),
    of(null).pipe(
      delay(maxWaitTimeMilliseconds),
      switchMap(() => throwError(`mailhogWaitForNEmails timeout after ${tries} tries`))
    )
  )

  // toPromise returns promise which contains the last value from the Observable sequence.
  // If the Observable sequence is in error, then the Promise will be in the rejected stage.
  // If the sequence is empty, the Promise will not resolve.
  return mails$.toPromise(Promise)
}

// mailhogWaitForEmailAndClean
import { mailhogWaitForNEmails } from './mailhogWaitForNEmails'

export async function mailhogWaitForEmailAndClean(mailhogClient) {
  const mails = await mailhogWaitForNEmails(mailhogClient, 1)

  if (mails.count !== 1) {
    throw new Error(
      `Expected to receive 1 email, but received ${mails.count} emails`,
    )
  }

  await mailhogClient.deleteAll()

  return mails.items[0]
}
1
srghma

Solution réécrite Angular/TypeScript d'en haut:

export interface PollOptions {
  interval: number;
  timeout: number;
}

const OPTIONS_DEFAULT: PollOptions = {
  interval: 5000,
  timeout: 60000
};
@Injectable()
class PollHelper {
  startPoll<T>(
    pollFn: () => Observable<T>, // intermediate polled responses
    stopPollPredicate: (value: T) => boolean, // condition to stop polling
    options: PollOptions = OPTIONS_DEFAULT): Observable<T> {
    return interval(options.interval)
      .pipe(
        exhaustMap(() => pollFn()),
        first(value => stopPollPredicate(value)),
        timeout(options.timeout)
      );
  }
}

Exemple:

pollHelper.startPoll<Response>(
  () => httpClient.get<Response>(...),
  response => response.isDone()
).subscribe(result => {
  console.log(result);
});
0
Felix