Loop over typescript Array asynchronously - javascript

I have a typescript array this.products
I need to loop over the elements of the array and for each element send parameters to Angular service which makes an API call and gets an answer to the client as an Observable. However, due to asynchronous nature of Observable, my loop finishes before all of the answer are sent back from the server.
This is my code:
this.products.forEeach((ele, idx) => {
this.myService.getSomeDetails(ele.prop1, ele.prop2).subscribe(result => {
// do something with result
});
});
I need for the loop to advance only after the completion of each observable subscription. How can I implement it? Thanks.

What you are looking for is forkJoin:
https://rxjs-dev.firebaseapp.com/api/index/function/forkJoin
Map your array of items to an array of api call observables, and pass them into forkJoin. This will emit an array of all your resolved api calls.
Quick and dirty example:
forkJoin(this.products.map(i => this.myService.getSomeDetails(ele.prop1, ele.prop2))).subscribe(arrayOfApiCallResults => {
// get results from arrayOfApiCallResults
})

You don't need async/await keywords to make your call in sequence.
import { concat } from 'rxjs';
concat(this.products.map(ele => this.myService.getSomeDetails(ele.prop1, ele.prop2)))
.subscribe(
response => console.log(response),
error => console.log(error),
() => console.log('all calls done')
)

Try this:
let results = await Promise.all(this.products.map(ele =>
this.myService.getSomeDetails(ele.prop1, ele.prop2).toPromise()
));
// put code to process results here
Requests are sent parallel. Rememeber to add async keyword in function definition which use above code. More info here.

This is good way to code it.
from(this.products).pipe(mergeMap(
ele => this.myService.getSomeDetails(ele.prop1, ele.prop2)
)).subscribe(result => {
() => console.log('.'),
e => console.error(e),
() => console.log('Complete')
});

Related

Throwing an error TypeError: Cannot add property 0, object is not extensible

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.

Chained fetches dependent on each other

Take the following scenario:
I need to show in a table a list with all countries and the population of each country. All data can be queried from here: api.population.io.
Thare are 2 api calls that can help me achieve what i want:
http://api.population.io:80/1.0/countries - returns a list of all existing countries
http://api.population.io:80/1.0/population/{$country}/today-and-tomorrow/ - returns the population of a particular country
As you can see i need to make 2 api calls since the second call is dependant of the name of the country made by the first call. I managed to make it work with the initial api call using fetch by using this code:
fetch('http://api.population.io:80/1.0/countries')
.then(results => {
return results.json();
}).then(data => {
//data.countries
})
This just returns me a list with all the countries.
Now i need to loop through data.countries and make a new api call for each country without breaking the whole process. I tried throwing another fetch call where data.countries is available while looping over data.countries but as you can imagine this breaks up the whole process, what i think happens is that the loop doesn't wait for the fetch call to complete thus messing up the query process.
I'm pretty new to this and i've tried googling it but i'm not sure what i can use to achieve what i need. Any help would be truly appreciated. I've been dealing with this problem the whole day
You could fire all the separate population requests at once and use the result when all of them have finished, with the help of Promise.all:
fetch("http://api.population.io:80/1.0/countries")
.then(results => {
return results.json();
})
.then(data => {
const populationPromises = data.countries.map(country => {
return fetch(
`http://api.population.io:80/1.0/population/${country}/today-and-tomorrow/`
).then(results => results.json());
});
return Promise.all(populationPromises);
})
.then(populations => {
console.log(populations);
})
.catch(error => {
console.error(error);
});
The approach with async/await makes the code more coherent and readable:
function getCountries() {
return fetch('http://api.population.io/1.0/countries/?format=json').then(s => s.json())
}
function getPopulation(country) {
return fetch(encodeURI(`http://api.population.io:80/1.0/population/${country}/today-and-tomorrow/?format=json`)).then(s => s.json())
}
(async () => {
try {
const { countries } = await getCountries();
const populations = await Promise.all(countries.map(getPopulation));
console.log(populations);
} catch(err) {
console.log(err);
}
})();

Passing composite data in rxjs observable chains

I have a block of code where I'm calling observables in a chain like so:
getData().flatMap(results => {
return callNextDataMethod(results);
}
.flatMap(results2 => {
// next operation and so forth
})
Now, I understand that flatMap will allow me to pass the results of the previous observable to the next one. However what I need is to both do that as well as pass the results on the first. Let's assume that I do some cleanup, validation, etc on the data that comes back in getData and I want that passed to all flatMap calls down the chain. Is there an operator in rxjs that will do this for me?
Thanks
You can use a map operator to combine the argument received by the flatMap projection function with the observable's result:
getData()
.flatMap(data =>
getMoreData(data).map(moreData => ({ data, moreData }))
)
.flatMap(({ data, moreData }) =>
getEvenMoreData(moreData).map(evenMoreData => ({ data, moreData, evenMoreData }))
)
.flatMap(({ data, moreData, evenMoreData }) =>
...

Using promise after iteration is finished

I am trying to figure out how implement an asynchronous way of executing after an iteration is done
Essentially I have something like this:
data.businesses.map( club => {
axios.get(`${ROOT_URL}/club/${currentEmail}/${club.id}`)
.then( ({data}) => {
placeholder.push( Object.assign({}, club, data))
})
.then( () => ..do extra stuff )
})
// when data.businesses.map is done iterating all of the elements, do something
As you can see, there is some asynchronous issue when retrieving fetching information via axios
I'm still new to the idea of Promises but I am not sure where to apply, if it applicable
You can wait for a collection of promises to resolve (or at least one to reject) using Promise.all().
To gather that collection, you can have the iterator function for .map() return each Promise by removing the braces from the arrow function.
let promisedClubs = data.businesses.map( club =>
axios.get(`${ROOT_URL}/club/${currentEmail}/${club.id}`)
.then( ({data}) => {
placeholder.push( Object.assign({}, club, data))
})
.then( () => ..do extra stuff )
);
Promise.all(promisedClubs).then(clubs => {
// when data.businesses.map is done iterating all of the elements, do something
}, failedClub => {
// error handling
});

rxjs check if stream is empty before handling data

We have the following stream.
const recorders = imongo.listCollections('recorders')
.flatMapConcat(names => {
const recorders = names
.map(entry => entry.name)
.filter(entry => !_.contains(
['recorders.starts',
'recorders.sources',
'system.indexes',
'system.users'],
entry));
console.log(recorders);
return Rx.Observable.fromArray(recorders);
});
recorders.isEmpty()
.subscribe(
empty => {
if(empty) {
logger.warn('No recorders found.');
}
},
() => {}
);
recorders.flatMapConcat(createRecorderIntervals)
.finally(() => process.exit(0))
.subscribe(
() => {},
e => logger.error('Error while updating: %s', e, {}),
() => logger.info('Finished syncing all recorders')
);
If the stream is empty then we don't want to createRecorderIntervals. The above piece of code is working. However, checking if the stream is empty, is causing the console.log to be executed twice. Why is this happening? Can I fix it somehow?
EDIT: So, I went the following way, after rethinking it thanks to #Martin's answer
const recorders = imongo.listCollections('recorders')
.flatMapConcat(names => {
const recorders = names
.map(entry => entry.name)
.filter(entry => !_.contains(
['recorders.starts',
'recorders.sources',
'system.indexes',
'system.users'],
entry));
if(!recorders.length) {
logger.warn('No recorders found.');
return Rx.Observable.empty();
}
return Rx.Observable.fromArray(recorders);
})
.flatMapConcat(createRecorderIntervals)
.finally(() => scheduleNextRun())
.subscribe(
() => {},
e => logger.error('Error while updating: %s', e, {}),
() => logger.info('Finished syncing all recorders')
);
When you call subscribe() method on an Observable it causes the entire chain of operators to be created which it turn calls imongo.listCollections('recorders') twice in your case.
You can insert an operator before calling flatMapConcat(createRecorderIntervals) that checks whether the result is empty. I have one of them in mind particularly but there might be other that suit your needs even better:
takeWhile() - takes predicate as an argument and emits onComplete when it return false.
Then your code would be like the following:
const recorders = imongo.listCollections('recorders')
.flatMapConcat(names => {
...
return Rx.Observable.fromArray(recorders);
})
.takeWhile(function(result) {
// condition
})
.flatMapConcat(createRecorderIntervals)
.finally(() => process.exit(0))
.subscribe(...);
I don't know what exactly your code does but I hope you get the idea.
Edit: If you want to be notified when the entire Observable is empty than there're a multiple of ways:
do() operator and a custom Observer object. You'll write a custom Observer and put it using do() operator before .flatMapConcat(createRecorderIntervals) . This object will count how many times its next callback was called and when the preceding Observable completes you can tell whether there was at least one or there were no results at all.
create a ConnectableObservable. This one is maybe the most similar to what you we're doing at the beginning. You'll turn your recorders into ConnectableObservable using publish() operator. Then you can subscribe multiple Observers without triggering the operator chain. When you have all your Observers subscribed you call connect() and it'll sequentially emit values to all Observers:
var published = recorders.publish();
published.subscribe(createObserver('SourceA'));
published.subscribe(createObserver('SourceB'));
// Connect the source
var connection = published.connect();
In your case, you'd create two Subjects (because they act as Observable and Observer at the same time) and chain one of them with isEmpty() and the second one with flatMapConcat(). See the doc for more info: http://reactivex.io/documentation/operators/connect.html
I think the first option is actually easier for you.

Categories

Resources