Chaining two observables http and storage - javascript

I am trying to chain two observables. I'm working with the Ionic2 framework. The first observable tries to get an access token out of a mobile device and then second observable then calls the api with that access token. But for some reason the second observable never gets called even tho I subscribed to the chain.
What am I missing?
this.observableChain = Observable.fromPromise(this.storage.get('accessToken'))
.map(accessToken => this.http.post('http://1234/user/login', {'accessToken': accessToken}))
.subscribe(function(result) {
console.log(result);
}, function(error) {
console.log(error);
});
Thanks in advance.

When chaining observables you need to use flatMap().
Observable.fromPromise(this.storage.get('accessToken'))
.flatMap(accessToken =>
this.http.post('http://1234/user/login', {'accessToken': accessToken})
)
.subscribe(
result => console.log(result),
error => console.log(error)
);
A good explanation of this concept is found here - http://reactivex.io/documentation/operators/flatmap.html

You have subscribed, but there is no such thing as subscribing to the chain. Your first observable emits an observable, which is never subscribed to. Basically you have to do something like this:
obs1.subscribe(obs2 => obs2.subscribe(result => ...))
HTH

Related

Why observable source does not emit values when used in race (or merge) but emits when I manually subscribe to it

I have three observable sources in my code that emit values of the same type.
const setTitle$ = params$.do(
params => this.titleService.setTitle( `${params[1].appname} - ${this.pagename}` )
).switchMap(
() => Observable.of(true)
);
const openDocument$ = params$.switchMap(
params => this.openDocument(params[0].id)
);
const saveDocument$ = params$.switchMap(
params => this.saveDocument(params[0].id)
);
When i use them in race like this
setTitle$.race(
openDocument$,
saveDocument$
).subscribe();
works only setTitle and when i subscribe manually to another two sorces like
const openDocument$ = params$.switchMap(
params => this.openDocument(params[0].id)
).subscribe();
const saveDocument$ = params$.switchMap(
params => this.saveDocument(params[0].id)
).subscribe();
then they work too. Help me understand why it's going on and how to force to work all sources in race, merge, etc.
From the documentation, the .race() operator does this:
The observable to emit first is used.
That is why, you will only get ONE emission, because only one out of the three observables that emits first will get emitted.
What you are looking for is .forkJoin() or .combineLatest().
If you want all the observables to execute in parallel and wait for ALL of them to come back as one observables, use .forkJoin():
Observable
.forkJoin([...setTitle$, openDocument$, saveDocument$])
.subscribe(([setTitle, openDocument, saveDocument]) => {
//do something with your your results.
//all three observables must be completed. If any of it was not completed, the other 2 observables will wait for it
})
If you however wants to listen to every emission of all the observables regardless when they are emitted, use .combineLatest():
Observable
.combineLatest(setTitle$, openDocument$, saveDocument$)
.subscribe(([setTitle, openDocument, saveDocument]) => {
//do something with your your results.
// as long as any of the observables completed, it will be emitted here.
});
Problem was with shared params source.
const params$ = this.route.params.map(
routeParams => {
return {
id: <string>routeParams['id']
};
}
).combineLatest(
this.config.getConfig()
).share();
I have shared it with share operator. But in this article from the first comment to my question i found this:
When using multiple async pipes on streams with default values, the .share() operator might cause problems:
The share() will publish the first value of the stream on the first subscription. The first async pipe will trigger that subscription and get that initial value. The second async pipe however will subscribe after that value has already been emitted and therefore miss that value.
The solution for this problem is the .shareReplay(1) operator, which will keep track of the previous value of the stream. That way all the async pipes will get the last value.
I replaced share() with shareReplay(1) and all sources began emitting values.
const params$ = this.route.params.map(
routeParams => {
return {
id: <string>routeParams['id']
};
}
).combineLatest(
this.config.getConfig()
).shareReplay(1);
Thanks to everyone for help!

Observable - 401 causing forkJoin to error out

I am using forkJoin to make several server requests. This is a pattern I have commonly been using through out my application and it has been working great. However we just started implementing user roles which is done on the backend. I am not sure what is the best practice for implementing roles as I am mostly a front end developer, nonetheless this is the problem I have encountered:
Our application has member and admin member roles.
From each view I must make calls to the backend for both member and admin member roles regardless as roles are not determined on the frontend.
Member data is always returned in for both roles as members and admin members both have personal data.
Requests made for admin data is only returned when the user is an admin. Whenever the user does not have admin access the request returns a 401 error. This is where I am having a problem.
Whenever the call returns a 401, the error method in my subscribe method is invoked and I do not have access to any of the calls that were made including the calls associated to the member data.
In my included code within the forkJoin there are five calls passed into the method. The third and forth call only return data if the user is an admin while the rest of the calls are always returned for either member or admin.
When the user is not an admin the third call returns a 401 and the stream stops and the error handler in my subscribe method is invoked. This is obviously not what I want. I want the stream to continue so I can use the data in the _data method.
I have only been using RXJS for 6 months and am learning. Maybe I should be using a different pattern or maybe there is a way to fix this. Any help with code examples would be greatly appreciated. Below my code example I included another example of code in which I attempted to fix the problem by playing around with catch methods. It didn't work.
My View get method:
private getZone() {
this.spinner.show();
this.zonesService.getZone(this.zoneId)
.map(response => {
this.zone = response['group'];
return this.zone;
})
.flatMap(() => {
return Observable.forkJoin(
this.teamsService.getTeam(this.zone['TeamId']),
this.zonesService.getZoneAssociations(this.zone['id'], '/myDevices'),
this.zonesService.getZoneAssociations(this.zone['id'], '/devices'),
this.zonesService.getZoneAssociations(this.zone['id'], '/groupMembers'),
this.sitesService.getSite(this.zone['SiteId'])
);
})
.subscribe(
_data => {
// data handling...
},
_error => {
// error handling ...
}
);
}
My attempt to fix:
private getZone() {
this.spinner.show();
this.zonesService.getZone(this.zoneId)
.map(response => {
this.zone = response['group'];
return this.zone;
})
.flatMap(() => {
return Observable.forkJoin(
this.teamsService.getTeam(this.zone['TeamId']),
this.zonesService.getZoneAssociations(this.zone['id'], '/myDevices'),
this.zonesService.getZoneAssociations(this.zone['id'], '/devices')
.catch(error => Observable.throw(error)),
this.zonesService.getZoneAssociations(this.zone['id'], '/groupMembers')
.catch(error => Observable.throw(error)),
this.sitesService.getSite(this.zone['SiteId'])
);
})
.subscribe(
_data => {
// data handling...
},
_error => {
// error handling...
}
);
}
Returning Observable.throw will just rethrow the caught error, which will see forkJoin emit the error.
Instead, you could use Observable.of(null) to emit null and then complete, which will see forkJoin emit a null for the observable that emitted the error:
return Observable.forkJoin(
this.teamsService.getTeam(this.zone['TeamId']),
this.zonesService.getZoneAssociations(this.zone['id'], '/myDevices'),
this.zonesService.getZoneAssociations(this.zone['id'], '/devices')
.catch(error => Observable.of(null)),
this.zonesService.getZoneAssociations(this.zone['id'], '/groupMembers')
.catch(error => Observable.of(null)),
this.sitesService.getSite(this.zone['SiteId'])
);
Or, if you wanted to emit the error as a value, you could use Observable.of(error).

Better way to use fork join subsequent to another observable

I have a login process that has fairly complicated login variations and has to be scalable to easily add more in the future. So initially the user is authenticated in the typical manner and a user object is returned. Then I must make additional http calls to get information that will determine the various requirements before the user is granted access to the app. This is done using some of the values returned in the user object. I want to write the code in a way that I can easily add http calls without changing current code so I thought using fork join for the subsequent calls would be good since they can be done in parallel. Below is my working code.
I can easily add new requests to the fork join call and while it doesn't look too bad to me I have been told nested subscriptions is a code smell and typically bad practice. Any ideas on how to do this better would be great.
Thanks.
this.authenticate.login(this.model)
.subscribe(
_data => {
this.subscription = Observable.forkJoin(
this.devicesHttp.getDevicesByMacAddress(this.macAddress),
this.teamsService.getTeamsByUserId(_data['userId'])
);
this.subscription.subscribe(
_data => {
// Check login type and other stuff...
}
);
}
);
For example like this using the concatMap() operator:
this.authenticate.login(this.model)
.concatMap(_data => Observable.forkJoin(
this.devicesHttp.getDevicesByMacAddress(this.macAddress),
this.teamsService.getTeamsByUserId(_data['userId'])
))
.subscribe(_data => {
// Check login type and other stuff...
});
The Observables in forkJoin will run in parallel and forkJoin will wait until they both finish.
Also concatMap() waits until the inner Observable completes and then pushes the result further.
In 2021 this must be written with pipe, stand-alone operators, array in forkJoin and Observer argument in subscribe:
import { concatMap, forkJoin } from 'rxjs';
this.getFirst().pipe(
concatMap(data =>
forkJoin([
this.getSecond(data),
this.getThird(data)
])
)
).subscribe({
next: result => ...,
error: e => ...
});
How about this:
this.authenticate.login(this.model)
.switchMap(data => Observable.forkJoin(
this.devicesHttp.getDevicesByMacAddress(this.macAddress),
this.teamsService.getTeamsByUserId(data['userId'])
))
.subscribe(...,...)

Split observable on error in RxJS

With promises, we can use a variant of .then to split up the chain when an error occurs. Here is an example using fetch
fetch('http://website.com').then(
// Perform some logic
(response) => response.json().then(({ answer }) => `Your answer: ${answer}`),
// Skip json parsing when an error occurs
(error) => 'An error occurred :(',
).then(console.log);
This allows me to skip the response processing logic and only respond to errors that were raised in the original fetch statement. Something similar in RxJS might look like this:
Observable.fromPromise(fetch('http://website.com'))
// if I put .catch here, the result will be piped into flatMap and map
.flatMap(response => response.json())
.map(({ answer }) => `Your answer: ${answer}`)
// if I put .catch here, errors thrown in flatMap and map will also be caught
.subscribe(console.log);
As the comments in the code state, I can't simply put a catch operator in as it doesn't have the same behaviour as my promise chain.
I know I can get it with custom operators involving materialising, or merging an error catching observable with this one, but it all seems like pretty major overkill. Is there a simple way to achieve the promise chain behaviour?
Actually, if I was in your situation I wouldn't be worried about catching errors from flatMap and map. When the source Observable throws an error then it'll be propagated to the observer. So I'd just use an error handler when calling subscribe (otherwise the error is rethrown):
.subscribe(console.log, err => console.log('error:', err));
Note that when an error occurs in the source Observable (a Promise in your case) that it's propagated as error notifications, not as standard next notifications. This means that flatMap() and map() won't have absolutely any effect on the error message. If you used catch() or materialize() that both operators (flatMap and map) would have to be able to handle this type of data (and not throw yet another error).
Anyway you can always use share() or publish() and make two different subscriptions where each handles only one type of signals:
let source = Observable.fromPromise(fetch('http://website.com')).publish();
source
.subscribe(undefined, err => console.log(err));
source
.flatMap(...)
.map(...)
.subscribe(console.log, () => {});
source.connect();
Now I have a separate observer only for errors.
Note that I had to make an empty callback with () => {} so the error will be silently ignored. Also note that when using multicasting (the publish() operator) then the Subject inside might have some specific behavior I should be aware of but maybe it doesn't matter in your use-case.

RxJs Create Observable from resulting Promise

Im fairly new to RxJs and I would like to understand what the best way is to work with Rx in combination with Promises.
What I want to create is a service in Angular that acts much as an event dispatcher pattern and emits an event once a promise is complete. What I also require is that, if there are no (event) subscribers the observable never gets called. The last thing I want to happen is that any subsequent subscribers to the observable get the same result without triggering another request to the server.
I have managed to implement my own solution here:
// ... CountryService code
var COUNTRIES_LOADED = Rx.Observable
.create(function (observer) {
$http
.get('/countries')
.then(function (res) {
observer.onNext(res);
}, function (err) {
observer.onError(err);
})
.finally(function () {
observer.onCompleted();
});
})
.shareReplay();
Now anytime I subscribe a new "listener" to subject the observable will be pulled. Any new subscribers will get the value cached without touching the server again.
So inside my "consumer" (Angular Directive) I would like to do something like this:
// ... countryInput directive code:
COUNTRIES_LOADED.subscribe(function (response) {
// Fill in countries into scope or ctrl
scope.countries = response.countries;
});
Any future subscribers to the COUNTRIES_LOADED observer MUST NOT trigger an $http request. Likewise, if the directive is never included on the page, $http will never get called.
The solution above works, however I am not aware of the potential drawbacks and memory implications of this approach. Is this a valid solution? Is there a better / more appropriate way to achieve this using RxJs?
Many thanks!
Use Rx.Observable.fromPromise(promise)
fromPromise:
Converts a Promises/A+ spec compliant Promise and/or ES2015 compliant
Promise or a factory function which returns said Promise to an
Observable sequence.
example:
var source = Rx.Observable.fromPromise(promise);
var subscription = source.subscribe(
function (x) {
console.log('Next: %s', x);
},
function (err) {
console.log('Error: %s', err);
},
function () {
console.log('Completed');
});
update
rxjs6 method is from
update
As of rxjs6 you can use from()
Did you tried to use the fromPromise() API of rxjs5 ?
Check it's documentation here !
I found the answer here (Just slightly differently named)
rxjs using promise only once on subscribe
So for my example the answer is as simple as:
var loadCountries = function () { return $http.get('/countries'); };
var observable = Rx.Observable.defer(loadCountries).shareReplay();
This is how you can use Observables
Lets say you have a method called getuser(username).
//Returns an observable
getUser(username){
return $http.get(url)
.map(res => res.json());
}
And you can use it as below
getUser.subscribe(res => console.log(response));
BUT if you want to use promises
//Returns an Promise
//Donot forget to import toPromise operator
getUser(username){
return $http.get(url)
.map(res => res.json())
.toPromise();
}
And you can use it as below
getUser.then(res => console.log(response));

Categories