Perform sequential api calls with RxJs? - rxjs

Is there a way in RxJs to perform two api calls where the second requires data from the first and return a combined result as a stream? What I'm trying to do is call the facebook API to get a list of groups and the cover image in various sizes. Facebook returns something like this:
// call to facebook /1234 to get the group 1234, cover object has an
// image in it, but only one size
{ id: '1234', cover: { id: '9999' } }
// call to facebook /9999 to get the image 9999 with an array
// with multiple sizes, omitted for simplicity
{ images: [ <image1>, <image2>, ... ] }
// desired result:
{ id: '1234', images: [ <image1>, <image2>, ... ] }
So I have this:
var result = undefined;
rxGroup = fbService.observe('/1234');
rxGroup.subscribe(group => {
rxImage = fbService.observe(`/${group.cover.id}`);
rxImage.subscribe(images => {
group.images = y;
result = group;
}
}
I want to create a method that accepts a group id and returns an Observable that will have the combined group + images (result here) in the stream. I know I can create my own observable and call the next() function in there where I set 'result' above, but I'm thinking there has to be an rx-way to do this. select/map lets me transform, but I don't know how to shoe-in the results from another call. when/and/then seems promising, but also doesn't look like it supports something like that. I could map and return an observable, but the caller would then have to do two subscribes.

Looks like flatMap is the way to go (fiddle). It is called like subscribe and gives you a value from a stream. You return an observable from that and it outputs the values from all the created observables (one for for each element in the base stream) into the resulting stream.
var sourceGroup = { // result of calling api /1234
id: '1234',
cover: {
id: '9999'
}
};
var sourceCover = { // result of calling api /9999
id: '9999',
images: [{
src: 'image1x80.png'
}, {
src: 'image1x320.png'
}]
};
var rxGroup = Rx.Observable.just(sourceGroup);
var rxCombined = rxGroup.flatMap(group =>
Rx.Observable.just(sourceCover)
.map(images => ({
id: group.id,
images: images.images
}))
)
rxCombined.subscribe(x =>
console.log(JSON.stringify(x, null, 2)));
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/4.1.0/rx.all.min.js"></script>
Result:
{
"id": "1234",
"images": [
{
"src": "image1x80.png"
},
{
"src": "image1x320.png"
}
]
}

You should use concatMap instead of flatMap, it will preserve the order of the source emissions.

Related

Enrich RxJS observable result array with data from another request

Considering the following simplified data structure:
Teacher
{
id: number,
name: string
students?: Student[] // filled with an inner second request
}
Student
{
name: string
}
TeachersResult (response of the first api request with no students inside)
{
teachers: Teacher[]
pagesCount: number // metadata for the pagination
}
My main question is how use RxJS to fill the students-property for every teacher with these two api endpoints:
GET http://localhost:1337/api/teachers
GET http://localhost:1337/api/students/{teacherId}
The first idea was to start with something like this:
getTeachersWithStudents(): Observable<TeachersResult> {
return this.apiService.getTeachers().pipe(
concatMap(teachersResult => {
const studentsObservables$: Observable<Student[]>[] = [];
teachersResult.teachers.foreach(teacher =>
studentsObservables$.push(this.apiService.getStudents(teacher.id)));
// Add students to associated teacher here? Is forkJoin() the right choice?
});
);
}
It feels complicated for me to add every students-result from the second api request to the associated teacher. This is the end result for me I want to achieve:
{
teachers: [
{
id: 1,
name: 'Uncle Bob',
students: [
{ name: 'Alice' },
{ name: 'Caren' }
]
},
{
...
}
],
pagesCount: 42
}
You may try something like this
getTeachersWithStudents(): Observable<TeachersResult> {
return this.apiService.getTeachers().pipe(
// concatMap here is right since you want to continue only after upstream
// has notifies the list of teachers
concatMap(teachersResult => {
const teachers = teachersResult.teachers;
const secondCalls = teachers.map(teacher => {
return this.apiService.getStudents(teacher.id).pipe(
// set the students into the teacher object and return the teacher
// filled with the students
map(students => {
teacher.students = students;
return teacher
})
);
})
// now you have an array of Observables for the second calls, you
// can therefore use forkJoin to execute them in parallel
return forkJoin(secondCalls).pipe(
// add the page number
map(teachersWithStudents => {
return {
teachers: teachersWithStudents,
pagesCount: teachersResult.pagesCount
}
})
)
})
);
}
In this way you are executing all the calls to get the students concurrently.
If you want to limit the concurrency rate, then you can use mergeMap in a slightly more complex stream, something like this
getTeachersWithStudents(): Observable<TeachersResult> {
return this.apiService.getTeachers().pipe(
concatMap(teachersResult => {
// transform the teachers array into a stream of teachers
const teachers = teachersResult.teachers;
return from(teachers).pipe(
// here you use mergeMap with the rate of concurrency desired
// in this example I set it to 10, which means there will be at
// most 10 requests for students on flight at the same time
mergeMap(teacher => {
return this.apiService.getStudents(teacher.id).pipe(
// set the students into the teacher object and return the teacher
// filled with the students
map(students => {
teacher.students = students;
return teacher
})
)
}, 10),
// now we want to repack all the teachers in one array using toArray
toArray(),
// here we create the response desired and return it
map(teachersWithStudents => {
return {
teachers: teachersWithStudents,
pagesCount: teachersResult.pagesCount
}
})
)
})
);
}
This stackblitz shows an example of the above 2 implementations.

Wait for Subscription set Recursively to Complete

I have an array of objects with children and have a need to set a field (hidden) in each of those objects recursively. The value for each is set in a subscription. I want to wait until each item in the array is recursively updated before the subscription is complete.
The hidden field will be set based on roles and permissions derived from another observable. In the example I added a delay to simulate that.
Here's my first pass at it. I'm certain there is a much cleaner way of going about this.
https://codesandbox.io/s/rxjs-playground-hp3wr
// Array structure. Note children.
const navigation = [
{
id: "applications",
title: "Applications",
children: [
{
id: "dashboard",
title: "Dashboard"
},
{
id: "clients",
title: "Clients"
},
{
id: "documents",
title: "Documents",
children: [
{
id: "dashboard",
title: "Dashboard"
},...
]
},
{
id: "reports",
title: "Reports"
},
{
id: "resources",
title: "Resources"
}
]
}
];
In the code sandbox example, looking at the console messages, I get the correct result. However, I would like to avoid having to subscribe in setHidden and recursivelySetHidden. I would also like to avoid using Subject if possible.
Here is my approach:
const roleObservable = timer(1000).pipe(mapTo("**************"));
function populateWithField(o, field, fieldValue) {
if (Array.isArray(o)) {
return from(o).pipe(
concatMap(c => populateWithField(c, field, fieldValue)),
toArray()
);
}
if (o.children) {
return roleObservable.pipe(
tap(role => (fieldValue = role)),
concatMap(role => populateWithField(o.children, field, role)),
map(children => ({
...o,
[field]: fieldValue,
children
}))
);
}
return roleObservable.pipe(
map(role => ({
[field]: role,
...o
}))
);
}
of(navigation)
.pipe(concatMap(o => populateWithField(o, "hidden")))
.subscribe(console.log, e => console.error(e.message));
The main thing to notice is the frequent use of concatMap. It it a higher-order mapping operator which means, among other things, that it will automatically subscribe to/unsubscribe from its inner observable.
What differentiates concatMap from other operators, is that it keeps a buffer of emitted values, which means that it will wait for the current inner observable to complete before subscribing to the next one.
In this case, you'd have to deal with a lot of Observables-of-Observables(higher-order observables), which is why you have to use concatMap every time you encounter a children property. Any child in that property could have their own children property, so you must make sure an Observable contains only first-order Observables.
You can read more about higher-order and first-order observables here.
Here is a CodeSandbox example

RxJS Map array to observable and back to plain object in array

I have an array of objects from which I need to pass each object separately into async method (process behind is handled with Promise and then converted back to Observable via Observable.fromPromise(...) - this way is needed because the same method is used in case just single object is passed anytime; the process is saving objects into database). For example, this is an array of objects:
[
{
"name": "John",
...
},
{
"name": "Anna",
...
},
{
"name": "Joe",,
...
},
{
"name": "Alexandra",
...
},
...
]
Now I have the method called insert which which inserts object into database. The store method from database instance returns newly created id. At the end the initial object is copied and mapped with its new id:
insert(user: User): Observable<User> {
return Observable.fromPromise(this.database.store(user)).map(
id => {
let storedUser = Object.assign({}, user);
storedUser.id = id;
return storedUser;
}
);
}
This works well in case I insert single object. However, I would like to add support for inserting multiple objects which just call the method for single insert. Currently this is what I have, but it doesn't work:
insertAll(users: User[]): Observable<User[]> {
return Observable.forkJoin(
users.map(user => this.insert(user))
);
}
The insertAll method is inserting users as expected (or something else filled up the database with that users), but I don't get any response back from it. I was debugging what is happening and seems that forkJoin is getting response just from first mapped user, but others are ignored. Subscription to insertAll does not do anything, also there is no any error either via catch on insertAll or via second parameter in subscribe to insertAll.
So I'm looking for a solution where the Observable (in insertAll) would emit back an array of new objects with users in that form:
[
{
"id": 1,
"name": "John",
...
},
{
"id": 2,
"name": "Anna",
...
},
{
"id": 3,
"name": "Joe",,
...
},
{
"id": 4,
"name": "Alexandra",
...
},
...
]
I would be very happy for any suggestion pointing in the right direction. Thanks in advance!
To convert from array to observable you can use Rx.Observable.from(array).
To convert from observable to array, use obs.toArray(). Notice this does return an observable of an array, so you still need to .subscribe(arr => ...) to get it out.
That said, your code with forkJoin does look correct. But if you do want to try from, write the code like this:
insertAll(users: User[]): Observable<User[]> {
return Observable.from(users)
.mergeMap(user => this.insert(user))
.toArray();
}
Another more rx like way to do this would be to emit values as they complete, and not wait for all of them like forkJoin or toArray does. We can just omit the toArray from the previous example and we got it:
insertAll(users: User[]): Observable<User> {
return Observable.from(users)
.mergeMap(user => this.insert(user));
}
As #cartant mentioned, the problem might not be in Rx, it might be your database does not support multiple connections. In that case, you can replace the mergeMap with concatMap to make Rx send only 1 concurrent request:
insertAll(users: User[]): Observable<User[]> {
return Observable.from(users)
.concatMap(user => this.insert(user))
.toArray(); // still optional
}

Produce a stream of values with data-driven delays in RxJS

Given an array of objects which contain a message payload and time parameter like this:
var data = [
{ message:"Deliver me after 1000ms", time:1000 },
{ message:"Deliver me after 2000ms", time:2000 },
{ message:"Deliver me after 3000ms", time:3000 }
];
I would like to create an observable sequence which returns the message part of each element of the array and then waits for the corresponding amount of time specified in the object. I'm open to reorganising the data structure of the array if that is necessary.
I've seen Observable.delay but can't see how it could be used with a dynamic value in this way. I'm working in RxJS 5.
You could use delayWhen:
var data = [
{ message:"Deliver me after 1000ms", time:1000 },
{ message:"Deliver me after 2000ms", time:2000 },
{ message:"Deliver me after 3000ms", time:3000 }
];
Rx.Observable
.from(data)
.delayWhen(datum => Rx.Observable.timer(datum.time))
.do(datum => console.log(datum.message))
.subscribe();
<script src="https://unpkg.com/#reactivex/rxjs#5.0.3/dist/global/Rx.js"></script>

Map reduce to count tags

I am developing a web app using Codeigniter and MongoDB.
I am trying to get the map reduce to work.
I got a file document with the below structure. I would like to do a map reduce to
check how many times each tag is being used and output it to the collection files.tags.
{
"_id": {
"$id": "4f26f21f09ab66c1030d0000e"
},
"basic": {
"name": "The filename"
},
"tags": [
"lorry",
"house",
"car",
"bicycle"
],
"updated_at": "2012-02-09 11:08:03"
}
I tried this map reduce command but it does not count each individual tag:
$map = new MongoCode ("function() {
emit({tags: this.tags}, {count: 1});
}");
$reduce = new MongoCode ("function( key , values ) {
var count = 0;
values.forEach(function(v) {
count += v['count'];
});
return {count: count};
}");
$this->mongo_db->command (array (
"mapreduce" => "files",
"map" => $map,
"reduce" => $reduce,
"out" => "files.tags"
)
);
Change your Map function to:
function map(){
if(!this.tags) return;
this.tags.forEach(function(tag){
emit(tag, {count: 1});
});
}
Yea, this map/reduce simply calculate total count of tags.
In mongodb cookbook there is example you are looking for.
You have to emit each tag instead of entire collection of tags:
map = function() {
if (!this.tags) {
return;
}
for (index in this.tags) {
emit(this.tags[index], 1);
}
}
You'll need to call emit once for each tag in the input documents.
MongoDB documentation for example says:
A map function calls emit(key,value) any
number of times to feed data to the reducer. In most cases you will
emit once per input document, but in some cases such as counting tags,
a given document may have one, many, or even zero tags.

Resources