How to combine the results of two observable in angular?
this.http.get(url1)
.map((res: Response) => res.json())
.subscribe((data1: any) => {
this.data1 = data1;
});
this.http.get(url2)
.map((res: Response) => res.json())
.subscribe((data2: any) => {
this.data2 = data2;
});
toDisplay(){
// logic about combining this.data1 and this.data2;
}
The above is wrong, because we couldn't get data1 and data2 immediately.
this.http.get(url1)
.map((res: Response) => res.json())
.subscribe((data1: any) => {
this.http.get(url2)
.map((res: Response) => res.json())
.subscribe((data2: any) => {
this.data2 = data2;
// logic about combining this.data1 and this.data2
// and set to this.data;
this.toDisplay();
});
});
toDisplay(){
// display data
// this.data;
}
I can combine the results in the subscribe method of the second observable.
But I'm not sure if it's a good practice to achieve my requirement.
Update:
Another way I found is using forkJoin to combine the results and return a new observable.
let o1: Observable<any> = this.http.get(url1)
.map((res: Response) => res.json())
let o2: Observable<any> = this.http.get(url2)
.map((res: Response) => res.json());
Observable.forkJoin(o1, o2)
.subscribe(val => { // [data1, data2]
// logic about combining data1 and data2;
toDisplay(); // display data
});
toDisplay(){
//
}
A great way to do this is to use the rxjs forkjoin operator (which is included with Angular btw), this keeps you away from nested async function hell where you have to nest function after function using the callbacks.
Here's a great tutorial on how to use forkjoin (and more):
https://coryrylan.com/blog/angular-multiple-http-requests-with-rxjs
In the example you make two http requests and then in the subscribe fat arrow function the response is an array of the results that you can then bring together as you see fit:
let character = this.http.get('https://swapi.co/api/people/1').map(res => res.json());
let characterHomeworld = this.http.get('http://swapi.co/api/planets/1').map(res => res.json());
Observable.forkJoin([character, characterHomeworld]).subscribe(results => {
// results[0] is our character
// results[1] is our character homeworld
results[0].homeworld = results[1];
this.loadedCharacter = results[0];
});
The first element in the array always corresponds to the first http request you pass in, and so on. I used this successfully a few days ago with four simultaneous requests and it worked perfectly.
TRY with forkJoin if it's not working then give this a try combineLatest()
What it do - it combine the last emitted value from your stream array into one before completion of your stream array.
Observable.combineLatest(
this.filesServiceOberserval,
this.filesServiceOberserval,
this.processesServiceOberserval,
).subscribe(
data => {
this.inputs = data[0];
this.outputs = data[1];
this.processes = data[2];
},
err => console.error(err)
);
We can combine observables in different ways based on our need. I had two problems:
The response of first is the input for the second one: flatMap() is
suitable in this case.
Both must finish before proceeding further: forkJoin()/megre()/concat() can be used depending on how you want
your output.
You can find details of all the above functions here.
You can find even more operations that can be performed to combine observables here.
You can merge multiple observables into a single observable and then reduce the values from the source observable into a single value.
const cats = this.http.get<Pet[]>('https://example.com/cats.json');
const dogs = this.http.get<Pet[]>('https://example.com/dogs.json');
const catsAndDogs = merge(cats, dogs).pipe(reduce((a, b) => a.concat(b)));
You can also use mergemap which merges two obserables
Merge map documentation
Related
type Movie = {id: string};
type FullMovie = {id: string, picture: string};
I have a url that returns an array of type Movie:
http.get(url).subscribe(res: Movie[])
I use http.get(movie.id) for each movie in the array returning a FullMovie:
http.get(movie.id).subscribe(res: FullMovie)
so in essence I want to create a method that returns a stream of FullMovie objects, as the requests resolve: getAll = (url): Observable<FullMovie>
getAll = (url): Observable<FullMovie> => {
return http.get(url)
//must pipe the array into a stream of FullMovies but not a stream of FullMovie Observables. I don't want to subscribe to each of the returned FullMovies
//something like
.pipe(//map(array => array.forEach(movie => return http.get(movie.id))))
}
At the moment I have the following solution that works but I want to a more concise solution:
private getFull = (queryGroup: string): Observable<TMDBMovie> =>
new Observable<TMDBMovie>((observer) => {
//get movie array
this.httpGet(queryGroup).subscribe((movies) => {
var j = 0;
if (movies.length === 0) return observer.complete();
//loop through elements
movies.forEach(movie => {
this.getById(movie.id).subscribe(
(res) => complete(observer.next(res)),
(error) => complete()
);
});
}
const complete = (arg: any = 0) => {
if (++j === len) observer.complete();
};
});
});
EDIT:
This works
newGetFull = (queryGroup: string) =>
this.httpGet(queryGroup)
.pipe(concatMap((arr) => from(arr)))
.pipe(
mergeMap((movie) => this.getById(movie.id).pipe(catchError(() => of())))
);
You may want to try something along these lines
getAll = (url): Observable<FullMovie> => {
return http.get(url)
.pipe(
// turn the array Movie[] into a stream of Movie, i.e. an Obsevable<Movie>
concatMap(arrayOfMovies => from(arrayOfMovies)),
// then use mergeMap to "flatten" the various Obaservable<FullMovie> that you get calling http.get(movie.id)
// in other words, with mergeMap, you turn a stream of Observables into a stream of the results returned when each Observable is resolved
mergeMap(movie => http.get(movie.id))
)
}
Consider that using mergeMap as above you do not have guarantee that the final stream will have the same order as the array of Movies you get from the first call. This is because each http.get(movie.id) can take different time to return and therefore the order is not guaranteed.
If you need to guarantee the order, use concatMap rather than mergeMap (actually concatMap is mergeMap with concurrency set to 1).
If you want all the http.get(movie.id) to complete before returning the result, then use forkJoin rather than mergeMap like this
getAll = (url): Observable<FullMovie> => {
return http.get(url)
.pipe(
// turn the array Movie[] into an array of Observable<Movie>
map(arrayOfMovies => arrayOfMovies.map(movie => http.get(movie.id))),
// then use forkJoin to resolve all the Observables in parallel
concatMap(arrayOfObservables => forkJoin(arrayOfObservables))
).subscribe(
arrayOfFullMovies => {
// the result notified by forkJoin is an array of FullMovie objects
}
)
}
getEnrolledPlayers should fetch an array of 'player' objects from the database and then pass it to the matchMaking function. However, it doesn't get passed correctly.
I tried adding observables, playing around with subscriptions
initializeEvent(eventId: string) {
const enrolledPlayers: PlayerStat[] = [];
this.getEnrolledPlayers(eventId)
.subscribe((playerIds: string[]) => {
for (const playerId of playerIds) {
this.dataService.fetchSinglePlayer(playerId)
.subscribe((playerStat: PlayerStat) => enrolledPlayers.push(playerStat));
}
this.matchMaking(enrolledPlayers);
});
}
When I call these series of asynchronous functions, enrolledPlayers[] is calculated correctly (array of 7 elements), but it doesn't get called to the matchMaking() function correctly. I assume it's because of asynchronous runtime.
Yes. It's definitely an issue caused because of the time difference in which the inner subscription resolves a value.
I'd suggest using a forkJoin and waiting on getting all the values resolved before calling matchMaking.
Give this a try:
initializeEvent(eventId: string) {
const enrolledPlayers: PlayerStat[] = [];
this.getEnrolledPlayers(eventId)
.subscribe((playerIds: string[]) => {
const playerInfos$ = playerIds.map(playerId => this.dataService.fetchSinglePlayer(playerId));
forkJoin(...playerInfos$)
.subscribe(enrolledPlayers: PlayerStat[] => this.matchMaking(enrolledPlayers));
});
}
Or with one subscribe
initializeEvent(eventId: string) {
const enrolledPlayers: PlayerStat[] = [];
this.getEnrolledPlayers(eventId)
.take(1)
.switchMap((playerIds: string[]) => {
const playerInfos$ = playerIds.map(playerId => this.dataService.fetchSinglePlayer(playerId).take(1));
return forkJoin(...playerInfos$);
})
.tap(this.matchMaking)
.subscribe();
}
this is a nested subscribe anti pattern... you never nest subscribes, this is how it should look using higher order operators:
initializeEvent(eventId: string) {
this.getEnrolledPlayers(eventId)
.pipe(
switchMap(playerIds =>
forkJoin(playerIds.map(playerId => this.dataService.fetchSinglePlayer(playerId)))
)
).subscribe((enrolledPlayers) =>
this.matchMaking(enrolledPlayers)
);
}
use switchMap to switch into a new observable and then forkJoin to run many observables in parrallel
I have an array of URL's for which i need to perform a http.get sequentially. And, depending on the result, carry on to the next item in the array.
For example:
var myArray = this.myService.getUrls();
// myArray can be something like this:
// ['http://www.us.someurl.com', 'http://www.hk.anotherurl.com']
// Or...
// ['http://www.us.randomurl.com']
// Or...
// ['http://www.us.someurl.com', 'http://www.hk.notaurl.com', 'http://www.pl.someurl.com', 'http://www.in.boringurl.com]
When i perform a http.get on each of these, my server will respond within its data something along the line of {isReady: true} or {isReady: false}
What i want to be able to do it for each item in myArray, perform the http.get, and if the result of that individual call is isReady = true, dont perform the next http.get in the array. If isReady = false, then move on to the next URL in the array.
I have tried flatMap, but its seems i cannot get around hard coding the URLs:
var myArray = ['http://www.us.someurl.com', 'http://www.hk.anotherurl.com'];
this.http.get('http://www.us.someurl.com')
.map(res => res.json())
.flatMap(group => this.http.get('http://www.hk.anotherurl.com') )
.map(res => res.json())
.subscribe((res) => {
console.log(res);
});
});
You can achieve it using a combination of concatMap, filter and take:
const URLS = ['http://www.us.someurl.com', 'http://www.hk.anotherurl.com'];
// ...
public getResponse()
{
return from(URLS).pipe(
concatMap(url => this.http.get<{ isReady: boolean }>(url)),
filter(({ isReady }) => isReady),
take(1),
);
}
DEMO
After some research here I found out that the best way to make multiple requests in Rxjs is flatMap(). However, I can't get it to work when there's mulitple requests inside the flatMap() method aswell. The output in subscribe() is observables instead of values.
this.companyService.getSuggestions(reference)
.pipe(
flatMap((suggestions: string[]) => {
let infos = [];
suggestions.forEach(suggestion => {
infos.push(this.companyService.getInformation(suggestion));
});
return of(infos);
}),
).subscribe((val) => {
console.log('subscribe', val); //Output here is array of observables instead of values
});
flatMap will only wait for one value.
I would convert the suggestions string[] into a stream and do a flatMap over that.
import { flatMap, from } 'rxjs/operators'
const suggestionsRequest$ = this.companyService.getSuggestions(reference)
// convert to suggestion stream
const suggestions$ = suggestionsRequest$.pipe(
flatMap((suggestions: string[]) => from(suggestions))
)
// get all infos
const infos$ = suggestions$.pipe(
flatMap((suggestion: string) => this.companyService.getInformation(suggestion))
)
We have the following stream.
const recorders = imongo.listCollections('recorders')
.flatMapConcat(names => {
const recorders = names
.map(entry => entry.name)
.filter(entry => !_.contains(
['recorders.starts',
'recorders.sources',
'system.indexes',
'system.users'],
entry));
console.log(recorders);
return Rx.Observable.fromArray(recorders);
});
recorders.isEmpty()
.subscribe(
empty => {
if(empty) {
logger.warn('No recorders found.');
}
},
() => {}
);
recorders.flatMapConcat(createRecorderIntervals)
.finally(() => process.exit(0))
.subscribe(
() => {},
e => logger.error('Error while updating: %s', e, {}),
() => logger.info('Finished syncing all recorders')
);
If the stream is empty then we don't want to createRecorderIntervals. The above piece of code is working. However, checking if the stream is empty, is causing the console.log to be executed twice. Why is this happening? Can I fix it somehow?
EDIT: So, I went the following way, after rethinking it thanks to #Martin's answer
const recorders = imongo.listCollections('recorders')
.flatMapConcat(names => {
const recorders = names
.map(entry => entry.name)
.filter(entry => !_.contains(
['recorders.starts',
'recorders.sources',
'system.indexes',
'system.users'],
entry));
if(!recorders.length) {
logger.warn('No recorders found.');
return Rx.Observable.empty();
}
return Rx.Observable.fromArray(recorders);
})
.flatMapConcat(createRecorderIntervals)
.finally(() => scheduleNextRun())
.subscribe(
() => {},
e => logger.error('Error while updating: %s', e, {}),
() => logger.info('Finished syncing all recorders')
);
When you call subscribe() method on an Observable it causes the entire chain of operators to be created which it turn calls imongo.listCollections('recorders') twice in your case.
You can insert an operator before calling flatMapConcat(createRecorderIntervals) that checks whether the result is empty. I have one of them in mind particularly but there might be other that suit your needs even better:
takeWhile() - takes predicate as an argument and emits onComplete when it return false.
Then your code would be like the following:
const recorders = imongo.listCollections('recorders')
.flatMapConcat(names => {
...
return Rx.Observable.fromArray(recorders);
})
.takeWhile(function(result) {
// condition
})
.flatMapConcat(createRecorderIntervals)
.finally(() => process.exit(0))
.subscribe(...);
I don't know what exactly your code does but I hope you get the idea.
Edit: If you want to be notified when the entire Observable is empty than there're a multiple of ways:
do() operator and a custom Observer object. You'll write a custom Observer and put it using do() operator before .flatMapConcat(createRecorderIntervals) . This object will count how many times its next callback was called and when the preceding Observable completes you can tell whether there was at least one or there were no results at all.
create a ConnectableObservable. This one is maybe the most similar to what you we're doing at the beginning. You'll turn your recorders into ConnectableObservable using publish() operator. Then you can subscribe multiple Observers without triggering the operator chain. When you have all your Observers subscribed you call connect() and it'll sequentially emit values to all Observers:
var published = recorders.publish();
published.subscribe(createObserver('SourceA'));
published.subscribe(createObserver('SourceB'));
// Connect the source
var connection = published.connect();
In your case, you'd create two Subjects (because they act as Observable and Observer at the same time) and chain one of them with isEmpty() and the second one with flatMapConcat(). See the doc for more info: http://reactivex.io/documentation/operators/connect.html
I think the first option is actually easier for you.