web-dev-qa-db-ja.com

RxJs:間隔が完了するまでポーリングするか、正しいデータを受信します

RxJを使用してブラウザーで次のシナリオを実行する方法:

  • キューにデータを送信して処理する
  • ジョブIDを取得する
  • 結果が利用可能になるまで、または60秒が経過するまで1秒ごとに別のエンドポイントをポーリングします(その後失敗します)。

私が思いついた中間ソリューション:

 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. データが到着するかエラーが発生したら、中間変数なしでタイマーを停止する方法はありますか?新しいオブザーバブルを導入してtakeUntilを使用できるようになりました
  2. ここでのflatMapの使用法は意味的に正しいですか?たぶん、この全体を書き直して、flatMapとチェーンしないでください。
21
gerasalus

上から始めて、あなたは観察可能になるという約束を持っています。これにより値が得られたら、特定の応答(成功)を受け取るまで、または特定の時間が経過するまで、1秒に1回呼び出しを行います。この説明の各部分をRxメソッドにマッピングできます。

「これが値を生成したら」= map/flatMap(この場合はflatMapなので、次に来るものも観測可能であり、それらを平坦化する必要があるため)

"1秒あたり1回" = interval

「特定の応答を受け取る」= filter

「または」= amb

「一定の時間が経過しました」= timer

そこから、次のようにつなぎ合わせることができます。

_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)
  )
;
_

最初の結果が得られたら、それを2つのオブザーバブル間の競合に投影します。1つは成功した応答を受け取ったときに値を生成し、もう1つは一定の時間が経過したときに値を生成します。 2番目のflatMapがあるのは、_.throw_がオブザーバブルインスタンスに存在しないためであり、_Rx.Observable_のメソッドはフラット化する必要があるオブザーバブルを返します。

次のように、amb/timerコンボは実際にはtimeoutで置き換えることができることがわかります。

_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)
  )
;
_

サンプルにある_.delay_は、必要なロジックでは説明されていないため省略しましたが、このソリューションに簡単に適合させることができます。

だから、あなたの質問に直接答えるには:

  1. 上記のコードでは、intervalは、サブスクライバーカウントが0になった瞬間に破棄されるため、手動で停止する必要はありません。これは、take(1)またはamb/timeoutが完了しました。
  2. はい、どちらの場合もオブザーバブルの各要素を新しいオブザーバブルに投影しており、オブザーバブルの結果として得られるオブザーバブルを通常のオブザーバブルにフラット化することを望んでいたため、元の使用法はどちらも有効でした。

これがjsbinです 一緒にスローしてソリューションをテストします(pollQueueForResultに返された値を調整して、目的の成功/タイムアウトを取得できます。時間を短縮するために、時間を10で割っていますテスト)。

30
Matt Burnell

@ matt-burnellの優れた回答に対する小さな最適化。次のように、filterおよびtake演算子をfirst演算子に置き換えることができます。

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)
  );

また、知らない人のために、flatMap演算子はRxJS 5.0のmergeMapのエイリアスです。

8
Joe King

あなたの質問ではありませんが、同じ機能が必要でした

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
}

// 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

上からAngular/TypeScriptで書き換えられたソリューション:

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)
      );
  }
}

例:

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