Comment exécuter le scénario suivant dans le navigateur avec RxJs:
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)
);
takeUntil
flatMap
ici est-elle sémantiquement correcte? Peut-être que tout cela devrait être réécrit et non enchaîné avec flatMap
?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:
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.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).
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.
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
}
// 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]
}
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);
});