I'm having a problem with a asynchronous function in javascript
My function looks like this:
async function animate(animations) {
const promises = animations.map(async(element, index) => {
const arrayBars = document.getElementsByClassName(classes.arrayElement);
if (element.operation === 'change-color') {
const [barOneIndex, barTwoIndex] = element.positions;
const barOneStyle = arrayBars[barOneIndex].style;
const barTwoStyle = arrayBars[barTwoIndex].style;
setTimeout(() => {
barOneStyle.backgroundColor = SECONDARY_COLOR;
barTwoStyle.backgroundColor = SECONDARY_COLOR;
}, index * speed);
}
if (element.operation === 'revert-color') {
const [barOneIndex, barTwoIndex] = element.positions;
const barOneStyle = arrayBars[barOneIndex].style;
const barTwoStyle = arrayBars[barTwoIndex].style;
setTimeout(() => {
barOneStyle.backgroundColor = PRIMARY_COLOR;
barTwoStyle.backgroundColor = PRIMARY_COLOR;
}, index * speed);
}
if (element.operation === 'swap') {
setTimeout(() => {
const [barOneIndex, newHeight] = element.positions;
const barOneStyle = arrayBars[barOneIndex].style;
barOneStyle.height = `${newHeight / 1.4}px`;
}, index * speed);
}
});
await Promise.all(promises);
console.log('finished');
}
It basically animates a sorting algorithm, here's the link of the project to help you to understand easier : https://divino.dev/Sorting-algorithms-visualizer/
The problem is, I need to know when the animation ends, but everything I tried didn't wait the animations to finish.
For promises to be an array of Promises, the .map() callback needs to return Promise.
For that, you need to promisify setTimeout, for which standard practice is to write a small Promise-returning delay() function.
async function animate(animations) {
function delay(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
const promises = animations.map(async(element, index) => {
const arrayBars = document.getElementsByClassName(classes.arrayElement);
return delay(index * speed).then(() => {
if (element.operation === 'change-color') {
const [barOneIndex, barTwoIndex] = element.positions;
arrayBars[barOneIndex].style.backgroundColor = SECONDARY_COLOR;
arrayBars[barTwoIndex].style.backgroundColor = SECONDARY_COLOR;
}
if (element.operation === 'revert-color') {
const [barOneIndex, barTwoIndex] = element.positions;
arrayBars[barOneIndex].style.backgroundColor = PRIMARY_COLOR;
arrayBars[barTwoIndex].style.backgroundColor = PRIMARY_COLOR;
}
if (element.operation === 'swap') {
const [barOneIndex, newHeight] = element.positions;
arrayBars[barOneIndex].style.height = `${newHeight / 1.4}px`;
}
});
});
await Promise.all(promises);
console.log('finished');
}
Note that, since the delay is the same in all three cases, it's more economical of source code to have delay(index * speed).then(...) as an outer structure and the decision-making if(){...} if(){...} if(){...} as inner structures (or the equivalent or switch/case structure).
I see your .map() function doesn't return any promises. You can fix this with
const promises = animations.map((element, index) => new Promise((resolve, reject) => {
const arrayBars = ...
...
resolve();
}))
await Promise.all(promises);
console.log('finished');
Related
I have a button when user click on it I will send a request and receive answer. If user click 100 times on this button I want to send 100 requests to server and each request send after previous. because I need previous response in next request.
example:
<button #click="sendRequest">send</button>
methods:{
sendRequest:function(){
axios.post('https:/url/store-project-item', {
'id': this.project.id,
"items": this.lists,
'labels': this.labels,
'last_update_key': this.lastUpdateKey,
'debug': 'hYjis6kwW',
}).then((r) => {
if (r.data.status) {
this.change = false
this.lastUpdateKey = r.data.lastUpdateKey;
this.showAlert('success')
} else {
if (r.data.state == "refresh") {
this.showAlert('error')
this.getProject()
} else {
this.showAlert('error')
}
}
}).catch(() => {
this.showAlert('error')
})
}}
I keep a higher-order function (i.e. a function that returns a function) withMaxDOP (DOP = degrees-of-parallelism) handy for this kind of thing:
const withMaxDOP = (f, maxDop) => {
const [push, pop] = createAsyncStack();
for (let x = 0; x < maxDop; ++x) {
push({});
}
return async(...args) => {
const token = await pop();
try {
return await f(...args);
} finally {
push(token);
}
};
};
The function makes use of an async stack data structure (implementation is in the attached demo), where the pop function is async and will only resolve when an item is available to be consumed. maxDop tokens are placed in the stack. Before invoking the supplied function, a token is popped from the stack, sometimes waiting if no token is immediately available. When the supplied completes, the token is returned to the stack. This has the effect of limiting concurrent calls to the supplied function to the number of tokens that are placed in the stack.
You can use the function to wrap a promise-returning (i.e. async) function and use it to limit re-entrancy into that function.
In your case, it could be used as follows:
sendRequest: withMaxDOP(async function(){ /*await axios.post...*/ }, 1)
to ensure that no call to this function ever overlaps another.
Demo:
const createAsyncStack = () => {
const stack = [];
const waitingConsumers = [];
const push = (v) => {
if (waitingConsumers.length > 0) {
const resolver = waitingConsumers.shift();
if (resolver) {
resolver(v);
}
} else {
stack.push(v);
}
};
const pop = () => {
if (stack.length > 0) {
const queueItem = stack.pop();
return typeof queueItem !== 'undefined' ?
Promise.resolve(queueItem) :
Promise.reject(Error('unexpected'));
} else {
return new Promise((resolve) => waitingConsumers.push(resolve));
}
};
return [push, pop];
};
const withMaxDOP = (f, maxDop) => {
const [push, pop] = createAsyncStack();
for (let x = 0; x < maxDop; ++x) {
push({});
}
return async(...args) => {
const token = await pop();
try {
return await f(...args);
} finally {
push(token);
}
};
};
// example usage
const delay = (duration) => {
return new Promise((resolve) => setTimeout(() => resolve(), duration));
};
async function doSomething(name) {
console.log("starting");
// simulate async IO
await delay(1000);
const ret = `hello ${name}`;
console.log(`returning: ${ret}`);
return ret;
}
const limitedDoSomething = withMaxDOP(doSomething, 1);
//call limitedDoSomething 5 times
const promises = [...new Array(5)].map((_, i) => limitedDoSomething(`person${i}`));
//collect the resolved values and log
Promise.all(promises).then(v => console.log(v));
The Problem is with the uplines.push.
I always get an empty uplines array so the last part of the code doesn't run. The promises resolve later and I get the correct data. May I know how to go about doing it the correct way?
const getAllUplines = async () => {
uplines = [];
const findUser = async (userFid) => {
const userDoc = await firestore.collection("users").doc(userFid).get();
if (userDoc.exists) {
const user = { ...userDoc.data(), id: userDoc.id };
console.log(user);
uplines.push(user);
if (user.immediateUplineFid) {
findUser(user.immediateUplineFid); //self looping
}
} else {
console.log("No User Found");
return null;
}
};
sale.rens.forEach(async (ren) => {
findUser(ren.userFid);
});
console.log(uplines);
return uplines;
};
let uplines = await getAllUplines();
console.log(uplines);
uplines = uplines.filter(
(v, i) => uplines.findIndex((index) => index === v) === i
); //remove duplicates
uplines.forEach((user) => {
if (user.chatId) {
sendTelegramMessage(user.chatId, saleToDisplay, currentUser.displayName);
console.log("Telegram Message Sent to " + user.displayName);
} else {
console.log(user.displayName + " has no chatId");
}
});
There are a few things that you have missed out while implementing the async call, which are explained in the inline comments in the code snippet.
A short explanation for what happened in your code is that in the line sale.rens.forEach you are passing an async function in the argument, which does not make any difference to the function forEach, it will execute it without waiting for it to complete.
Therefore in my answer I am using Promise.all to wait for all the async function calls to complete before returning the result.
// This is wrapped in an immediately executed async function because await in root is not supported here
(async () => {
const mockGetData = () => new Promise(resolve => setTimeout(resolve, 1000));
const sale = {
rens: [
{ userFid: 1 },
{ userFid: 2 },
{ userFid: 3 }
]
};
const getAllUplines = async () => {
const uplines = [];
const findUser = async (userFid) => {
// Simulating an async function call
const userDoc = await mockGetData();
console.log("User data received");
uplines.push(`User ${userFid}`);
};
const promises = [];
sale.rens.forEach(ren => { // This function in foreach does not have to be declared as async
// The function findUser is an async function, which returns a promise, so we have to keep track of all the promises returned to be used later
promises.push(findUser(ren.userFid));
});
await Promise.all(promises);
return uplines;
};
let uplines = await getAllUplines();
console.log(uplines);
})();
In order to get the results of getAllUplines() properly, you need to add await to all async functions called in getAllUplines().
const getAllUplines = async () => {
uplines = [];
const findUser = async (userFid) => {
const userDoc = await firestore.collection("users").doc(userFid).get();
if (userDoc.exists) {
const user = { ...userDoc.data(), id: userDoc.id };
console.log(user);
uplines.push(user);
if (user.immediateUplineFid) {
await findUser(user.immediateUplineFid); //self looping
}
} else {
console.log("No User Found");
return null;
}
};
sale.rens.forEach(async (ren) => {
await findUser(ren.userFid);
});
console.log(uplines);
return uplines;
};
I have a function like this:
const init = async () => {
const els = [...]; //array of html elements
els[0].classList.add('hidden');
await sleep(200);
els[1].classList.remove('hidden');
await sleep(500);
els[3].classList.remove('hidden');
await sleep(4000);
els[3].classList.add('hidden');
els[2].classList.remove('hidden');
els[1].classList.add('hidden');
await sleep(800);
els[3].classList.add('out');
els[4].classList.remove('hidden');
}
As you can see there's a 4 second await in there. I want to, using an external function that comes from a click, to be able to skip that 4000ms delay.
const cancelAnimation = () => {
// whatever
}
I thought of using a flag variable to change the number from 4000 to 500 for example, but if it already gotten into that sleep(4000) it doesn't matter cause the number won't change.
So, is there any way to cancel this out?
Btw, this is the code from the sleep function:
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
You can make your promise cancelable:
const cancelableSleep = (ms) => {
let timeout;
return {
promise: new Promise((resolve) => {
timeout = setTimeout(resolve, ms);
}),
cancel() {
clearTimeout(timeout);
},
};
};
const init = async () => {
const cancelable = cancelableSleep(10000);
//simulate click in 2 seconds
setTimeout(() => cancelable.cancel(), 2000);
console.log("sleeping");
await cancelable.promise;
console.log("awake");
};
init();
A bit of magic (https://codesandbox.io/s/green-dream-u2yxk?file=/src/index.js) :)
import CPromise from "c-promise2";
const init = () => CPromise.from(function* () {
let skip = false;
let promise;
this.on("signal", (type) => {
if (type === "skip") {
promise ? promise.cancel() : (skip = true);
}
});
console.log("stage1");
yield CPromise.delay(200);
console.log("stage2");
yield CPromise.delay(500);
console.log("stage3");
if (!skip) {
yield (promise = CPromise.delay(4000)).cancelled();
}
console.log("stage4");
yield CPromise.delay(800);
console.log("stage5");
});
const task = init();
console.log(task instanceof Promise); // true
setTimeout(() => {
task.emitSignal("skip");
}, 800);
I have an array of asynchronous functions, it is necessary to call in order and the result of the call of the previous function is passed into the arguments. How can this be done approximately?
// lets say we have a function that takes a value and adds 20 to it asynchronously
const asyncPlus20 = num => Promise.resolve(num+a)
const arr = [asyncPlus20, asyncPlus20]
let res = 0 // some starting value
for (const f of arr) res = await f(res)
// res is now 20
One of the best way for Array of Async functions is to use For...of.
Run the below snippet in the console. >> Also includes the argument passing
const twoSecondsPromise = () => {
return new Promise((resolve) => {
setTimeout(() => resolve('2000_'), 2000);
})
};
const threeSecondsPromise = (val) => {
return new Promise((resolve) => {
setTimeout(() => resolve(val + '3000_'), 3000);
})
};
const fiveSecondsPromise = (val) => {
return new Promise((resolve) => {
setTimeout(() => resolve(val + '5000_'), 5000);
})
};
(async function () {
const asyncFunctions = [twoSecondsPromise, threeSecondsPromise, fiveSecondsPromise];
let result;
for (const file of asyncFunctions) {
result = await file(result);
console.log(result);
}
})();
Even though javascript runs in single thread, concurency issues may still arise in async functions. Some of them may be avoided by greatly increasing the complexity of the code, but some I solve like this:
// private "lock"
let _lock = null;
// this function waits till the last call is done, then
// initiates next one
async function doTheStuff() {
while (_lock) {
await _lock;
}
_lock = actuallyDoTheStuff();
const result = await _lock;
_lock = null;
return result;
}
async function actuallyDoTheStuff() {
// this function really does the stuff
}
This ensures that only one instance of actuallyDoTheStuff is running, but it doesn't really look that nice.
Will this truly work? Can I be sure there will be no endless loop/lock?
And, whether it works or not, isn't there a better way to do this?
I'd encapsulate everything inside actuallyDoTheStuff, which simply calls .then on the last Promise it generated:
const actuallyDoTheStuff = (() => {
let lastProm = Promise.resolve();
return () => {
const nextProm = lastProm.then(() => {
return new Promise(resolve => setTimeout(() => {
console.log('resolving');
resolve();
}, 1000));
});
lastProm = nextProm;
return lastProm;
};
})();
console.log('start');
actuallyDoTheStuff();
actuallyDoTheStuff();
actuallyDoTheStuff();
setTimeout(() => {
actuallyDoTheStuff();
actuallyDoTheStuff();
}, 200);
If it may throw, then add a catch when reassigning to lastProm
const actuallyDoTheStuff = (() => {
let lastProm = Promise.resolve();
return () => {
const nextProm = lastProm.then(() => {
return new Promise(resolve => setTimeout(() => {
console.log('resolving');
resolve();
}, 1000));
});
lastProm = nextProm.catch(() => null);
return nextProm;
};
})();
console.log('start');
actuallyDoTheStuff();
actuallyDoTheStuff();
actuallyDoTheStuff();
setTimeout(() => {
actuallyDoTheStuff();
actuallyDoTheStuff();
}, 200);
I'm not sure exactly what actuallyDoTheStuff eventually should do, but if you're trying to sequence multiple calls of it (and await each call), you could make doTheStuff an async wrapper function with a for loop that awaits actuallyDoTheStuff on each iteration:
function actuallyDoTheStuff( iteration ) {
console.log( "Waiting...")
return new Promise( res => {
setTimeout( () => {
res( iteration );
}, 150 );
} );
}
async function doTheStuff() {
for ( let i = 0; i <= 5; i++ ) {
const result = await actuallyDoTheStuff( i );
console.log( result );
}
}
doTheStuff();
Or alternatively make actuallyDoTheStuff a recursive function:
let index = 1;
async function actuallyDoTheStuff( i ) {
if ( i <= 5 ) {
console.log( "Waiting..." )
await new Promise( res => {
setTimeout( () => {
console.log( i );
i++
res();
actuallyDoTheStuff( i );
}, 150 );
} );
}
}
actuallyDoTheStuff( index );