Question about asynchronous JavaScript with Promise - javascript

Here I have a function that takes an array of string that contains the user names of github accounts. And this function is going to return an array of user data after resolving. There should be one fetch request per user. and requests shouldn’t wait for each other. So that the data arrives as soon as possible. If there’s no such user, the function should return null in the resulting array.
An example for the input would be ["iliakan", "remy", "no.such.users"], and the expected returned promise after resolving would give us [null, Object, Object], Object being the data that contained info about a user.
Here is my attempt to solve this question.
function getUsers(names) {
return new Promise(resolve => {
const array = [];
const url = "https://api.github.com/users/";
const requests = names.map(name => {
const endpoint = `${url}${name}`;
return fetch(endpoint);
});
Promise.all(requests).then(reponses => {
reponses.forEach(response => {
if (response.status === 200) {
response.json().then(data => {
array.push(data);
});
} else {
array.push(null);
}
});
resolve(array);
});
});
}
It does work, i.e. returning an array [null, Object, Object]. And I thought it fulfilled the requirements I stated above. However, after looking at it closely, I felt like I couldn't fully make sense of it.
My question is, look at where we resolve this array, it resolved immediately after the forEach loop. One thing I don't understand is, why does it contain all three items when some of the items are pushed into it asynchronously after the json() is finished. what I mean is, in the case where response.status === 200, the array is pushed with the data resolved from json(), and I would assume this json() operation should take some time. Since we didn't resolve the array after json() operation is finished, how come we still ended up with all data resolved from json()?
Promise.all(requests).then(reponses => {
reponses.forEach(response => {
if (response.status === 200) {
response.json().then(data => {
array.push(data); <--- this should take some time
});
} else {
array.push(null);
}
});
resolve(array); <--- resolve the array immediately after the `forEach` loop
});
});
It looks to me like the array we get should only have one null in it since at the time it is revolved, the .json() should not be finished

You're right, the result is pushed later into the array.
Try to execute this:
const test = await getUsers(['Guerric-P']);
console.log(test.length);
You'll notice it displays 0. Before the result is pushed into the array, its length is 0. You probably think it works because you click on the array in the console, after the result has arrived.
You should do something like this:
function getUsers(names) {
const array = [];
const url = "https://api.github.com/users/";
const requests = names.map(name => {
const endpoint = `${url}${name}`;
return fetch(endpoint);
});
return Promise.all(requests).then(responses => Promise.all(responses.map(x => x.status === 200 ? x.json() : null)));
};

You should avoid using the Promise constructor directly. Here, we don't need to use it at all.
const url = "https://api.github.com/users/";
const getUsers = names =>
Promise.all(names.map(name =>
fetch(url + name).then(response =>
response.status === 200 ? response.json() : null)));
getUsers(["iliakan", "remy", "no.such.users"]).then(console.log);
The Promise constructor should only be used when you're creating new kinds of asynchronous tasks. In this case, you don't need to use the Promise constructor because fetch already returns a promise.
You also don't need to maintain an array and push to it because Promise.all resolves to an array. Finally, you don't need to map over the result of Promise.all. You can transform the promises returned by fetch.

The thing is that because json() operation is really quick, especially if response data is small in size it just has the time to execute. Second of all as objects in JavaScript passed by reference and not by value and Array is a object in JavaScript, independently of execution time it'll still push that data to the array even after it was resolved.

Related

Map array of objects and change one property with a function that calls an API. I keep getting promise pending

I have to loop through an array of objects and modify one single property in each object. I modify this property with a function that connects to the Twitter API. My problem is that I must be using async and await wrongly because I am getting a promise pending.
This is my code:
getProfile:(req,res)=>{
try {
const userId=req.params.id
const profile=db.query('SELECT * FROM profiles WHERE user_id=?',
[userId],async (err,result)=>{
if(err) return res.status(404).send(err)
const profiles= await result.map( obj=>{
const container={}
container['name']=obj.profile_name
container['desc']=obj.profile_desc
container['twitter']= connectTwitt.getTwitt(obj.twitt)//calls to api
return container
})
console.log(profiles)// promise pending
res.send(profiles)
This is the structure of the array of object that I am mapping:
[
{profile_name:`Elon Musk`, profile_desc:'enterpreneur',twitt:636465}
]
Yes, you are using the async/await syntax a little bit incorrectly.
Right now, you are calling await on the Array.map() method. However, that method is not promise-based.
Instead, you have to add the await keyword to the getTwitt() method, and await for all promises to complete.
With those changes, it should look like below.
const profiles = await Promise.all(result.map(async (obj) => { // This line has been modified
const container = {};
container["name"] = obj.profile_name;
container["desc"] = obj.profile_desc;
container["twitter"] = await connectTwitt.getTwitt(obj.twitt); // This line has been modified.
return container;
}));
Hopefully this helps with your <pending> issue!

Performing a series of async json fetch requests where each relies on a variable from the preceding fetch. Best approach?

I am doing a series of async await fetch requests, each one relying on a variable found in the preceding request. The way I have it set up now is through a series of dedicated functions that each return the variable I need. I subsequently have a .then chain, where the relevant variable is passed on, used, and finally placed in the html. These chains can sometimes contain up to 6 functions.
It might look something like this:
fetch1(artist_name) //returns an artistID
.then(artistID => fetch2(artistID, albumName)) //uses the artistID and album name to return a trackID
.then(trackID => fetch3(trackID)) //uses the trackID to return track credits
(result1)
fetch1(artist_name)
.then(artistID => fetch2(artistID, albumName))
.then(trackName => fetch4(trackName))
(result2)
This works fine, but I am curious whether this is the most efficient way to proceed, because for result 1 and 2, I repeat fetch1 and fetch2, relying on the same JSON object returned after the 2nd fetch to do my third fetch:
fetch1 > fetch2 > fetch3 > result 1
fetch1 > fetch2 > fetch4 > result 2
Can anyone point me to a resource, or provide an example of how this can be better achieved? I am aware that javascript works asynchronously (hence the async fetch).
My fetch requests all generally look like this:
async function fetch1(artist_name) {
try {
var url = `http://musicbrainz.org/ws/2/artist/?&fmt=json&limit=1&query=artist:"${artist_name}"`
let response = await fetch(url);
let data = await response.json()
let mbArtistID = data["artists"][0].id
return mbArtistID
} catch (error) {
console.log(error);
}

Tracking state of a chain of promises

I'm currently trying to track the progress of a chain of native es6 promises, and am wondering what the 'correct' way to go about this is.
I've simplified the actual code to thie following example, which is a basic set of chained promises (in reality, the promise chain is longer, and the session status value changes in more places depending on progress through the chain):
let sessions = {}
const asyncFunc = () => {
// Get a new id for state tracking
let session_id = getID()
sessions.session_id = 'PENDING'
// Fetch the first url
let result = api.get(url)
.then(res => {
// Return the 'url' property of the fetched data
return res.url
})
.then (url => {
// Fetch this second url
let data = api.get(url)
sessions.session_id = 'SUCCESS'
// Return the whole data object
return data
})
.catch(err => {
console.log("ERR", err)
sessions.session_id = 'ERROR'
})
return result
}
asyncFunc()
.then(res => {
console.log("URL", url)
})
This code tracks the state of the functions and stores them to the global sessions object - but the session_id isn't being passed back for inspection of status while the function is 'in-flight'.
One option I'm considering is adding the session_id as a property of the promise when it is returned, so this can be inspected - however I'm not sure if adding a property to a promise is a risky/hacky thing to do? Something like (simplified from above):
const asyncFunc = () => {
// Get a new id for state tracking
let session_id = getID()
sessions.session_id = 'PENDING'
// Fetch the first url
let result = api.get(url)
.then(...)
.then(...)
.catch(...)
// Add the session_id to the promise
result.session_id = session_id
return result
}
let func = asyncFunc()
let status =sessions[func.session_id]
func.then(...)
Any thoughts on the validity of this approach? I can see that I would probably also need to push the session id into the final return value as well, (so that the property exists in both the promise, and the resulting value of the resolved/rejected promise).
Alternatively, any other ways of handling this?
The obvious one is to make the function always return an array of arguments (promise and session_id) but I'd prefer to avoid having to always do e.g.:
let func = asyncFunc()
let status =sessions[func[1]]
func[0].then(...)

returning mapped array from chained promises

function createDataSet(username, region, champion, amount) {
var dataArray = []; //what I want to return, if possible with .map()
return getUserId(username, region) //required for getUserMatchlist()
.then(userId => {
getUserMatchlist(userId, region, champion, amount); //returns an array of objects
})
.then(matchlist => {
matchlist.forEach(match => {
getMatchDetails(match.gameId.toString(), region) //uses the Id from the matchlist objects to make another api request for each object
.then(res => {
dataArray.push(res); //every res is also an object fetched individually from the api.
// I would like to return an array with all the res objects in the order they appear in
})
.catch(err => console.log(err));
});
});
}
I'm trying to send data that I fetched from multiple apis to my frontend. Fetching the data isn't a problem, however, using .map() didn't work and from what I've read doesn't work well with promises. What is the best way for me to return that object? (function will be executed when a get request is received and dataArray will be sent back)
Promise.all(listOfPromises) will resolve to an array containing the resolved result of each promise in listOfPromises.
To apply that to your code, you would want something like (pseudocode):
Promise.all(matchlist.map(match => getMatchDetails(...)))
.then(listOfMatchDetails => {
// do stuff with your list!
});

How to execute Observable consistently and combine result

I have an issue with Observable chain and can't found decision.
I need to wait for result from IndexedDB, push it to next request and combine two results in last Observable.
Here is example:
const first$ = Database.gelAll(store); // return Observable
first.mergeMap((data) => {
const uuids = data.map((v) => {
return v.uuid;
});
// here i need to send request with 'uuids' array and combine result with
values from first request
const second$ = database.find(store, {'uuid': uuids}); // return Observable
});
Thanks for any advices.
If i understand you correctly, you're trying to make it so the end result of your observable is an object containing both the result from Database.gelAll and also the result from database.find. To do that, you'll just need to add a .map statement after the database.find, and return it to the merge map.
Database.gelAll(store)
.mergeMap((data) => {
const uuids = data.map((v) => v.uuid);
return database.find(store, {'uuid': uuids})
.map((databaseResult) => {
// Combine with data in some fashion
return {
firstData: data,
secondData: databaseResult
}
});
})
.subscribe(objectWithBothFirstDataAndSecondData => {
console.log(objectWithBothFirstDataAndSecondData)
});
Also, you should consider whether .mergeMap is appropriate. If .gelAll only emits one value then it should be ok, but in most cases, .switchMap or .concatMap is a better choice than .mergeMap. With mergeMap you have no guarantee of the order. Switchmap ensures that only the latest is used, concatMap ensures that everything gets through, but they get through in the same order they were asked for.

Categories

Resources