Performing a dynamic quantity of https synchronously using Observable - javascript

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

Related

Handling multiple API calls of a single API call

With my team we are trying to implement a command for a really common operation for the business logic but I'm having issues handling its implementation.
Basically:
We have to retrieve an array of objects (GET).
For each of that objects we have to retrieve (GET) another object inside its father.
For each of that sub-objects (childs) we have to check a condition and if it is the wanted condition we retrieve the child, otherwise we pass null.
Q: How do I handle multiple API calls that depends from a single API call without getting outside the CY chain?
This is my current implementation (doesn't works but kinda explains the wanted logic)
Cypress.Commands.add('myCommand', (sumCriteria: Function, anotherCriteria: Function) => {
// I only retrieve fathers with certain criteria
return cy.request('GET', fathersUrl).its('body').then(fatherObjects => {
return fatherObjects.filter(father => father.childs.length && father.childs.find(sumCriteria))
}).then(filteredFathers => {
filteredFathers.forEach(father => {
// For each father I retrieve a single child
const targetChildId = father.childs.find(sumCriteria).id;
// For each single child I retrieve its data and evaluate if it has the needed criteria
cy.request('GET', `${childsUrl}/${targetChildId}`)
.its('body')
.then(property => anotherCriteria(property))
})
});
})
Thanks in advance!
You almost have the correct pattern, but instead of returning results, put them on the queue.
Cypress does two things to make this work
in a custom command, it waits for any asynchronous commands to resolve
it returns whatever is on the queue at the last evaluation
Cypress.Commands.add('myCommand', (sumCriteria, anotherCriteria) => {
cy.request('GET', fathersUrl)
.its('body')
.then(fatherObjects => {
const filteredFathers = fatherObjects.filter(father => {
return father.childs.find(sumCriteria)
});
const results = []
filteredFathers.forEach(father => {
cy.request('GET', father) // waits for all these to resove
.its('body')
.then(property => anotherCriteria(property))
})
cy.then(() => results) // returns this last queued command
})
})
A reproducible example:
Cypress.Commands.add('myCommand', (sumCriteria, anotherCriteria) => {
const fathersUrl = 'https://jsonplaceholder.typicode.com/todos/1'
cy.request('GET', fathersUrl)
.then(() => {
// simulated url extraction
const filteredFathers = [
'https://jsonplaceholder.typicode.com/todos/2',
'https://jsonplaceholder.typicode.com/todos/3'
]
const results = []
filteredFathers.forEach(father => {
cy.request('GET', father)
.then(res => {
results.push(res.body.id)
})
});
cy.then(() => results)
});
})
cy.myCommand()
.should('deep.eq', [2,3]) // ✅ passes

Use RxJS pipe() to turn array into stream of asynchronous values in Angular

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
}
)
}

Multiple http requests in same flatMap method

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))
)

Is there a way to set state for each iteration of a foreach

I'm working with an API within a React Application and I'm trying to make the API calls come back as one promise.
I'm using the Promise.all() method which is working great.
I'm stuck trying to set the results of two API calls to state with their own name. The promise code is working correctly and I am trying to forEach() or map() over the two sets of data and save them to state with their own name.
I'm sure there is a simple solution but I've been scratching my head for far too long over this!
I've tried searching all the docs for .map and .forEach with no luck!
fetchData(){
this.setState({loading: true})
const urls = ['https://api.spacexdata.com/v3/launches/past', 'https://api.spacexdata.com/v3/launches']
let requests = urls.map(url => fetch(url));
Promise.all(requests)
.then(responses => {
return responses
})
.then(responses => Promise.all(responses.map(r => r.json())))
.then(launches => launches.forEach(obj => {
// I need to set both values to state here
}))
.then(() => this.setState({loading: false}))
}
The API call returns two different arrays. I need to set both arrays to State individually with their own name. Is this possible?
If I understand your question correctly, a better approach might be to avoid iteration altogether (ie the use of forEach(), etc). Instead, consider an approach based on "destructuring syntax", seeing you have a known/fixed number of items in the array that is resolved from the prior promise.
You can make use of this syntax in the following way:
/*
The destructing syntax here assigns the first and second element of
the input array to local variables 'responseFromFirstRequest'
and 'responseFromSecondRequest'
*/
.then(([responseFromFirstRequest, responseFromSecondRequest]) => {
// Set different parts of state based on individual responses
// Not suggesting you do this via two calls to setState() but
// am doing so to explicitly illustrate the solution
this.setState({ stateForFirstRequest : responseFromFirstRequest });
this.setState({ stateForSecondRequest : responseFromSecondRequest });
return responses
})
So, integrated into your existing logic it would look like this:
fetchData() {
this.setState({
loading: true
})
const urls = ['https://api.spacexdata.com/v3/launches/past', 'https://api.spacexdata.com/v3/launches']
const requests = urls.map(url => fetch(url));
Promise.all(requests)
.then(responses => Promise.all(responses.map(r => r.json())))
.then(([responseFromFirstRequest, responseFromSecondRequest]) => {
this.setState({ stateForFirstRequest : responseFromFirstRequest });
this.setState({ stateForSecondRequest : responseFromSecondRequest });
return responses
})
.then(() => this.setState({
loading: false
}))
}
If the two arrays won't interfere with each other in the state, is there a problem with just calling setState in each iteration?
.then(launches => launches.forEach(obj => {
this.setState({ [obj.name]: obj });
}))
If you want to minimise the number of updates then you can create an Object from the two arrays and spread that into the state in one call:
.then(launches => this.setState({
...launches.reduce((obj, launch) => {
obj[launch.name] = launch
return obj
}, {})
}))
forEach also provides the index as the second parameter. Wouldn't something like this work?
launches.forEach((obj, idx) => {
if (idx === 0) {
this.setState('first name', obj);
} else if (idx === 1) {
this.setState('second name', obj);
}
})
Also, this portion literally does nothing..
.then(responses => {
return responses
})
and the Promise.all() here also does nothing.
.then(responses => Promise.all(responses.map(r => r.json())))
should be
.then(responses => responses.map(r => r.json()))

How to combine the results of two observable in angular?

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

Categories

Resources