In this example code, the showMenu button requests a sort of custom context menu which returns a promise waiting for the user to make a selection from the menu. When the menu is requested, a busyType is passed indicating whether or not other events should be blocked until the work of the menu selection is complete.
Once the user makes a selection, I'm trying to get function BusyHandler to wait for SelectionHandler to wait for OptionHandler (where the "real" async work is performed). However, BusyHandler is done waiting before OptionHandler completes; and I think it is because all SelectionHandler does is resolve the promise and, apparently, that is not something that can be awaited. I don't want SelectionHanlder to return to the await of BusyHandler until the the setTimeout in OptionHandler completes.
Is this possible and, if so, can you show how to do it? Thank you.
When run the snippet, click the Show Menu button to display the menu, select either option from the menu which will then close, and the console log will show that BusyHandler is done waiting before the setTimeout of OptionHandler completes.
let
showMenu = document.querySelector(".showMenu"),
menu = document.querySelector(".menu"),
mapObj = Object.create(null)
;
mapObj.busyType = '';
mapObj.busyState = 0;
showMenu.addEventListener('mousedown', OptionHandler, false);
menu.addEventListener('mousedown',BusyHandler, false);
async function OptionHandler() {
let option = await MenuHandler('b');
console.log(option);
let d = Date.now();
console.log(`Work began at ${d}`);
let p = new Promise( (resolve,reject) => {
setTimeout( () => {resolve('delay resolved');}, 2000);
});
await p;
console.log(`Work completed at ${Date.now()-d}`);
}
function MenuHandler(busyType) {
menu.classList.add('show');
mapObj.busyType = busyType;
return new Promise( (resolve, reject) => {
// mapObj.resolve = resolve;
// mapObj.reject = reject;
mapObj.resolve = async function (v) {resolve(v)};
mapObj.reject = async function (v) {reject(v)};
});
}
async function SelectionHandler(evt) {
let e = evt.target;
if (
!e.matches('.menu button, .menu button *')
|| !(e = e.closest('button')) ) return;
menu.classList.remove('show');
await mapObj.resolve(e.value);
}
async function BusyHandler(evt) {
if ( mapObj.busyType === 'b' ) {
if ( mapObj.busyState === 1) return;
mapObj.busyState = 1;
await SelectionHandler(evt);
console.log("BusyHandler done waiting");
mapObj.busyState= 0;
} else {
SelectionHandler(evt);
}
}
.menu {
display: none;
border: 1px solid black;
padding: 10px;
flex-flow: column;
width: fit-content;
}
.menu.show {
display: flex;
}
button {
margin: 5px;
}
<button class="showMenu">Show Menu</button>
<div class="menu">
<button value=1>Option 1</button>
<button value=2>Option 2</button>
</div>
Added this second example to illustrate what I think the second suggestion of @Bergi is. It appears to work and the BusyHandler now waits until the OptionHandler completes its work.
let
showMenu = document.querySelector(".showMenu"),
menu = document.querySelector(".menu"),
busyObj = Object.create(null),
mapObj = Object.create(null)
;
busyObj.busyType = '';
busyObj.busyState = 0;
busyObj.promise = null;
busyObj.resolve = null;
busyObj.reject = null;
showMenu.addEventListener('mousedown', OptionHandler, false);
menu.addEventListener('mousedown',BusyHandler, false);
async function OptionHandler() {
let option = await MenuHandler('b');
console.log(option);
let d = Date.now();
console.log(`Work began at ${d}`);
let p = new Promise( (resolve,reject) => {
setTimeout( () => {resolve('delay resolved');}, 2000);
});
await p;
console.log(`Work completed at ${Date.now()-d}`);
busyObj.resolve();
}
function MenuHandler(busyType) {
menu.classList.add('show');
busyObj.busyType = busyType;
busyObj.promise = new Promise( (resolve,reject) => {
busyObj.resolve = resolve;
busyObj.reject = reject;
});
return new Promise( (resolve, reject) => {
mapObj.resolve = resolve;
mapObj.reject = reject;
});
}
function SelectionHandler(evt) {
let e = evt.target;
if (
!e.matches('.menu button, .menu button *')
|| !(e = e.closest('button')) ) return;
menu.classList.remove('show');
mapObj.resolve(e.value);
}
async function BusyHandler(evt) {
if ( busyObj.busyType === 'b' ) {
if ( busyObj.busyState === 1) return;
busyObj.busyState = 1;
SelectionHandler(evt);
await busyObj.promise;
console.log("BusyHandler done waiting");
busyObj.busyState= 0;
} else {
SelectionHandler(evt);
}
}
.menu {
display: none;
border: 1px solid black;
padding: 10px;
flex-flow: column;
width: fit-content;
}
.menu.show {
display: flex;
}
button {
margin: 5px;
}
<button class="showMenu">Show Menu</button>
<div class="menu">
<button value=1>Option 1</button>
<button value=2>Option 2</button>
</div>
I think it is because all SelectionHandler does is resolve the promise
Yes. Calling the resolve()
(or reject()
) function of a promise does nothing but to change the state of the promise and send a signal to any observers that their reactions may run now. It does not know what these reactions are (nor how many there are), it does not know what they do, it does not know what may happen after them (maybe the resolve another promise?), and it cannot wait for them.
You will need some other way to signal this. There are some possible approaches:
mapObj.busyState= 0;
in the OptionHandler
, after the work is actually doneBusyHandler
itself, you need to explicitly send a signal back from the work function and have it resolve a promise that you can await
. Just like what you did with mapObj.resolve
, just in the other direction. You can even make the mapObj.resolve
method return that promise.async
/await
, pass more callbacks around. Or use more async
/await
if you really want a sequential flow, but then don't offer multiple buttons that share some global state.I can't advise which of these is the best in your case since I can't quite tell what you're after, the example (while a great minimal demo code) is too abstract.