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))
)
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
}
)
}
Please see the code below for reference.
export const listenToHotels = (hotelIds: string[]): Observable < Hotel[] > => {
return new Observable < Hotel[] > ((observer) => {
const hotels: any = [];
hotelIds.forEach((hotelId) => {
let roomingList: any;
return FirestoreCollectionReference.Hotels()
.doc(hotelId)
.onSnapshot(
(doc) => {
roomingList = {
hotelId: doc.id,
...doc.data()
}
as Hotel;
console.log(`roomingList`, roomingList);
hotels.push({
...roomingList
});
},
(error) => observer.error(error)
);
});
//Check for error handling
observer.next(hotels);
console.log('hotels', hotels);
});
};
As you can see I am trying to run a forEach on a hotelId Array and in that firestore listener is being executed. Now I want to save the response and push that into hotels array but it gives me an error object not extensible error.
The thing is observer and console.log('hotels',hotels) run first because of promise being executed at later stage.
Please let me know how can I resolve this issue.
I think you could use map instead of forEach, because you could use await with map
example of using map and await: https://flaviocopes.com/javascript-async-await-array-map/
Check out forkJoin. It takes an array of Observables and subscribes to them at the same time.
This example is written in Angular but the operators should be identical as long as you're using a current version of rxjs.
I'm starting with an array of User objects (users.mock.ts)
Each Object is then map to a single Observable (mapCalls)
I now have an array of Observables that will make HTTP calls. These calls will not be made until subscribed to
forkJoin is then added as a wrapper to the array
You subscribe to that forkJoin. All of the objects will make the call.
When the calls have completed, the logic inside of subscribe() will be run
So in your case:
Map the Hotel Ids to a variable calls. Each item is the Observable logic you posted
You would then run forkJoin(calls).subscribe() to make all the calls
export const listenToHotels = (hotelIds: string[]): Observable<Hotel[]> => {
const hotelsObservable = hotelIds.map((hotelId) => {
let roomingList: any;
return new Observable<Hotel>((observerInside) => {
FirestoreCollectionReference.Hotels()
.doc(hotelId)
.onSnapshot(
(doc) => {
roomingList = { hotelId: doc.id, ...doc.data() } as Hotel;
observerInside.next(roomingList);
},
(error) => observerInside.error(error)
);
});
});
const combinedObservable = combineLatest(hotelsObservable);
return combinedObservable;
//Check for error handling
};
The issue was how I was handling the chained observables(Execution of promise is delayed than a normal code because they will be put in micro-queue first). They need to be handled using zip or combineLatest but latter is much better in this use case as we need the latest values for the observables.
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
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