Angular 7 docs provide this example of practical usage of rxjs Observables in implementing an exponential backoff for an AJAX request:
import { pipe, range, timer, zip } from 'rxjs';
import { ajax } from 'rxjs/ajax';
import { retryWhen, map, mergeMap } from 'rxjs/operators';
function backoff(maxTries, ms) {
return pipe(
retryWhen(attempts => range(1, maxTries)
.pipe(
zip(attempts, (i) => i),
map(i => i * i),
mergeMap(i => timer(i * ms))
)
)
);
}
ajax('/api/endpoint')
.pipe(backoff(3, 250))
.subscribe(data => handleData(data));
function handleData(data) {
// ...
}
While I understand the concept of both Observables and backoff, I can’t quite figure out, how exactly retryWhen will calculate time intervals for resubscribing to the source ajax.
Specifically, how do zip, map, and mapMerge work in this setup?
And what’s going to be contained in the attempts object when it’s emitted into retryWhen?
I went through their reference pages, but still can’t wrap my head around this.
I have spent quite some time researching this (for learning purposes) and will try to explain the workings of this code as thoroughly as possible.
First, here’s the original code, annotated:
import { pipe, range, timer, zip } from 'rxjs';
import { ajax } from 'rxjs/ajax';
import { retryWhen, map, mergeMap } from 'rxjs/operators';
function backoff(maxTries, ms) { // (1)
return pipe( // (2)
retryWhen(attempts => range(1, maxTries) // (3)
.pipe(
zip(attempts, (i) => i), // (4)
map(i => i * i), // (5)
mergeMap(i => timer(i * ms)) // (6)
)
)
); // (7)
}
ajax('/api/endpoint')
.pipe(backoff(3, 250))
.subscribe(data => handleData(data));
function handleData(data) {
// ...
}
Easy enough, we’re creating custom backoff operator out of retryWhen operator. We’ll be able to apply this later within pipe function.
In this context, pipe method returns a custom operator.
Our custom operator is going to be a modified retryWhen operator. It takes a function argument. This function is going to be called once — specifically, when this retryWhen is first encountered/invoked. By the way, retryWhen gets into play only when the source observable produces an error. It then prevents error from propagating further and resubscribes to the source. If the source produces a non-error result (whether on first subscription or on a retry), retryWhen is passed over and is not involved.
A few words on attempts. It’s an observable. It is not the source observable. It is created specifically for retryWhen. It has one use and one use only: whenever subscription (or re-subscription) to the source observable results in an error, attempts fires a next. We are given attempts and are free to use it in order to react in some way to each failed subscription attempt to the source observable.
So that’s what we are going to do.
First we create range(1, maxTries), an observable that has an integer for every retry we are willing to perform. range is ready to fire all it’s numbers right then and there, but we have to hold its horses: we only need a new number when another retry happens. So, that’s why we...
... zip it with the attempts. Meaning, marry each emitted value of attempts with a single value of range.
Remember, function we’re currently in is going to be called only once, and at that time, attempts will have only fired next once — for the initial failed subscription. So, at this point, our two zipped observables have produced just one value.
Btw, what are the values of the two observables zipped into one? This function decides that: (i) => i. For clarity it can be written (itemFromRange, itemFromAttempts) => itemFromRange. Second argument is not used, so it’s dropped, and first is renamed into i.
What happens here, is we simply disregard the values fired by attempts, we are only interested in the fact that they are fired. And whenever that happens we pull the next value from range observable...
...and square it. This is for the exponential part of the exponential backoff.
So, now whenever (re-)subscription to source fails, we have an ever increasing integer on our hands (1, 4, 9, 16...). How do we transform that integer into a time delay until next re-subscription?
Remember, this function we are currently inside of, it must return an observable, using attempts as input. This resulting observable is only built once. retryWhen then subscribes to that resulting observable and: retries subscribing to source observable whenever resulting observable fires next; calls complete or error on source observable whenever resulting observable fires those corresponding events.
Long story short, we need to make retryWhen wait a bit. delay operator could maybe be used, but setting up exponential growth of the delay would likely be pain. Instead, mergeMap operator comes into play.
mergeMap is a shortcut for two operators combined: map and mergeAll. map simply converts every increasing integer (1, 4, 9, 16...) into a timer observable which fires next after passed number of milliseconds. mergeAll forces retryWhen to actually subscribe to timer. If that last bit didn’t happen, our resulting observable would just fire next immediately with timer observable instance as value.
At this point, we’ve built our custom observable which will be used by retryWhen to decide when exactly to attempt to re-subscribe to source observable.
As it stands I see two problems with this implementation:
As soon as our resulting observable fires its last next (causing the last attempt to resubscribe), it also immediately fires complete. Unless the source observable returns result very quickly (assuming that the very last retry will be the one that succeeds), that result is going to be ignored.
This is because as soon as retryWhen hears complete from our observable, it calls complete on source, which may still be in the process of making AJAX request.
If all retries were unsuccessful, source actually calls complete instead of more logical error.
To solve both these issues, I think that our resulting observable should fire error at the very end, after giving the last retry some reasonable time to attempt to do its job.
Here’s my implementation of said fix, which also takes into account deprecation of zip operator in latest rxjs v6:
import { delay, dematerialize, map, materialize, retryWhen, switchMap } from "rxjs/operators";
import { concat, pipe, range, throwError, timer, zip } from "rxjs";
function backoffImproved(maxTries, ms) {
return pipe(
retryWhen(attempts => {
const observableForRetries =
zip(range(1, maxTries), attempts)
.pipe(
map(([elemFromRange, elemFromAttempts]) => elemFromRange),
map(i => i * i),
switchMap(i => timer(i * ms))
);
const observableForFailure =
throwError(new Error('Could not complete AJAX request'))
.pipe(
materialize(),
delay(1000),
dematerialize()
);
return concat(observableForRetries, observableForFailure);
})
);
}
I tested this code and it seems to work properly in all cases. I can’t be bothered to explain it in detail right now; I doubt anyone will even read the wall of text above.
Anyway, big thanks to #BenjaminGruenbaum and #cartant for setting me onto right path for wrapping my head around all this.
Here is a different version that can be easily extended/modified:
import { Observable, pipe, throwError, timer } from 'rxjs';
import { mergeMap, retryWhen } from 'rxjs/operators';
export function backoff(maxRetries = 5): (_: Observable<any>) => Observable<any> {
return pipe(
retryWhen(errors => errors.pipe(
mergeMap((error, i) => {
const retryAttempt = i + 1;
if (retryAttempt > maxRetries) {
return throwError(error);
} else {
const waitms = retryAttempt * retryAttempt * 1000;
console.log(`Attempt ${retryAttempt}: retrying in ${waitms}ms`);
return timer(waitms);
}
}),
))
);
};
Ref retryWhen
Related
I'm trying to read files by using bindNodeCallback and fs readdir, stat.
Here's my code:
import { readdir, stat, Stats } from "fs";
import { bindNodeCallback, Observable, of } from "rxjs";
import { catchError, filter, flatMap, map, mergeMap, switchMap, tap } from 'rxjs/operators';
const readDirBindCallback: (path: string) => Observable<string[]> = bindNodeCallback(readdir);
const fileStateBindCallback: (path: string) => Observable<Stats> = bindNodeCallback(stat);
readDirBindCallback('/Users/.../WebstormProjects/copy')
.pipe(
flatMap(x => x),
tap(console.log),
switchMap(status => {
console.log(status);
return fileStateBindCallback('/Users/.../WebstormProjects/copy/' + status);
})
)
.subscribe(result => {
console.log(result)
});
"switchMap" has been called multiple times correctly.
The problem is fileStateBindCallback has been called only once, the final subscibe logged only once.
Why is this happened?
If I subscribe the fileStateBindCallback manually in switchMap block. It would run as I expect. But that is not a good pratice and not fit in my request.
I think the problem is the switchMap operator.
switchMap can only have one active inner observable at a time. If an outer values comes in and there is an active inner obs., it will be unsubscribed and a new one will be created based on the newly arrived value and the provided function to switchMap.
readdir will return an array.
flatMap(arr => arr) will simply emit the array's items separately and synchronously.
Let's assume your directory has N items.
flatMap will emits these items one by one.
So item 1 is passed along and switchMap will create an inner obs (fileStateBindCallback), which involves asynchronous operation.
Then, item 2 is sent, but as this happens synchronously, the current inner observable(that one created due to item 1) handled by switchMap will be unsubscribed and a new one will be created for item 2.
And so forth, until item N finally arrives. Remember that the array's items are emitted synchronously. The item N-1's inner obs. will be unsubscribed and a new one will be created for item N. But since N is the last item in the array, it can't be interrupted by any subsequent emissions, meaning that its inner observable will have the time to emit & complete.
What you might be looking for is concatMap.
When Nth item arrives, it will wait for the current inner observable created due to the N-1th item to complete, before creating a new one based on the Nth item.
So, replacing switchMap with concatMap should do the job. If the order is not important, you can opt for mergeMap.
Suppose I want observable to emit value periodically until another observable emits. So I can use timer and takeUntil to achive that.
But then I want to process every emitted value and stop (error) emitting when some condition becomes true. So I write next piece of code:
const { timer, Subject } = 'rxjs';
const { takeUntil, map } = 'rxjs/operators';
const s = new Subject();
let count = 0;
function processItem(item) {
count++;
return count < 3;
}
const value = "MyValue";
timer(0, 1000).pipe(
takeUntil(s),
map(() => {
if (processItem(value)) {
console.log(`Processing done`);
return value;
} else {
s.next(true);
s.complete();
console.error(`Processing error`);
throw new Error(`Stop pipe`);
}
}),
)
Playground
But instead of getting error I have my Observable completed.
Only if I comment out takeUntil(s) operator, I get error.
Looks like when pipe operator completes, it's value is not emitted immediately, but remembered and emitted at the end of next "iteration" of the pipe, then replaced by new result and so on. And in my situation next iteration, when error should be emitted, is prevented by takeUntil. Question is am I right with that assumption, and if so, why rxjs is designed in that way?
First of all, each Rx chain can emit one error or one complete notification but never both. (See http://reactivex.io/documentation/contract.html section "The Contract Governing Notifications").
takeUntil operator emits complete when it's notification Observable (s in your case) emits any next notification. This means that when s emits the chain will be completed and you never receive any further error notifications.
The last thing and probably most confusing is that everything in RxJS happens synchronously unless you work with time (eg. delay operator) or you specifically use observeOn operator with an asynchronous scheduler. So when you call s.next(true) inside map this next notification is immediately propagated to takeUntil which completes the chain and as I mentioned above, you can receive one error or one complete notification but never both.
It looks like you don't even need to be using takeUntil because if you throw an error inside map it's automatically wrapped and sent further as an error notification (How to throw error from RxJS map operator (angular)) and the chain is disposed automatically so there's no point in trying to complete it after that with takeUntil.
I am currently struggling to wrap my head around angular (2+), the HttpClient and Observables.
I come from a promise async/await background, and what I would like to achieve in angular, is the equivalent of:
//(...) Some boilerplate to showcase how to avoid callback hell with promises and async/await
async function getDataFromRemoteServer() {
this.result = await httpGet(`/api/point/id`);
this.dependentKey = someComplexSyncTransformation(this.result);
this.dependentResult = await httpGet(`/api/point/id/dependent/keys/${this.dependentKey}`);
this.deeplyNestedResult = await httpGet(`/api/point/id/dependen/keys/${this.dependentResult.someValue}`);
}
The best I could come op with in angular is:
import { HttpClient } from `#angular/common/http`;
//(...) boilerplate to set component up.
constructor(private http: HttpClient) {}
// somewhere in a component.
getDataFromRemoteServer() {
this.http.get(`/api/point/id`).subscribe( result => {
this.result = result;
this.dependentKey = someComplexSyncTransformation(this.result);
this.http.get(`/api/point/id/dependent/keys/${this.dependentKey}`).subscribe( dependentResult => {
this.dependentResult = dependentResult;
this.http.get(`/api/point/id/dependen/keys/${this.dependentResult.someValue}`).subscribe( deeplyNestedResult => {
this.deeplyNestedResult = deeplyNestedResult;
});
})
});
}
//...
As you might have noticed, I am entering the Pyramid of Doom with this approach, which I would like to avoid.
So how could I write the angular snippet in a way as to avoid this?
Thx!
Ps: I am aware of the fact that you can call .toPromise on the result of the .get call.
But let's just assume I want to go the total Observable way, for now.
When working with observables, you won't call subscribe very often. Instead, you'll use the various operators to combine observables together, forming a pipeline of operations.
To take the output of one observable and turn it into another, the basic operator is map. This is similar to how you can .map an array to produce another array. For a simple example, here's doubling all the values of an observable:
const myObservable = of(1, 2, 3).pipe(
map(val => val * 2)
);
// myObservable is an observable which will emit 2, 4, 6
Mapping is also what you do to take an observable for one http request, and then make another http request. However, we will need one additional piece, so the following code is not quite right:
const myObservable = http.get('someUrl').pipe(
map(result => http.get('someOtherUrl?id=' + result.id)
)
The problem with this code is that it creates an observable that spits out other observables. A 2-dimensional observable if you like. We need to flatten this down so that we have an observable that spits out the results of the second http.get. There are a few different ways to do the flattening, depending on what order we want the results to be in if multiple observables are emitting multiple values. This is not much of an issue in your case since each of these http observables will only emit one item. But for reference, here are the options:
mergeMap will let all the observables run in whatever order, and outputs in whatever order the values arrive. This has its uses, but can also result in race conditions
switchMap will switch to the latest observable, and cancel old ones that may be in progress. This can eliminate race conditions and ensure you have only the latest data.
concatMap will finish the entirety of the first observable before moving on to the second. This can also eliminate race conditions, but won't cancel old work.
Like i said, it doesn't matter much in your case, but i'd recommend using switchMap. So my little example above would become:
const myObservable = http.get('someUrl').pipe(
switchMap(result => http.get('someOtherUrl?id=' + result.id)
)
Now here's how i can use those tools with your code. In this code example, i'm not saving all the this.result, this.dependentKey, etc:
getDataFromRemoteServer() {
return this.http.get(`/api/point/id`).pipe(
map(result => someComplexSyncTransformation(result)),
switchMap(dependentKey => this.http.get(`/api/point/id/dependent/keys/${dependentKey}`)),
switchMap(dependantResult => this.http.get(`/api/point/id/dependent/keys/${dependentResult.someValue}`)
});
}
// to be used like:
getDataFromRemoteServer()
.subscribe(deeplyNestedResult => {
// do whatever with deeplyNestedResult
});
If its important to you to save those values, then i'd recommend using the tap operator to highlight the fact that you're generating side effects. tap will run some code whenever the observable emits a value, but will not mess with the value:
getDataFromRemoteServer() {
return this.http.get(`/api/point/id`).pipe(
tap(result => this.result = result),
map(result => someComplexSyncTransformation(result)),
tap(dependentKey => this.dependentKey = dependentKey),
// ... etc
});
}
Let's say that I have a stream (observable) with some elements:
--a---b-c---d--
If I have a function that takes one of these elements and returns a promise, like a request, and I do a flatMap with that function, the resulting response stream will be something like this (upper case letters are the responses):
--a---b-c---d--
----A----B---CD
But that means that the request for c will start before the request for b ends. Suppose that I want to avoid the request for c to be performed and to have this as a result:
--a---b-c---d--
----A----B----D
How should I approach this problem?
In the following code, I have a stream that emits after 1, 2, 4 and 7 seconds. I have a request function that takes two seconds to complete. I want the function to be called only with 1, 4 and 7 (not with 2, because the request for 1 hasn't finished yet).
const Rx = require('rx');
const logNext = x => console.log(new Date(), 'Next:', x);
const logError = x => console.log(new Date(), 'Error:', x);
const logCompleted = () => console.log(new Date(), 'Completed.');
Rx.Observable.fromArray([1, 2, 4, 7])
.flatMap(x => Rx.Observable.of(x).delay(x * 1000))
.flatMapFirst(request)
.subscribe(logNext, logError, logCompleted);
function request(x) {
console.log(`Starting request with ${x}`);
return new Promise(resolve => {
setTimeout(
() => {
console.log(`Finishing request with ${x}`);
resolve(x)
},
2000
);
})
}
flatMapFirst produces the correct response stream, but I want to avoid the side-effects produced by calling request(2).
you can try to use flatMapFirst if you use rxjs v4. I could not ascertain that the operator exist in rxjs v5. From the documentation:
The flatMapFirst operator is similar to the flatMap and concatMap
methods described above, however, rather than emitting all of the
items emitted by all of the Observables that the operator generates by
transforming items from the source Observable, flatMapFirst instead
propagates the first Observable exclusively until it completes before
it begins subscribes to the next Observable. Observables that come
before the current Observable completes will be dropped and will not
propagate.
Code could be like this :
source$.flatMapFirst(makeRequest)
What will happen here is that incoming b will lead to the creation of makeRequest(b), and the same for c. However, makeRequest(c) wont ever be subscribed to, meaning the effects it might include in its implementation wont be performed.
If makeRequest itself (i.e. the function, not the observable makeRequest(x) is actually doing some effects to create its observable output, and you want to prevent that, then you can use defer :
source$.flatMapFirst(x => Rx.Observable.defer(() => makeRequest(x)))
You can also review the previous following answers for more examples of use of defer :
What's the difference between two Observables if one is created by defer?
Rxjs, understanding defer
Is there a way to have a hot observable from an EventEmitter (or equivalent available in Angular 2 alpha 46 / RxJS 5 alpha)? i.e. if we subscribe after the value is resolved, it triggers with the previously resolved value. Similar to what we have when always returning the same promise.
Ideally, only using Angular 2 objects (I read somewhere a light RxJS would be embedded later to remove the dependency), otherwise importing RxJS is fine. AsyncSubject seems to match my need, but it is not available in RxJS 5 alpha.
I tried the following, without success (never triggers). Any idea about how to use it?
let emitter = new EventEmitter<MyObj>();
setTimeout(() => {emitter.next(new MyObj());});
this.observable = emitter;
return this.observable.share();
Full plunker here comparing hot and cold
Usecase: reach some async objects only once (for example a series of HTTP calls merged/wrapped in a new EventEmitter), but provide the resolved async object to any service/component subscribing to it, even if they subscribe after it is resolved (the HTTP responses are received).
EDIT: the question is not about how to merge HTTP responses, but how to get a (hot?) observable from EventEmitter or any equivalent available with Angular 2 alpha 46 / RxJS 5 alpha that allows to subscribe after the async result is retrieved/resolved (HTTP is just an example of async origin). myEventEmitter.share() does not work (cf plunker above), although it works with the Observable returned by HTTP (cf plunker from #Eric Martinez). And as of Angular 2 alpha 46, .toRx() method does not exist any more, the EventEmitter is the observable and subject itself.
This is something working well with promises as long as we always return the same promise object. Since we have observers introduced with HTTP Angular 2 services, I would like to avoid mixing promises and observers (and observers are said to be more powerful than promises, so it should allow to do what is easy with promises).
Specs about share() (I haven't found doc for version 5 alpha - version used by Angular 2) - working on the Observable returned by the Angular 2 HTTP service, not working on EventEmitter.
EDIT: clarified why not using the Observable returned by HTTP and added that not using RxJS directly would be even better.
EDIT: changed description: the concern is about multiple subscriptions, not merging the HTTP results.
Thanks!
The functionality you seem to be describing is not that of a cold observable but more than of a Rx.BehaviourSubject. Have a look here for an explanation on Rxjs subjects : https://github.com/Reactive-Extensions/RxJS/blob/master/doc/gettingstarted/subjects.md.
I quote from there :
BehaviourSubject is similar to ReplaySubject, except that it only stored the last value it published. BehaviourSubject also requires a default value upon initialization. This value is sent to observers when no other value has been received by the subject yet. This means that all subscribers will receive a value instantly on subscribe, unless the Subject has already completed.
The Rx.AsyncSubject would be the closest in behaviour to a promise :
AsyncSubject is similar to the Replay and Behavior subjects, however it will only store the last value, and only publish it when the sequence is completed. You can use the AsyncSubject type for situations when the source observable is hot and might complete before any observer can subscribe to it. In this case, AsyncSubject can still provide the last value and publish it to any future subscribers.
Two more comments:
in your plunker : this._coldObservable = emitter.share();. Using share returns a hot observable!
EventEmitter actually extends subject in the first place
UPDATE :
Wrapping an EventEmitter around an Rx.Observable:
function toRx ( eventEmitter ) {
return Rx.Observable.create(function ( observer ) {
eventEmitter.subscribe(function listener ( value ) {observer.onNext(value)});
// Ideally you also manage error and completion, if that makes sense with Angular2
return function () {
/* manage end of subscription here */
};
};
)
}
Once you have that Rx.Observable, you can apply share(), shareReplay(1), anything you want.
My bet is that the Angular team will sooner or later propose a brigding function but if you don't want to wait, you can do it yourself.
ReplaySubject is doing what I was looking for. #robwormald provided a working example on gitter I slightly modified to better demonstrate.
Exposing HTTP response:
import {Injectable} from 'angular2/angular2';
import {Http} from 'angular2/http';
import {ReplaySubject} from '#reactivex/rxjs/dist/cjs/Rx'
#Injectable()
export class PeopleService {
constructor(http:Http) {
this.people = new ReplaySubject(1);
http.get('api/people.json')
.map(res => res.json())
.subscribe(this.people);
}
}
Subscribing multiple times:
// ... annotations
export class App {
constructor(peopleService:PeopleService) {
people.subscribe(v => {
console.log(v)
});
//some time later
setTimeout(() => {
people.subscribe(v => {
console.log(v)
});
people.subscribe(v => {
console.log(v)
});
},2000)
}
}
Full plunker
EDIT: the BehaviorSubject is an alternative. In this usecase, the difference is the initial value, for example if we want to display content from cache before updating with the HTTP response.