Problems with async / await - How to wait for requests - javascript

I have an array of objects containing metadata for videos. I need to iterate through it and for each object I need to make an API call passing its filename to get a streamingLink and assign it to the object's streamingLink. My problem is by the time I am returning this array, the array is undefined. How do I tell the code to wait until streamingLink has been assigned?
Here's kind of what my code looks like:
// get array from database
const items = await client.db('database').collection('collection').find({}).toArray();
// for each object get and replace streamingLink
let items_withLinks = items.map((item) => {
getStreamingLink(item.filename) // API call to get link
.then(response => {
item.streamingLink = response.result.link;
return item;
})
});
console.log(items_withLinks); // undefined
I have tried using await in different places and wrapping this in an async function but I can't figure out the right way to do it.

You can loop over your array and create a promise for each item. Then you can call Promise.all.
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'foo');
});
Promise.all([promise1, promise2, promise3]).then((values) => {
console.log(values);
});
// expected output: Array [3, 42, "foo"]

The fundamental problem is that the function you are using in the .map doesn't return anything. (So you will actually technically get an array of undefined values, rather than undefined itself - but that obviously is no good.)
You need to return the promise and then use Promise.all to get the array of results.
// get array from database
const items = await client.db('database').collection('collection').find({}).toArray();
// for each object get and replace streamingLink
let items_withLinks = items.map((item) => {
// return is the only thing I've added here!
return getStreamingLink(item.filename) // API call to get link
.then(response => {
item.streamingLink = response.result.link;
return item;
})
});
const result = await Promise.all(items_withLinks);
console.log(result);

Related

'getDownloadURL' returns one less URL in a Promise

I'm trying to upload several pictures to Firebase Storage and get their URLs to add these as a field in a gym's profile. However, with the code I came up with, always one URL is not returned. If I upload 3 pictures, 3 pictures appear in Storage but I only get 2 URLs. How do I fix that?
const promises = [];
const URLarray = [];
imageAsFile.forEach(img => {
const uploadTask = storage.ref().child(`/photos/${doc.id}/${img.name}`).put(img);
promises.push(uploadTask);
uploadTask.then((uploadTaskSnapshot) => {
return uploadTaskSnapshot.ref.getDownloadURL();
}).then((pic) => {
const addPics = db.collection("gyms").doc(doc.id)
promises.push(addPics);
addPics.update({gymPhoto: firebase.firestore.FieldValue.arrayUnion(pic)});
// URLarray.push(pic)
})
});
Promise.all(promises).then(() => {
// console.log(URLarray)
alert("Done");
})
The following should work (untested):
const promises = [];
imageAsFile.forEach(img => {
const uploadTask = storage.ref().child(`/photos/${doc.id}/${img.name}`).put(img);
promises.push(uploadTask);
});
Promise.all(promises)
.then(uploadTaskSnapshotsArray => {
const promises = [];
uploadTaskSnapshotsArray.forEach(uploadTaskSnapshot => {
promises.push(uploadTaskSnapshot.ref.getDownloadURL());
});
return Promise.all(promises);
})
.then(urlsArray => {
const docRef = db.collection("gyms").doc(doc.id);
return docRef.update({ gymPhoto: firebase.firestore.FieldValue.arrayUnion(...urlsArray) });
})
Promise.all() returns a single promise: This promise is fulfilled with an Array containing all the resolved values in the Array passed as the argument.
So, if you call Promise.all() with the Array of uploadTasks, when the returned promise is fulfilled you will get an Array of uploadTaskSnapshots.
Then you need to use this Array to get an Array of URLs, again by using Promise.all().
Then, when you get the Array of URLs, you need to update only once the document with id = doc.id. So here you don't need Promise.all(). What you need to do is to pass several values to the FieldValue.arrayUnion() method. For that you need to use the Spread operator, as follows:
docRef.update({ gymPhoto: firebase.firestore.FieldValue.arrayUnion(...urlsArray) });

getting data from js promise

I am doing an api call that returns a promise. The call works fine but I want to do treat data contained within the promise. Here is My call:
let promiseArray = this.get('store').query('member', {query: term, option: this.get('option')});
promiseArray.then(members => {console.log(members);
});
let var= members;
console.log(var);
My problem is that this doesn't return an array of my model i.e members, also the second display of members display undefined , it return an object containing a lot of meta data, also the array but inside some meta data.
How could I get simply the array ?
You can use async await for your purpose.
const promiseFunc = () => {
// Return the promise and await this inside a async function
return this.get('store').query('member', {query: term, option: this.get('option')});
}
const asyncFunc = async () => {
const value = await promiseFunc();
console.log(value);
}
asyncFunc();

Waiting for all firebase call to finish with map function

I'm fetching my user data and the map function is called several times for each user. I want to wait until all data was pushed to the array and then manipulate the data. I tried using Promise.all() but it didn't work.
How can I wait for this map function to finish before continuing?
Needless to say that the array user_list_temp is empty when printed inside the Promise.all().
const phone_list_promise_1 = await arrWithKeys.map(async (users,i) => {
return firebase.database().ref(`/users/${users}`)
.on('value', snapshot => {
user_list_temp.push(snapshot.val());
console.log(snapshot.val());
})
}
);
Promise.all(phone_list_promise_1).then( () => console.log(user_list_temp) )
I changed the code to this but I still get a wrong output
Promise.all(arrWithKeys.map(async (users,i) => {
const eventRef = db.ref(`users/${users}`);
await eventRef.on('value', snapshot => {
const value = snapshot.val();
console.log(value);
phone_user_list[0][users].name = value.name;
phone_user_list[0][users].photo = value.photo;
})
console.log(phone_user_list[0]);
user_list_temp.push(phone_user_list[0]);
}
));
console.log(user_list_temp); //empty array
}
It is possible to use async/await with firebase
This is how I usually make a Promise.all
const db = firebase.database();
let user_list_temp = await Promise.all(arrWithKeys.map(async (users,i) => {
const eventRef = db.ref(`users/${users}`);
const snapshot = await eventref.once('value');
const value = snapshot.value();
return value;
})
);
This article gives a fairly good explanation of using Promise.all with async/await https://www.taniarascia.com/promise-all-with-async-await/
Here is how I would refactor your new code snippet so that you are not mixing promises and async/await
let user_list_temp = await Promise.all(arrWithKeys.map(async (users,i) => {
const eventRef = db.ref(`users/${users}`);
const snapshot= await eventRef.once('value');
const value = snapshot.val();
console.log(value);
phone_user_list[0][users].name = value.name; // should this be hardcoded as 0?
phone_user_list[0][users].photo = value.photo; // should this be hardcoded as 0?
console.log(phone_user_list[0]);
return phone_user_list[0]; // should this be hardcoded as 0?
})
);
console.log(user_list_temp);
Here is a simple example that uses fetch, instead of firebase:
async componentDidMount () {
let urls = [
'https://jsonplaceholder.typicode.com/todos/1',
'https://jsonplaceholder.typicode.com/todos/2',
'https://jsonplaceholder.typicode.com/todos/3',
'https://jsonplaceholder.typicode.com/todos/4'
];
let results = await Promise.all(urls.map(async url => {
const response = await fetch(url);
const json = await response.json();
return json;
}));
alert(JSON.stringify(results))
}
If I understand your question correctly, you might consider revising your code to use a regular for..of loop, with a nested promise per user that resolves when the snapshot/value for that user is available as shown:
const user_list_temp = [];
/*
Use regular for..of loop to iterate values
*/
for(const user of arrWithKeys) {
/*
Use await on a new promise object for this user that
resolves with snapshot value when value recieved for
user
*/
const user_list_item = await (new Promise((resolve) => {
firebase.database()
.ref(`/users/${users}`)
.on('value', snapshot => {
/*
When value recieved, resolve the promise for
this user with val()
*/
resolve(snapshot.val());
});
}));
/*
Add value for this user to the resulting user_list_item
*/
user_list_temp.push(user_list_item);
}
console.log(user_list_temp);
This code assumes that the enclosing function is defined as an asynchronous method with the async keyword, seeing that the await keyword is used in the for..of loop. Hope that helps!

Display results of all promises with order

I am new to promises.
I encountered a case where I have to loop through a series of tasks and get data. For some reason I have to do it in order and have to do it using promises. But to my surprise, the result only has the last element instead of all elements.
The simplified version of code
const _ = require('lodash');
const test = (name) => {
return new Promise(function(resolve, reject) {
//This is simplified from my actual code
resolve(name);
});
};
const testAll = (arr) => {
//This chains and returns a series of functions with the order of arr, so that the can be executed in the order, not simultaneously
let functions = _.map(arr, element => test.bind(null, element));
//This reduces the functions and is supposed to return an array of all values
return functions.reduce((fun1, fun2) => fun1.then(fun2), Promise.resolve([]));
}
const arr = ['promise1', 'promise2', 'promise3'];
testAll(arr)
.then(data => console.log(data));
I was expecting the output to be (with order):
promise1
promise2
promise3
but what I really got was just promise3. Is it because the Promise.resolve([]) does not include every element in an array?
You seem to want to accumulate the individual results into an array. For that you must at least capture the resolved value from each of the promises. When you do fun1.then(fun2) you throw away the value promised by fun1. To use it, you need to do something with the argument that is passed to the then callback. In your case you want to concatenate that with the promised value of fun2().
But since you have the first, but must still wait for the second, you could benefit from Promise.all, like this:
const testAll = (arr) => {
let functions = arr.map(element => test.bind(null, element));
return functions.reduce((prom, fun) =>
prom.then(data => Promise.all(data.concat(fun())) ),
Promise.resolve([])
);
}
Now your final call to testAll will give you an array as result.
const test = (name) => {
return new Promise(function(resolve, reject) {
setTimeout(_ => { // Introduce delay for better demo
console.log('.'); // a little trace in the output
resolve(name);
}, 500);
});
};
const testAll = (arr) => {
let functions = arr.map(element => test.bind(null, element));
return functions.reduce((prom, fun) =>
prom.then(data => Promise.all(data.concat(fun())) ),
Promise.resolve([])
);
}
const arr = ['promise1', 'promise2', 'promise3'];
testAll(arr).then(data => console.log(data));

How to use promises to wait for async API calls

I am creating an API that when GET, a series of calls to the News API are made, news article titles are extracted into a giant string, and that string is processed into an object to be delivered to a wordcloud on the front-end. So far, I've been able to use underscore's _.after and request-promise to make my app wait till all API calls have completed before calling processWordBank() which takes the giant string and cleans it up into an object. However, once processWordBank() is called, I don't understand where the flow of the program is. Ideally, processWordBank() returns obj to cloudObj in the router, so that the obj can be passed to res.json() and spit out as the response. I believe my use of _.after has put me in a weird situation, but it's the only way I've been able to get async calls to finish before proceeding to next desired action. Any suggestions?
(I've tried to leave out all unnecessary code but let me know if this is insufficient)
// includes...
var sourceString = ""
// router
export default ({ config }) => {
let news = Router()
news.get('/', function(req, res){
var cloudObj = getSources()
res.json({ cloudObj })
})
return news
}
// create list of words (sourceString) by pulling news data from various sources
function getSources() {
return getNewsApi()
}
// NEWS API
// GET top 10 news article titles from News API (news sources are determined by the values of newsApiSource array)
function getNewsApi() {
var finished = _.after(newsApiSource.length, processWordBank)
for(var i = 0; i < newsApiSource.length; i++) {
let options = {
uri: 'https://newsapi.org/v1/articles?source=' + newsApiSource[i] + '&sortBy=' + rank + '&apiKey=' + apiKey,
json: true
}
rp(options)
.then(function (res) {
let articles = res.articles // grab article objects from the response
let articleTitles = " " + _.pluck(articles, 'title') // extract title of each news article
sourceString += " " + articleTitles // add all titles to the word bank
finished() // this async task has finished
})
.catch(function (err) {
console.log(err)
})
}
}
// analyse word bank for patterns/trends
function processWordBank(){
var sourceArray = refineSource(sourceString)
sourceArray = combineCommon(sourceArray)
sourceArray = getWordFreq(sourceArray)
var obj = sortToObject(sourceArray[0], sourceArray[1])
console.log(obj)
return obj
}
A big issue in your asynchronous flow is that you use a shared variable sourceString to handle the results. When you have multiple calls to getNewsApi() your result is not predictable and will not always be the same, because there is no predefined order in which the asynchronous calls are executed. Not only that, but you never reset it, so all subsequent calls will also include the results of the previous calls. Avoid modifying shared variables in asynchronous calls and instead use the results directly.
I've been able to use underscore's _.after and request-promise to make my app wait till all API calls have completed before calling processWordBank()
Although it would possible to use _.after, this can be done very nicely with promises, and since you're already using promises for your requests, it's just a matter of collecting the results from them. So because you want to wait until all API calls are completed you can use Promise.all which returns a promise that resolves with an array of the values of all the promises, once all of them are fulfilled. Let's have a look at a very simple example to see how Promise.all works:
// Promise.resolve() creates a promise that is fulfilled with the given value
const p1 = Promise.resolve('a promise')
// A promise that completes after 1 second
const p2 = new Promise(resolve => setTimeout(() => resolve('after 1 second'), 1000))
const p3 = Promise.resolve('hello').then(s => s + ' world')
const promises = [p1, p2, p3]
console.log('Waiting for all promises')
Promise.all(promises).then(results => console.log('All promises finished', results))
console.log('Promise.all does not block execution')
Now we can modify getNewsApi() to use Promise.all. The array of promises that is given to Promise.all are all the API request you're doing in your loop. This will be created with Array.protoype.map. And also instead of creating a string out of the array returned from _.pluck, we can just use the array directly, so you don't need to parse the string back to an array at the end.
function getNewsApi() {
// Each element is a request promise
const apiCalls = newsApiSource.map(function (source) {
let options = {
uri: 'https://newsapi.org/v1/articles?source=' + source + '&sortBy=' + rank + '&apiKey=' + apiKey,
json: true
}
return rp(options)
.then(function (res) {
let articles = res.articles
let articleTitles = _.pluck(articles, 'title')
// The promise is fulfilled with the articleTitles
return articleTitles
})
.catch(function (err) {
console.log(err)
})
})
// Return the promise that is fulfilled with all request values
return Promise.all(apiCalls)
}
Then we need to use the values in the router. We know that the promise returned from getNewsApi() fulfils with an array of all the requests, which by themselves return an array of articles. That is a 2d array, but presumably you would want a 1d array with all the articles for your processWordBank() function, so we can flatten it first.
export default ({ config }) => {
let news = Router()
new.get('/', (req, res) => {
const cloudObj = getSources()
cloudObj.then(function (apiResponses) {
// Flatten the array
// From: [['source1article1', 'source1article2'], ['source2article1'], ...]
// To: ['source1article1', 'source1article2', 'source2article1', ...]
const articles = [].concat.apply([], apiResponses)
// Pass the articles as parameter
const processedArticles = processWordBank(articles)
// Respond with the processed object
res.json({ processedArticles })
})
})
}
And finally processWordBank() needs to be changed to use an input parameter instead of using the shared variable. refineSource is no longer needed, because you're already passing an array (unless you do some other modifications in it).
function processWordBank(articles) {
let sourceArray = combineCommon(articles)
sourceArray = getWordFreq(sourceArray)
var obj = sortToObject(sourceArray[0], sourceArray[1])
console.log(obj)
return obj
}
As a bonus the router and getNewsApi() can be cleaned up with some ES6 features (without the comments from the snippets above):
export default ({ config }) => {
const news = Router()
new.get('/', (req, res) => {
getSources().then(apiResponses => {
const articles = [].concat(...apiResponses)
const processedArticles = processWordBank(articles)
res.json({ processedArticles })
})
})
}
function getNewsApi() {
const apiCalls = newsApiSource.map(source => {
const options = {
uri: `https://newsapi.org/v1/articles?source=${source}&sortBy=${rank}&apiKey=${apiKey}`,
json: true
}
return rp(options)
.then(res => _.pluck(res.articles, 'title'))
.catch(err => console.log(err))
})
return Promise.all(apiCalls)
}

Categories