Reputation: 8105
I hope this is a simple question but I currently can't wrap my head around.
What I want to do is break out of a while loop which contains a delay when a promise gets resolved.
In pseudocode this would look like:
while( ! promise.resolved ){
doSomthing()
await sleep( 5min )
}
The loop must break instantly after the promise is resolved and not wait for sleep
to finish.
sleep
is currently implemented trivially by setTimeout
but can be implemented differently.
I would like to have some kind of spatial separation between the awaited promise and sleep to show its working more clearly*) (and because I hope for an elegant solution to learn from). So what would work but I don't like is something like:
while( true ){
doSomething()
try {
await Promise.race([promise,rejectAfter(5000)])
break
} catch( e ){}
}
If you must know:
doSomething
is sending out status information.
promise
is waiting for user interaction.
*) Part of the purpose of this code is to show/demonstrate others how things are expect to work. So I'm looking for the clearest solution on this level of implementation.
Upvotes: 3
Views: 1700
Reputation: 664494
One approach would be to wait for the promise result, assuming it is a truthy value1:
const promise = someTask();
let result = undefined;
while (!result) {
doSomething();
result = await Promise.race([
promise, // resolves to an object
sleep(5000), // resolves to undefined
]);
}
1: and if it isn't, either chain .then(_ => true)
to the promise
, or make sleep
fulfill with a special value that you can distinguish from everything someTask
might return (like the symbol in Jeff's answer).
Also works nicely with a do
-while
loop, given result
is always undefined at first:
const promise = someTask();
let result = undefined;
do {
doSomething();
result = await Promise.race([
promise, // resolves to an object
sleep(5000), // resolves to undefined
]);
} while (!result);
A downside here is that if doSomething()
throws an exception, the promise is never awaited and might cause an unhandled rejection crash when the task errors.
Another approach would be not to use a loop, but an old-school interval:
const i = setInterval(doSomething, 5000);
let result;
try {
result = await someTask();
} finally {
clearInterval(i);
}
A downside here is that doSomething
is not called immediately but only after 5s for the first time. Also, if doSomething
throws an exception, it will instantly crash the application. It still might be a good approach if you don't expect doSomething
to throw (or handle each exception in the setInterval
callback and expect the "loop" to carry on).
The "proper" approach that will forward all exceptions from both someTask()
and doSomething()
could look like this:
let done = false;
const result = await Promise.race([
(async() => {
while (!done) {
doSomething();
await sleep(5000)
}
})(),
someTask().finally(() => {
done = true;
}),
]);
(Instead of .finally()
, you can also wrap the Promise.race
in a try
-finally
, like in approach two.)
The only little disadvantage in comparison to approach two is that sleep(5000)
will keep running and is not immediately cancelled when someTask
finishes (even though result
is immediately available), which might prevent your program from exiting as soon as you want.
Upvotes: 2
Reputation: 29007
Minor modification of your idea to make it work better:
const sleep = ms =>
new Promise(resolve => setTimeout(resolve, ms));
async function waitAndDo(promise) {
let resolved = false;
const waitFor = promise
.then((result) => resolved = true);
while(!resolved) {
doSomething();
await Promise.race([waitFor, sleep(5000)]);
}
}
waitFor
promise will finish after promise
is fulfilled and resolved
updated to true
.while
loop can then loop until the resolved
variable is set to true
. In that case, the loop will end an execution continues after it.Promise.race()
will ensure that it will stop awaiting as soon as the promise resolves or the sleep expires. Whichever comes first.Therefore, as soon as the promise gets resolve, the .then()
handler triggers first and updates resolved
. The await Promise.race();
will end the waiting after and the while
loop will not execute again, since resolved
is now true
.
Upvotes: 2
Reputation: 95634
As an alternative to VLAZ's very reasonable answer, you can avoid the separate boolean sentinel by having your sleep function return some kind of unique sentinel return value that indicates the the timeout. Symbol is exactly the kind of lightweight, unique object for this use case.
function sleepPromise(ms, resolveWith) {
return new Promise(resolve => {
setTimeout(resolve, ms, resolveWith);
});
}
const inputPromise = new Promise(
resolve => document.getElementById("wakeUp").addEventListener("click", resolve));
async function yourFunction() {
const keepSleeping = Symbol("keep sleeping");
do {
/* loop starts here */
console.log("Sleeping...");
/* loop ends here */
} while (await Promise.race([inputPromise, sleepPromise(3000, keepSleeping)]) === keepSleeping);
console.log("Awake!");
}
yourFunction();
<button id="wakeUp">Wake up</button>
Upvotes: 1
Reputation: 207501
Seems like you would just use some sort of interval and kill it when the promise is done.
const updateMessage = (fnc, ms, runInit) => {
if (runInit) fnc();
const timer = window.setInterval(fnc, ms);
return function () {
console.log('killed');
timer && window.clearTimeout(timer);
}
}
const updateTime = () => {
document.getElementById("out").textContent = Date.now();
}
const updateEnd = updateMessage(updateTime, 100, true);
new Promise((resolve) => {
window.setTimeout(resolve, Math.floor(Math.random()*5000));
}).then(updateEnd);
<div id="out"></div>
Upvotes: 1