How to combine two responses in to an array in angular?
Following is making an http post requests to two endpoints. How can I combine both of them an provide a return value as an array?
Like this:
postCombined() {
return combineLatest([this.link1$, this.link2$])
.pipe(
mergeMap(([link1, link2]: [string, string]) => {
return [
this.http.post(link1, values1, { headers }),
this.http.post(link2, values2, { headers }),
];
})
)
.subscribe(console.log);
}
Is my implementation is correct? or do I need to use forkJoin?
forkJoin seems the better option as it allows us to group multiple observables and execute them in parallel, then return only one observable.
mergeMap maintains multiple active inner subscriptions at once, so it’s possible to create a memory leak through long-lived inner subscriptions.
postCombined() {
return combineLatest([this.link1$, this.link2$])
.pipe(
let http1$ = this.http.post(link1, values1, {
headers
}), http2$ = this.http.post(link2, values2, {
headers
})
forkJoin([https1$, http2$])
)
.subscribe(console.log);
}
Taken from this article
Related
So, i am new to RXJS, and i have checked a lot of stackoverflow and documentation before coming here and asking this, but i'm finding a hard time to make my logic work.
I have an Observable that will fetch a collection of documents and return them, and i use the pipe operator to make some changes, like using the map operator to change the object. So far, everything is fine.
The problem is here. Afterward, i need to run an "http request" for every document, in order to get specific data about them ("tags"). The http request is of course made as an Observable too, that needs to get subscribed on to fetch the data. However, the subscription takes some time, and the resulting object afterward doesn't have the required data.
let myFunction.pipe(
// mapping to add missing data needed for the front-end
map((results) => ({
...results,
documents: results._embedded.documents.map((document) => ({
...document,
tags: []
})),
})),
// mapping to loop through each document, and use the observable to get the tags with the document id
map((results) => {
let documents = results.documents.map((document: Document) => {
// get Tags for each document
let tagsToReturn = []
this.getDocumentTags(document.id)
.pipe(
// map function to return only the ids for each document, and not the complete tag object
map((tagsArray) => {
const modifiedTagsArray = tagsArray.map((tagObject: any) => {
if (tagObject !== undefined) {
return tagObject.id
}
})
return modifiedTagsArray
})
)
// the subscription to "actually" get the tags
.subscribe((tagsArray: number[]) => {
// Here the tags are found, but the latter code is executed first
// document.tags = tagsArray
tagsToReturn = tagsArray
})
// console.log(JSON.stringify(document))
// Here the tags are not found yet
console.log(JSON.stringify(tagsToReturn))
return { ...document, tags: tagsToReturn }
})
// I then, normally return the new documents with the tags for each document, but it is empty because the subscribe didn't return yet.
return {
_links: results._links,
page: results.page,
documents: documents,
}
}),
map((results) => {
results.documents.forEach((doc) => {
return this.addObservablesToDocument(doc)
})
return results
})
)
I have tried some solutions with switchmap, forkjoin, concat...etc but it didn't work, or i didn't find the correct way to use them. This is why i'm asking if there is a way to stop or another way to handle this problem.
I have tried using different operators like: mergemap, concat, switchmap to swich to the new request, but afterward, i can't have the global object.
I mostly tried to replicate/readapt this in some ways
By using mergemap combined with forkjoin, i was able to replicate what you were looking for.
Not really sure of how i can explain this, because i'm also not an expert coming to Rxjs, but i used the code from : this stackoverflow answer that i adapted
How i understand it is that, when using mergeMap in the pipe flow, you make sur that everything that get returned there, will be executed by the calling "subscribe()",then the mergeMap returns a forkJoin which is an observable for each document tags
I hope this can help
.pipe(
// mapping to add missing data needed for the front-end
map((results) => ({
...results,
documents: results._embedded.documents.map((document) => ({
...document,
tags: []
})),
})),
/******** Added Code *********/
mergeMap((result: ResultsNew<Document>) => {
let allTags = result._embedded.documents.map((document) =>
this.getDocumentTags(document.id).pipe(
map((tagsArray) => tagsArray.map((tagObject: any) => tagObject.id))
)
)
return forkJoin(...allTags).pipe(
map((idDataArray) => {
result._embedded.documents.forEach((eachDocument, index) => {
eachDocument.tags = idDataArray[index]
})
return {
page: result.page,
_links: result._links,
documents: result._embedded.documents,
}
})
)
}),
/******** Added Code *********/
map((results) => {
results.documents.forEach((doc) => {
return this.addObservablesToDocument(doc)
})
return results
})
)
I have an API (getNewStories) that returns the data as an array of numbers(ids) such as [1,2,3,4...].
There is another API (getItem) that uses the number(id) and give its details.
How can I accomplish this with rxjs operators, so that I should only subscribe to it once and it gives me an array of the records with those ids?
I am able to accomplish this using 2 subscriptions, but I want it with one. Is it possible? and if it's, then, how?
this.hnService.getNewStories().subscribe(data => {
// data is [1,2,3,4,5]
// create an array of observables for all the ids and get the record for that id
const observables = data.map(item => this.hnService.getItem(item));
// use forkJoin to combine the array to single results variable
forkJoin(...observables).subscribe(results => {
this.stories = results;
});
});
with this I have to subscribe to both the APIs.
You were going the right direction with using forkJoin:
this.hnService.getNewStories()
.pipe(
concatMap(data => {
const items$ = data.map(item => this.hnService.getItem(item));
return forkJoin(...items$);
}),
)
.subscribe(allItems => ...);
forkJoin will wait until all source Observables complete and only then emit all results as a single array.
I think you can achieve this using a flattening operator like this.
this.hnService.getNewStories().pipe(
.mergeMap(data => this.hnService.getItem(data))
.subscribe(res => this.stories = res);
Other option can be to create two observable streams and use a combineLatest.
You can simply have implemented as in the the following snippet: (Yes, mergeAll flattens an observable containing an array, for further explanation refer to #Martin's post about the Best way to “flatten” an array inside an RxJS Observable
)
getNewStories().pipe(mergeAll(), concatMap(this.getItem), toArray()).subscribe()
You can try running the following snippet:
const { of } = rxjs;
const { concatMap, toArray, mergeAll } = rxjs.operators;
function getItem(x) {
return of({ item : x })
}
of([1, 2, 3, 4])
.pipe(
mergeAll(),
concatMap(getItem),
toArray()
)
.subscribe(console.log)
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.6.2/rxjs.umd.min.js"></script>
If you look at the picture both arrays consist of same kind of object. first I create it with empty data as placeholder, but second one I create it with data coming from server.
writeValue(v: any) {
console.log('aaa');
console.log(v);
console.log('aaa');
this.form = new FormArray([]);
for (const value of v) {
console.log('bbb');
console.log(value);
console.log('bbb');
this.form.push(new FormControl(value));
}
this.form.valueChanges.subscribe(res => {
if (this.onChange) {
this.onChange(this.form.value);
}
});
}
for first case it goes through all of the writeValue code, for second one it doesn't go through the for(const values of v) code. why is this happening? when I print them out they seem to be the same other than one difference [{...}] vs [] in browser tools.
If you want to see how I create them. the first one is routes and the second one is routeslocal. I put them in angular formcontrol, and thats how it gets to writeValue via controlvalueaccessor. If you want to know how it works you could check my previous question here. there is more code, but it doesn't include the service.
ngOnInit() {
const routes: any[] = [];
routes.push({ ...dataI });
this.requestForm = this.fb.group({
statusId: null,
requestVehicles: this.fb.array([
this.fb.group({
garageId: 0,
routes: new FormControl(routes),
endDateTime: 0,
})
])
});
if (this.data.isEdit) {
this.Title = 'Edit';
this.data.fService.getRequest(this.data.requestId).subscribe(thisRequest => {
this.requestForm = this.fb.group({
statusId: thisRequest.status,
requestVehicles: this.fb.array([
])
});
thisRequest.requestVehicles.forEach((element, index) => {
const routeslocal: any[] = [];
element.routes.forEach((elementt, indexx) => {
this.data.fService.getAddressPoint(elementt).subscribe(sbed => {
const newRoute = {
addressPointId: sbed.addressPointId,
municipalityId: sbed.municipalityId,
regionId: sbed.regionId,
rvId: element.rvId,
sequenceNumber: indexx,
settlementId: sbed.settlementId,
regionName: sbed.regionName,
municipalityName: sbed.municipalityName,
settlementName: sbed.settlementName,
description: sbed.description,
};
routeslocal.push({...newRoute});
});
});
this.requestVehicles.push(this.fb.group({
endDateTime: new Date(element.endDateTime),
garageId: element.garageId,
routes: new FormControl(routeslocal),
}));
});
});
});
});
}
}
The opening line, [] or [{}], is immediately drawn in the console.
In the case of [], there was nothing in the array at logging time, so the browser draw it as an empty array. But the data was present when you looked at it and clicked on the small triangle, later.
You can reproduce this behavior with this code in your console:
;(function(){ let arr=[]; setTimeout(()=>{ arr[0] = {b:3}; }); return arr;})()
So the difference you saw is related to the (a)synchronicity of array filling.
Vato, you has two functions in your service:getRequest(requestId) and getAddressPoint(requestVehicles). The idea is return a whole object. You can create the function in the own service or in the component. I'd like in the service, and that return an objservable. You must use forkJoin and swithMap So . It's for me impossible check if work
**Update, see the stackblitz
getFullRequest(id): Observable<any> {
return this.getRequest(id).pipe(
switchMap((request: any) => {
//here you has the request. We create an array of observables
return forkJoin(
request.requestVehicles.map(
(r: any) => this.getAddressPoint(r))).pipe(map((res: any[]) => {
res.forEach((x: any, index: number) => {
x.sequenceNumber = index
})
return {
statusId: request.statusID,
routes: res
}
})
)
}))
}
then, in your component
if (this.data.isEdit) {
this.Title = 'Edit';
this.data.fService.getFullRequest(this.data.requestId).subscribe(thisRequest => {
this.requestForm = this.fb.group({
statusId: thisRequest.status,
requestVehicles: thisRequest.routes
});
Update 2 briefly explain about switchMap and forkJoin.
When we make this.getRequest(id) we received in request an object. In this object we has in requestVehicles an array (can be an array of objects or an array of numbers -or strings-). With each element of this array we can make a call, But instead of make the calls one to one, we want to make all these together. For this we use forkJoin. forkJoin received an array of observables and, in subscribe received the response in an array
//if we has an observable like:
getValue(id:number):Observable<any>{
return of({one:id})
}
//and an array like
myArray=[1,2]
//and an array of response whe we can store the responses
response:any[]
//we can do
for (let id of myArray)
{
this.getValue(id).susbcribe(res=>{
this.response.push(res)
})
}
//or
observables:any[]
for (let id of myArray)
{
this.observables.push(this.getValue(id))
}
forkJoin(this.observables).subscribe((res;any[])=>{
//in res[0] we have the result of this.getValue(1)
//in res[1] we have the result of this.getValue(2)
//so, simply
this.response=res
})
//or in a compact way
//with each element of the array
observables=myArray.map(x=>this.getValues(x))
forkJoin(this.observables).subscribe((res;any[])=>{
this.response=res
})
Well, there are two problems more. We want add a new propertie "sequenceNumber" to all the response. So we use res.forEach(...) to add the property. And we want return an object with somes properties of our original request (statusID) and in "routes" the array with the response. So we use map to transform the response. In our simple example above
//not return simple {one:1}
//return {id:1,one:1}
getResponse(2).pipe.map(res=>{
return {
id:1,
one:res.one
}
}
In angular app I have an array of literal objects containing a url property.
I need to make a http request for each of these object, but one after another.
example:
let arr = [
{url: 'http://example.com/url1'},
{url: 'http://example.com/url2'},
{url: 'http://example.com/url3'}
]
(that is an example, they may be more of objects, and I don't know how many)
Now, I want to make a request to first url and when we have a response from it (or error, doesn't matter) THEN I want to make a request to second etc. Any idea how to efficiently implement that?
I don't want to make these request at single time - each one should be made only after previous was successful or failure.
A possible solution would be to use concatMap, toArray and switchMapTo.
So first you have a list of urls:
let arr = [{url: 'http://example.com/url1'},{url: 'http://example.com/url2'}]
Then you transform them to an Observable:
of(arr)
.pipe(
concatMap(r=> http.get(r.url)), //MAKE EACH REQUEST AND WAIT FOR COMPLETION
toArray(), // COMBINE THEM TO ONE ARRAY
switchMapTo(http.get("FINALURL") // MAKE REQUEST AFTER EVERY THING IS FINISHED
)).subscribe()
We can use tricky method for this. You have to create method in the service like below.
// TestService.ts
callUrlAsync(url): any[] {
return this._http.get<any>(url);
}
In the component you have to call this method as follows.
//component.ts
let arr = [{url: 'http://example.com/url1'},{url: 'http://example.com/url2'},
{url:'http://example.com/url3'}]
public i = 0;
//trigger method
testMethod(){
this.callUrl(this.arr[0]);
}
callUrl(url){
this.testService.callUrlAsync(url)
.subscribe(data => {
console.log(data);
if(this.arr.length > this.i){
this.i++;
this.callUrl(this.arr[this.i]);
}
}
}, error => {
this.Error(error);
if(this.arr.length > this.i){
this.i++;
this.callUrl(this.arr[this.i]);
}
}
);
}
You can combine your observables with flatMap. Since you have a list of observables (or a list of urls that you want to transform into observables), you can do this with reduce.
Example:
// just some options to make a simple GET without parsing JSON
// and reading the response status
const httpOptions = {
headers: new HttpHeaders({
'Accept': 'text/html, application/xhtml+xml, */*',
'Content-Type': 'application/x-www-form-urlencoded'
}),
responseType: 'text',
observe: 'response'
};
const urls = [
{ url: 'www.google.com' },
{ url: 'www.stackoverflow.com' },
{ url: 'www.imgur.com' },
{ url: 'www.reddit.com' },
];
const reducer = (cur, acc) => acc.pipe(
flatMap(r => cur)
);
const all$ = urls.map(({url}) =>
// create the get requests
http.get(url, httpOptions).pipe(
// do something with the result
tap(r => console.log(r.url + ': ' + r.status))
))
.reverse()
.reduce(reducer);
all$.subscribe(x => {});
User forkJoin
Fork Join
let arr = [{url: 'http://example.com/url1'},{url: 'http://example.com/url2'},{url:
'http://example.com/url3'}]
forkJoin(arr);
Use concatAll() method from rxJs which collect observables and subscribe to next when previous completes. (Or you can use forkJoin for multiple request at single time.)
forkJoin - This will group all your request and execute one by one.
forkJoin waits for each http request to complete and group’s all the observables returned by each http call into a single observable array and finally return that observable array.
It accepts array as parameter. for example -
let response1 = this.http.get(requestUrl1);
let response2 = this.http.get(requestUrl2);
let response3 = this.http.get(requestUrl3);
return Observable.forkJoin([response1, response2, response3]);
For ref. - https://medium.com/#swarnakishore/performing-multiple-http-requests-in-angular-4-5-with-forkjoin-74f3ac166d61
This operator is best used when you have a group of observables and only care about the final emitted value of each.
In another way you can call each request in previous request's error or success block, which is lengthy process.
Use concatMap, make sure to catchError because you don't want the entire chain to fail if one link produced an error.
StackBlitz
let results$ = from(arr).pipe(
concatMap(({ url }) =>
requestData(url).pipe(catchError(err => of(`error from ${url}`)))
)
);
I'm using rxjs to make several http requests and I want to end up with an object that looks something like:
{
100: {
...response from api call...
},
205: {
...response from api call...
},
...etc...
}
Here's what I have so far:
const projectIds = [100, 205, 208, 300]
const source = Rx.Observable
.from(projectIds)
.flatMap(id => get(`projects/${id}/builds`))
.map(response => response['data'])
.zip(projectIds)
.toArray()
source.subscribe(pipelines => {
console.log(pipelines)
})
This gives me back an array of arrays where the first element is the response from the call and the second element is the id of the project.
The problem is that the response doesn't match the project id as the responses come back in different orders depending on which request completes first.
How can I preserve the order (or at least know which projectId goes with each response) while also ending up with an object at the end (currently is an array)?
Option 1
Instread of flatMap you could use concatMap, that should preserve the order.
Note: This won't make any concurrent requests though, if that is what you are looking for.
Option 2
If you want to make concurrent requests (at least from the RxJS side, depending on the browser this could still be limited) you could use some construct using forkJoin like the following:
const projectIds = [100, 205, 208, 300]
const source = Rx.Observable
.from(projectIds)
.map(id => get(`projects/${id}/builds`).pluck('data'))
.toArray()
.switchMap(requestArray => Rx.Observable.forkJoin(requestArray))
.zip(projectIds)
source.subscribe(pipelines => {
console.log(pipelines)
})
Just use the flatMap with elementSelector overload:
.flatMap(
projectId => getProjectDetails(projectId),
(projectId, details) => ({ id: projectId, details })
)
function getProjectDetails(id){
return get(`projects/${id}/builds`)
.map(response => response['data']);
}
This will let you combine the input argument and every output value from flatMap as you require, effectively preserving context. If you require the output order to stay the same you can use .concatMap but then all emissions are done after each other instead of concurrently.
Then finally use a .reduce to combine all objects back to one big emission:
.reduce((acc, curr) => acc[curr.id] = curr.details, {})