Promise, Async Await - javascript

setDeviceTimeout = id => timeout => {
const {onSetDevices, devices} = this.props;
var newDeviceList = devices.map(device => {
if (device.id === id) {
var newDevice = {
//...device,
timeout: timeout
};
deviceTable.oncePostDevice(newDevice).then( data => {
return newDevice = data
});
}
return device;
});
onSetDevices(newDeviceList);
}
So the issue I am having here is that the onSetDevices(newDeviceList) get's called before the devices.map() is finished. This is because the devices.map() has the call to a server oncePostDevice(newDevice), then returns the data and stores it in the newDevice variable and puts that into the newDeviceList array.
Because this happens onSetDevices doesn't include the the newDevice in the newDeviceList array of objects and when I set my redux state using onSetDevices, nothing has changed.
I am wondering how I turn this into an async, await or use a promise alone to finish the task of making onSetDevices wait for the devices.map() to finish.
Also here is the code for oncePostDevice:
export const oncePostDevice = (device) => new Promise(function(resolve, reject) {
fetch('https://url/devices/'+device.id, {
method: 'PUT',
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify(device)
})
.then(response => response.json())
.then(
data => {return resolve(data)},
error => {return reject(error)}
)
.catch(err => console.error(this.props.url, err.toString()));
});
As you can see I already have a promise in here working and returning the data afterwards.
I just need to know how to make my setDeviceTimeout inner mapping function finish before I hit onSetDevices.

Here's how you could do it (explanations inline in code):
// make the function async
setDeviceTimeout = id => async timeout => {
const {onSetDevices, devices} = this.props;
// make a list of promises, not of devices
// note: mapping a value via an async function will create promises
const newDeviceListPromises = devices.map(async device => {
if (device.id === id) {
const newDevice = {
...device,
timeout: timeout
};
return await deviceTable.oncePostDevice(newDevice);
}
return device;
});
// wait for all promises to finish and what they return will be the devices
const newDeviceList = await Promise.all(newDeviceListPromises);
onSetDevices(newDeviceList);
};

Related

How to know when the map function ends?

I have a "answersRequest" function, gets the id of the answers it writes to the "b" list
const answersRequest = () => {
let b = [];
answers.map(answer => {
axios({
method: 'POST',
url: 'http://127.0.0.1:8000/api/answers/',
data: answer
}).then(resp => {
b.push(resp.data.id)
})
})
}
And on completion of the map function, the below function needs to be run
const a = () => {setQuestionsBlok([...questionsBlok, {...questionBlokInputs, answers: b}]); setAnswers([])};
but I don't know how to find out when the map function ends
help me please
You basically need to push the return values from the axios call to an array and then use Promise.allSettled() or Promise.all() to wait for all the responses before you can continue processing that data.
// Just faking an axios call for this example:
function mockedAxios({ data }) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
data: {
id: Math.random().toString(36).substr(2, 9),
result: data * 2,
},
});
}, 1000 + Math.random() * 2000)
});
}
async function answersRequest(answers) {
const promises = answers.map((answer) => {
// Return each promise here:
return mockedAxios({
method: 'POST',
url: 'http://127.0.0.1:8000/api/answers/',
data: answer
});
// No need for a then block here, you can do it below in the `allSettled`'s `then`:
// .then((resp) => {
// return resp.data.id;
// });
});
// Wait for all responses and process the data:
const IDs = await Promise.allSettled(promises).then((result) => {
// result looks like { status: "fulfilled", value: { data: { id, result } } }
return result.map(r => r.value.data.id);
});
return IDs;
}
async function update() {
// You can now call and await this function from any other async function:
const IDs = await answersRequest([1,2,3,4,5]);
console.log(IDs);
// And then, one you get the result, do whatever you need to do with it:
// const a = () => {
// setQuestionsBlok([...questionsBlok, {...questionBlokInputs, answers: IDs }]);
// setAnswers([]);
// };
}
update();
You can use Promise.all to resolve all promises at once.
const answersRequest = () => Promise.all(
answers.map(answer =>
axios({
method: 'POST',
url: 'http://127.0.0.1:8000/api/answers/',
data: answer
})
)
);
Promise.all takes all the Promises passed to it (in this case, they are the HTTP requests) and only resolves when all the Promises are resolved. So, it's sending all the HTTP requests and waiting for all of them to finish. After that, the value of the returned Promise is an array containing all the responses, which means you don't need a separate variable for b anymore.
Then you can use Promise.then to call the second function.
answersRequest().then(values => {
setQuestionsBlok(
[...questionsBlok,
{
...questionBlokInputs,
answers: values.map(response => response.data.id)
}
]
);
setAnswers([]);
});
Here, values is an array containing all the responses from the requests. Then, the map call extracts all the id's from the response.
At first you should enable asynchronous to your requests.
After that you can call the function at the end of the loop like this:
const answersRequest = () => {
let i = 0; // Loop counter
let b = [];
// It's called at the end of requests
const funcA = (b) => {
setQuestionsBlok([
...questionsBlok,
{...questionBlokInputs, answers: b}
])
setAnswers([])
}
// Post answers request function
const createAnswers = async (answer) => {
return await axios({
method: 'POST',
url: 'http://127.0.0.1:8000/api/answers/',
data: answer
})
}
answers.map(answer => {
i++; // Increase loop counter value
createAnswers(answer).then(resp => {
b.push(resp.data.id)
// Runs specified function at the end of requests
if(i === answers.length) {
funcA(b)
}
})
})
}
You can use async/await to prompt your function to pause execution until a step completes:
const answersRequest = async () => {
let b = [];
await answers.map(answer => {
axios({
method: 'POST',
url: 'http://127.0.0.1:8000/api/answers/',
data: answer
}).then(resp => {
b.push(resp.data.id)
})
})
// execution pauses here
setQuestionsBlok([...questionsBlok, {...questionBlokInputs, answers: b}]); setAnswers([])
}
In this manner, b will be defined as desired for the setQuestionsBlok call.

Promise wont return valid value

I have this test I made just to check an API, but then i tryied to add an URL from a second fetch using as parameter a value obtained in the first fetch and then return a value to add in the first fecth. The idea is to add the image URL to the link. thanks in advance.
function script() {
const url = 'https://pokeapi.co/api/v2/pokemon/?offset=20&limit=20'
const result = fetch(url)
.then( (res)=>{
if(res.ok) {
return res.json()
} else {
console.log("Error!!")
}
}).then( data => {
console.log(data)
const main = document.getElementById('main');
main.innerHTML=`<p><a href='${data.next}'>Next</a></p>`;
for(let i=0; i<data.results.length;i++){
main.innerHTML=main.innerHTML+`<p><a href=${getImageURL(data.results[i].url)}>${data.results[i].name}</a></p>`;
}
})
}
async function getImageURL(imgUrl) {
const resultImg = await fetch(imgUrl)
.then( (res)=> {
return res.json()
})
.then (data => {
console.log(data.sprites.other.dream_world.front_default);
})
return resultImg.sprites.other.dream_world.front_default;
}
In general, don't mix .then/.catch handlers with async/await. There's usually no need, and it can trip you up like this.
The problem is that your fulfillment handler (the .then callback) doesn't return anything, so the promise it creates is fulfilled with undefined.
You could return data, but really just don't use .then/.catch at all:
async function getImageURL(imgUrl) {
const res = await fetch(imgUrl);
if (!res.ok) {
throw new Error(`HTTP error ${res.status}`);
}
const resultImg = await res.json();
return resultImg.sprites.other.dream_world.front_default;
}
[Note I added a check of res.ok. This is (IMHO) a footgun in the fetch API, it doesn't reject its promise on HTTP errors (like 404 or 500), only on network errors. You have to check explicitly for HTTP errors. (I wrote it up on my anemic old blog here.)]
There's also a problem where you use getImageURL:
// Incorrent
for (let i = 0; i < data.results.length; i++) {
main.innerHTML=main.innerHTML+`<p><a href=${getImageURL(data.results[i].url)}>${data.results[i].name}</a></p>`;
}
The problen here is that getImageURL, like all async functions, returns a promise. You're trying to use it as those it returned the fulfillment value you're expecting, but it can't — it doesn't have that value yet.
Instead, you need to wait for the promise(s) youre creating in that loop to be fulfilled. Since that loop is in synchronous code (not an async function), we'd go back to .then/.catch, and since we want to wait for a group of things to finish that can be done in parallel, we'd do that with Promise.all:
// ...
const main = document.getElementById('main');
const html = `<p><a href='${data.next}'>Next</a></p>`;
Promise.all(data.results.map(async ({url, name}) => {
const realUrl = await getImageURL(url);
return `<p><a href=${realUrl}>${name}</a></p>`;
}))
.then(paragraphs => {
html += paragraphs.join("");
main.innerHTML = html;
})
.catch(error => {
// ...handle/report error...
});
For one, your
.then (data => {
console.log(//...
at the end of the promise chain returns undefined. Just remove it, and if you want to console.log it, do console.log(resultImg) in the next statement/next line, after await.
This the final version that accomplish my goal. Just want to leave this just in case someone finds it usefull. Thanks for those who answer!
function script() {
const url = 'https://pokeapi.co/api/v2/pokemon/?offset=20&limit=20'
const result = fetch(url)
.then( (res)=>{
if(res.ok) {
return res.json()
} else {
console.log("Error!!")
}
}).then( data => {
console.log(data)
const main = document.getElementById('main');
main.innerHTML=`<p><a href='${data.next}'>Proxima Página</a></p>`;
Promise.all(data.results.map(async ({url, name}) => {
const realUrl = await getImageURL(url);
return `<div><a href=${realUrl}>${name}</a></div>`;
}))
.then(paragraphs => {
main.innerHTML=main.innerHTML+paragraphs;
})
.catch(error => {
console.log(error);
});
})
}
async function getImageURL(imgUrl) {
const res = await fetch(imgUrl);
if(!res.ok) {
throw new Error(`HTTP Error ${res.status}`)
}
const resultImg = await res.json();
return resultImg.sprites.other.dream_world.front_default
}

How do I make a long list of http calls in serial?

I'm trying to only make one http call at time but when I log the response from getUrl they are piling up and I start to get 409s (Too many requests)
function getUrl(url, i, cb) {
const fetchUrl = `https://api.scraperapi.com?api_key=xxx&url=${url.url}`;
fetch(fetchUrl).then(async res => {
console.log(fetchUrl, 'fetched!');
if (!res.ok) {
const err = await res.text();
throw err.message || res.statusText;
}
url.data = await res.text();
cb(url);
});
}
let requests = urls.map((url, i) => {
return new Promise(resolve => {
getUrl(url, i, resolve);
});
});
const all = await requests.reduce((promiseChain, currentTask) => {
return promiseChain.then(chainResults =>
currentTask.then(currentResult => [...chainResults, currentResult]),
);
}, Promise.resolve([]));
Basically I don't want the next http to start until the previous one has finished. Otherwise I hammer their server.
BONUS POINTS: Make this work with 5 at a time in parallel.
Since you're using await, it would be a lot easier to use that everywhere instead of using confusing .thens with reduce. It'd also be good to avoid the explicit Promise construction antipattern. This should do what you want:
const results = [];
for (const url of urls) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(response); // or whatever logic you need with errors
}
results.push(await response.text());
}
Then your results variable will contain an array of response texts (or an error will have been thrown, and the code won't reach the bottom).
The syntax for an async function is an async keyword before the argument list, just like you're doing in your original code:
const fn = async () => {
const results = [];
for (const url of urls) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(response); // or whatever logic you need with errors
}
results.push(await response.text());
}
// do something with results
};
To have a limited number of requests at a time, make a queue system - when a request completes, recursively call a function that makes another request, something like:
const results = [];
const queueNext = async () => {
if (!urls.length) return;
const url = urls.shift();
const response = await fetch(url);
if (!response.ok) {
throw new Error(response); // or whatever logic you need with errors
}
results.push(await response.text());
await queueNext();
}
await Promise.all(Array.from({ length: 5 }, queueNext));
// do something with results
You cannot use Array methods to sequentually run async operations because array methods are all synchronous.
The easiest way to achieve sequential async tasks is through a loop. Otherwise, you will need to write a custom function to imitate a loop and run .then after a async task ends, which is quite troublesome and unnecessary.
Also, fetch is already returning a Promise, so you don't have to create a Promise yourself to contain that promise returned by fetch.
The code below is a working example, with small changes to your original code (see comments).
// Fake urls for example purpose
const urls = [{ url: 'abc' }, { url: 'def', }, { url: 'ghi' }];
// To imitate actual fetching
const fetch = (url) => new Promise(resolve => {
setTimeout(() => {
resolve({
ok: true,
text: () => new Promise(res => setTimeout(() => res(url), 500))
});
}, 1000);
});
function getUrl(url, i, cb) {
const fetchUrl = `https://api.scraperapi.com?api_key=xxx&url=${url.url}`;
return fetch(fetchUrl).then(async res => { // <-- changes here
console.log(fetchUrl, 'fetched!');
if (!res.ok) {
const err = await res.text();
throw err.message || res.statusText;
}
url.data = await res.text();
return url; // <--- changes here
});
}
async function getAllUrls(urls){
const result = [];
for (const url of urls){
const response = await getUrl(url);
result.push(response);
}
return result;
}
getAllUrls(urls)
.then(console.log);
async/await is perfect for this.
Assuming you have an array of URLs as strings:
let urls = ["https://example.org/", "https://google.com/", "https://stackoverflow.com/"];
You simply need to do:
for (let u of urls) {
await fetch(u).then(res => {
// Handle response
}).catch(e => {
// Handle error
});
}
The loop will not iterate until the current fetch() has resolved, which will serialise things.
The reason array.map doesn't work is as follows:
async function doFetch(url) {
return await fetch(url).then(res => {
// Handle response
}).catch(e => {
// Handle error
});
}
let mapped = urls.map(doFetch);
is equivalent to:
let mapped;
for (u of urls) {
mapped.push(doFetch(u));
}
This will populate mapped with a bunch of Promises immediately, which is not what you want. The following is what you want:
let mapped;
for (u of urls) {
mapped.push(await doFetch(u));
}
But this is not what array.map() does. Therefore using an explicit for loop is necessary.
Many people provided answers using for loop. But in some situation await in for loop is not welcome, for example, if you are using Airbnb style guide.
Here is a solution using recursion.
// Fake urls for example purpose
const urls = [{ url: 'abc' }, { url: 'def', }, { url: 'ghi' }];
async function serialFetch(urls) {
return await doSerialRecursion(
async (url) => {
return result = await fetch(url)
.then((response) => {
// handle response
})
.catch((err) => {
// handle error
});
},
urls,
0
);
}
async function doSerialRecursion(fn, array, startIndex) {
if (!array[startIndex]) return [];
const currResult = await fn(array[startIndex]);
return [currResult, ...(await doSerialRecursion(array, fn, startIndex + 1))];
}
const yourResult = await serialFetch(urls);
The doSerialRecursion function will serially execute the function you passed in, which is fetch(url) in this example.

first fetch method in for loop running before any other methods

So I'm writing a program that requires me to run three fetch methods sequentially over each object in an array. Because each method is setting state, after I set the state, I attach a callback method that calls the next function.
I tried my naive thought of simply calling the functions one after the other with no callback, but since each function is modifying state, that does not work.
for (let i = 0; i < array.length; i++){
this.executeA(array[i]);
}
executeA(data){
fetch('http://localhost:8080/api/a', headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
})
.then((response) => {
response.json().then((res) => {
this.setState({ a: res }, () => {
executeB(data);
});
});
})
}
executeB(data){
fetch('http://localhost:8080/api/b', headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
})
.then((response) => {
response.json().then((res) => {
this.setState({ b: res }, () => {
execute(data);
});
});
})
}
executeC(data){
fetch('http://localhost:8080/api/c', headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
})
.then((response) => {
response.json().then((res) => {
this.setState({ c: res }, () => {
console.log(this.state.c);
});
});
})
}
For example, I would expect the order of operations for an array of length 3 to be A,B,C,A,B,C,A,B,C. Instead, the order of execution is A,A,A,B,C,B,C,B,C. I'm not quite sure why the first method executes for each object in the array before any other method gets called, as I thought that after it sets the state, the callback method would move onto the next function. Thanks for any clarification!
If you have some promise going on, you should propably use async/await and in the for loop use await too.
const someFunc = async () => {
for (let i = 0; i < array.length; i++){
await this.executeA(array[i]);
}
}
And also add async/await in //fetch stuff
The reason why you get AAABBBCCC is that in the for loop, you don't await the first A to complete to call the second A. You just pass through all A really fast.
The problem is that executeA returns as soon as this.setState is called (doesn't wait for the callback function passed to it to return) and that is true for all your functions. To keep this synchronised you can create new promises and fulfil them after the state is set, here is an example:
for (let i = 0; i < array.length; i++){
await this.executeA(array[i]);
}
async executeA(data){
const res = await fetch(....)
return new Promise(resolve => this.setState({ b: res }, async () => {
await executeB(data);
resolve();
});
});
}
async executeB(data){
const res = await fetch(....)
return new Promise(resolve => this.setState({ b: res }, async () => {
await executeC(data);
resolve();
});
});
}
async executeC(data){
const res = await fetch(....)
return new Promise(resolve => this.setState({ c: res }, resolve));
}
My answer is pretty similar to other people's answers except it's a working example. I hope it helps.
//* this part is just for testing
function fetchStuff(key) {
return new Promise(resolve => {
setTimeout(() => resolve(Math.random()), Math.random()*1000);
});
}
//*/
const array = ['A', 'B', 'C'];
async function runFor(elem) {
//* this part is just for testing
console.log('runFor', elem);
this.setState = function(state) {
console.log('setState', state);
};
//*/
await fetchStuff('a').then(res => {
this.setState({a: res});
});
await fetchStuff('b').then(res => {
this.setState({b: res});
});
await fetchStuff('c').then(res => {
this.setState({c: res});
});
}
async function main() {
for (let elem of array) {
await runFor(elem);
}
}
main();

async whilst with request promise

I need to iterate the http request once previous is completed. I am using request promise along with async js. My snippet is
const _getAllProduct = (token, cb) => {
const _condition = () => last !== true;
const recordCount = 50;
let last = false;
let currentPage = 0;
console.log(`\n2. Getting All Products`);
let options = {
headers: {'Content-Type': 'application/json'},
json: true
};
let allProducts = [];
const _iterate = callback => {
options.url = `${domain.url}/all?page=${currentPage}&size=${recordCount}`;
console.log(`Options: ${JSON.stringify(options)}`);
console.log(`Here`);
rp(options)
.then(res => {
last = res.last;
allProducts = _.concat(allProducts, res.content);
currentPage++;
callback(null, true);
})
.catch(error => callback(error));
};
async.whilst(_condition, _iterate, error => {
if (error) return cb(error);
console.log(`Total %d records fetched from domain`, allProducts ? _.size(allProducts) : 0);
return cb(null, allProducts);
});
};
The issue is I am getting warning the moment request is completed. Warning is promise created but not returned. My specification and requirements are using async.js and request-promise module.
(node:4159) Warning: a promise was created in a handler at /home/xavient/Desktop/node/kwa/syncing/utils/product-synb-job.js:65:8 but was not returned from it, see
at new Promise (/home/xavient/Desktop/node/kwa/syncing/node_modules/bluebird/js/release/promise.js:79:10)

Categories