Pass result of one RxJS observable into next sequential observable - javascript

I am trying to do a basic chaining of RxJS HTTP calls where the first call is going to pass the ID of an object that was created server-side into the second call. What I am observing with my implementation is that only the first call is made and the second chained call is never executed.
Below is roughly the code that I have.
First call:
createItem(itemData) {
return this.http.post(createUrl, itemData)
.map((res) => res.json())
.catch(this.handleError);
}
Second call:
addFileToItem(id, file): Observable<any> {
return this.http.post(secondUrl + id, file)
.map((res) => log(res))
.catch(this.handleError);
}
Function defined mapping one call into the other:
createAndUpload(itemData, file): Observable<any> {
return createItem(itemData, file)
.map((res) => {
if (file) {
return addFileToItem(res.id, file);
}
});
}
And finally, where the observable is executed through a subscribe():
someFunction(itemData, file) {
createAndUpload(itemData, file)
.subscribe(res => {
log(res);
//go do something else
};
}

The problem is in createAndUpload where map just turns the result from the first call into an Observable but you never subscribe to it.
So simply said you just need to use mergeMap (or concatMap in this case it doesn't matter) instead of map. The mergeMap operator will subscribe to the inner Observable and emit its result:
createAndUpload(itemData, file): Observable<any> {
return createItem(itemData, file)
.mergeMap((res) => {
if (file) {
return addFileToItem(res.id, file);
}
return Observable.empty(); // you can't return `undefined` from the inner Observable
});
}

Apparently the map() function doesn't actually return anything, meaning that an Observable object is not returned. Instead, a transforming operator such as mergeMap is needed.
Here is the code that ended up working, also using the "newer" .pipe() operator. The only place the code needed to be changed was in the Observable function where I defined the combination of the two separate Observables. In order to access the mergeMap operator, don't forget to import it from rxjs/operators.
For a reason that I will eventually figure out, I couldn't access the mergeMap without the pipe...
createAndUpload(itemData, file): Observable<any> {
return createItem(itemData, file)
.pipe(
mergeMap((res) => {
if (file) {
return addFileToItem(res.id, file);
}
})
)
}

Related

Calling $http.post in batches and chaining promises

I have a use case where I have to call $http.post(request) on batches of the input data.
For this, I created an array of requests. For each of them, I need to get the response of $http.post(), append it to an existing array and pass it to a rendering function. I have to make the next call only when the previous one completes and since $http.post() returns a promise (according to this), I am trying to do this using the reduce function.
function callToHttpPost(request) {
return $http.post('someurl', request);
}
function outerFunc($scope, someId) {
let completeArray = [];
let arrayOfRequests = getRequestInBatches();
arrayOfRequests.reduce((promiseChain, currentRequest) => {
console.log(promiseChain);
return promiseChain.then((previousResponse) => {
completeArray.push.apply(completeArray, previousResponse.data);
render($scope, completeArray, someId);
return callToHttpPost(currentRequest);
});
}, Promise.resolve()).catch(e => errorHandler($scope, e, someId));
}
(I have referred MDN and this answer)
But this gives me TypeError: previousResponse is undefined. The log statement shows the first promise as resolved (since it is the initial value passed to the reduce function), but the other promises show up as rejected due to this error. How can I resolve this?
Using vanilla Javascript
If the outerFunc function can be used in an async context (which it looks like it can, given that it returns nothing and the results are passed to the render function as they are built up), you could clean that right up, paring it down to:
async function outerFunc ($scope, someId) {
const completeArray = [];
try {
for (const request of getRequestInBatches()) {
const { data } = await callToHttpPost(request);
completeArray.push(...data);
render($scope, completeArray, someId);
}
} catch (e) {
errorHandler($scope, e, someId);
}
}
The sequential nature will be enforced by the async/await keywords.
Using RxJS
If you're able to add a dependency on RxJS, your can change the function to:
import { from } from 'rxjs';
import { concatMap, scan } from 'rxjs/operators';
function outerFunc ($scope, someId) {
from(getRequestInBatches()).pipe(
concatMap(callToHttpPost),
scan((completeArray, { data }) => completeArray.concat(...data), [])
).subscribe(
completeArray => render($scope, completeArray, someId),
e => errorHandler($scope, e, someId)
);
}
which revolves around the use of Observable instead of Promise. In this version, the sequential nature is enforced by the concatMap operator, and the complete array of results is reduced and emitted while being built up by the scan operator.
The error was in passing the initial value. In the first iteration of the reduce function, Promise.resolve() returns undefined. This is what is passed as previousResponse. Passing Promise.resolve({ data: [] }) as the initialValue to the reduce function solved the issue.
arrayOfRequests.reduce((promiseChain, currentRequest) => {
console.log(promiseChain);
return promiseChain.then((previousResponse) => {
completeArray.push.apply(completeArray, previousResponse.data);
render($scope, completeArray, someId);
return callToHttpPost(currentRequest);
});
}, Promise.resolve({ data: [] }))
.then(response => {
completeArray.push.apply(completeArray, previousResponse.data);
render($scope, completeArray, someId);
displaySuccessNotification();
})
.catch(e => errorHandler($scope, e, someId));
(edited to handle the final response)

How to wait until for loop which have multiple service calls inside in Angular

I am looking to stop loader only after for loop finish whole execution,
I have a loop in which I am calling metadata service to get all data for the condition checks and based on the condition I am calling save service.
I would like to wait until all save services finish before stopping the loader and showing a common success message and handle the error.
save(data) {
data.forEach(element => {
if(element.isCreatable){
forkJoin(this.http.getMetaData(element)).subscribe(values => {
//condition checks
//20 lines of code to check condition
//if condition fulfills call save service
if (condition) {
forkJoin(this.http.submitData(val)).subscribe(res => {
if (res) {
//success message
//stop loader
}
});
}
});
}
});
}
Update: Thanks BizzyBob, I Tried the answer provided but it is throwing the error "You provided undefined where a stream was expected."
function save1(data) {
forkJoin(
data
.filter(val => val.isCreatable)
.map(e => {
console.log(e); //coming as object
this.http.getMetaData(e).pipe(
switchMap(meta => {
//20 lines of code to check condition
if (condition) {
return this.http.submitData(meta);
} else {
return EMPTY;
}
})
);
})
).subscribe({
complete: () => console.log('stop loader')
});
}
You could create an observable that does both steps; get the metadata and calls submitData() if necessary. For a single element, it would look something like this:
const saveElementIfNecessary$ = getElement().pipe(
switchMap(element => this.http.getMetaData(element)),
switchMap(meta => condition ? this.http.submitData(meta) : EMPTY)
);
saveElementIfNecessary$.subscribe();
Here we use switchMap to transform one observable to another. Notice inside switchMap we pass a function that returns an observable. We do not need to subscribe, as switchMap handles this automatically.
Since you want to only call submitData() when the condition it true, we can simply return EMPTY (which is an observable that immediately completes without emitting anything) when the condition is false.
Since forkJoin takes an array of observables, you can map the array of elements to an array of observables like the above, that do both steps. Then, you can simply turn off your loader after the forkJoin observable completes:
forkJoin(data.map(e => this.http.getMetaData(e).pipe(
switchMap(meta => condition ? this.http.submitData(meta) : EMPTY)
))).subscribe({
complete: () => console.log('stop loader')
});
You can use async and await as follow
async save(data) {
await Promise.all(data.map(async element => {
if (element.isCreatable) {
await this.http.getMetaData(element).pipe(switchMap(values => {
//condition checks
//20 lines of code to check condition
//if condition fulfills call save service
if (condition) {
return this.http.submitData(val).pipe(tap(res => {
if (res) {
//success message
//stop loader
}
}));
}
return EMPTY;
})).toPromise();
}
}));

trigger a synchronous call inside loop wait till two api calls get success then next iteration need to starts in angular 6

Tried with below code not wait for post call success jumping to next iteration before response comes.
Requirement:Need to have next iteration after the success of two api(POST/PATCH) calls
for (item of data) {
A(item)
}
A(value) {
const resp = this.post(url, {
'rationale': value['rationale']
})
.mergeMap(tempObj => {
value['detail'] = tempObj['id']
return this.patch(url, value['extid'], value)
})
.subscribe()
}
Recently I have used the toPromise function with angular http to turn an observable into a promise. If you have the outer loop inside an async function, this may work:
// This must be executed inside an async function
for (item of data) {
await A(item)
}
async A(value) {
const resp = await this.post(url, {
'rationale': value['rationale']
})
.mergeMap(tempObj => {
value['detail'] = tempObj['id']
return this.patch(url, value['extid'], value)
}).toPromise();
}
Use from to emit the items of an array. Use concatMap to map to an Observable and only map to the next one when the previous completed.
const resp$ = from(data).pipe(
concatMap(value => this.post(url, { 'rationale': value['rationale'] }).pipe(
switchMap(tempObj => {
value['detail'] = tempObj['id']
return this.patch(url, value['extid'], value)
})
))
)
I used switchMap instead of mergeMap to indicate that the feature of mergeMap to run multiple Observables simultaneously isn't used.
You could use:
<form (ngSubmit)="submit$.next(form.value)" ...
In your component:
submit$= new Subject();
ngOnInit {
submit$.pipe(
exhaustMap(value =>
this.post(url, {'rationale': value.rationale}))
.pipe(concatMap( response => {
value.detail = response.id;
return this.patch(url, value.extid, value);
}))).subscribe(); // rember to handle unsubcribe
}
The reason I use exhaustMap generally post and path are mutating calls, so that operator ensures first submit is process ignore the rest while processing AKA avoid double submit
An even better way is using ngrx effects, which if you dont know it yet I recomend to learn it
submit$ = createEffect(
() => this.actions$.pipe(
ofType(FeatureActions.submit),
exhaustMap( ({value}) => // if action has value property
this.post(url, { rationale : value.rationale}))
.pipe(concatMap( response => {
value.detail = response.id;
return this.patch(url, value.extid, value);
})),
map(result => FeatureActions.submitSuccess(result))
)
);

Observable for mutiple responses in angular 2

So, I have this service which first calls a function from another module which basically returns an list of urls from an external api. This service then must http.get from all the urls in that list (every url returns a json object of same format) then return a single observable which I can then use in an angular component. Here's what my service code looks like:
import { Injectable } from '#angular/core';
import { Http } from '#angular/http';
import { Client } from 'external-api';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
let client = new Client();
#Injectable()
export class GbDataService {
constructor(private _http: Http) {
}
getGBData(): Observable<any> {
client.fetchUrls("").then(resp=> {
resp.forEach(url => {
//this._http.get(url).map(res => res.json);
// Create a single observable for every http response
});
}).catch(function(err){
console.log(err.message);
});
//return observable
};
}
http.get returns and Observable<Response> type but I couldn't find a way to create and return one Observable for all http.get responses. How can this be done ? Should I create an observable array then push() all the get response I get to the array?
EDIT: It doesn't really matters to me if responses are emitted one by one or all at once BUT there must be only a single Obeservable which emits the responses of all the http.get requests.
Further Edit: This is my fetchURLs method:
fetchURLs(): Promise<any> {
return new Promise((resolve, reject) => {
let _repos: Array<any>;
//scrapeTrendingRepos() is from https://github.com/rhysd/node-github-trend
scraper.scrapeTrendingRepos("").then(repos => {
repos.forEach(repo => {
let url = `https:api.github.com/repos/${repo.owner}/${repo.name}`;
_repos.push(url);
});
resolve(_repos);
}).catch(err => {
reject(err);
});
})
};
Have I implemented Promises in fetchURLs() right??
So, you make a request and get back an array of URLs that you then want to fetch all and get one response from?
Those are the types of things that RxJS excels at.
#Injectable()
export class GbDataService {
constructor(private _http: Http) {
}
getGBData(): Observable<any> {
return Observable
.fromPromise(client.fetchUrls()) // returns Observable<array>
.switchMap( urls => {
// convert your list of urls to a list of http requests
let urlRequests = urls.map( url => http.get(url) );
// combineLatest accepts an array of observables,
// and emits an array of the last results of each of the observables
// but the first emit only happens after every single observable
// has emitted its first result
// TLDR: combineLatest takes an array of Observables
// and will emit an array of those observable emissions // after all have emitted at least once
return Observable.combineLatest(urlRequests);
})
}).catch(function(err){
console.log(err.message);
});
//return observable
};
}
Further info:
Read up on the combineLatest observable. In this scenario, it accomplishes what you want of waiting for all its observable arguments to emit before emitting a single array. But if your observable arguments also emit multiple times, it may not do what you expect and you might want to try a different operator like forkJoin or zip.
Additionally
You might want to use switchMap rather than flatMap - if a new request for urls to fetch comes through, switchMap will cancel any requests currently in flight before sending the new list of requests.
Further Edit
Although your fetchURLs implementation can work in its current incarnation, you can simplify your method a bit if you wish by taking advantage of how promises work. In 'Promise-land', the then handler also returns a Promise, and that second Promise will resolve with whatever value you return from your then handler (this is the basic promise chaining concept). Using that knowledge, you can simplify your method to:
fetchURLs(): Promise<any> {
//scrapeTrendingRepos() is from https://github.com/rhysd/node-github-trend
return scraper.scrapeTrendingRepos("").then(repos => {
// since repos is an array, and you wish to transform each value
// in that array to a new value, you can use Array.map
return repos.map( repo => `https:api.github.com/repos/${repo.owner}/${repo.name}`);
});
}
if client.fetchUrls("") return a native Promise you may want to use snorkpete solution.
if not try to create an observable:
getGBData(): Observable<any> {
return Observable.create(observer => {
client.fetchUrls("").then(resp=> {
resp.forEach(url => {
this._http.get(url).map(res => res.json).subscribe(data=>{
observer.next(data);
});
});
}).catch(function(err){
console.log(err.message);
observer.error(err);
});
});
}

Passing params to mapped observable

I'm fairly new to Observables, Promises, Angular 2, and Javascript.
My question is how do I get a reference to the "item" object here:
getItemTransactions (item: Item): Observable<any> {
// Do some stuff ...
return this.http.post(this.url, body, options)
.map(this.extractData)
.catch(this.handleError);
}
In the mapped extractData helper?
private extractData(res: Response) {
let json = res.json().body
/// How do I assign back to item object here?
item.some_property = json["some_property"]
}
Code come from here:
https://angular.io/docs/ts/latest/guide/server-communication.html#!#extract-data
Map should be used to convert a type to another type. When using the http service you should convert your expected json result to some expected and known type. This can be done using .json() method on the response.
Use subscribe to then do something with the expected result from .json().
All of your actions can be done in the expression body, you do not need separate methods for them. This is a mater of preference so whatever you choose is fine.
See code below.
getItemTransactions (item: Item): Observable<any> {
// Do some stuff ...
return this.http.post(this.url, body, options)
.map((res) => res.json()) // get the data from the json result. This is equivalent to writing {return res.json()}
.subscribe((data) => {
this.doSomethingWithData(data, item); // pass that deserialized json result to something or do something with it in your expression
})
.catch(this.handleError);
}
private doSomethingWithData(data: any, item: Item) {
// Do some stuff ...
item.some_property = data["some_property"];
}
Why would you want to re-assign a method parameter in the first place? You probably want to assign a class property instead (this.item vs item).
But if for some reason you really want to reassign the item param, you could always inline the extractData helper, i.e.:
getItemTransactions(item: Item): Observable<any> {
return this.http.post(this.url, body, options)
.map((res: Response) => {
item = res.json(); // Re-assign some value to `item`
})
.catch(this.handleError);
}
It's probably NOT what you want to do. The usual pattern is to have a function return an observable and subscribe() to that observable somewhere else in your code. You can do the assignment inside the subscribe callback.
This would translate to the following code:
getItemTransactions(item: Item): Observable<any> {
return this.http.post(this.url, body, options)
.map((res: Response) => res.json())
.catch(this.handleError);
}
// Somewhere else in your code
this.getItemTransactions(item: Item).subscribe(data => {
item = data; // for instance
}

Categories

Resources