I'm running into an issue where a callback sent to setTimeout
from a resolved promise never get executed.
supposed I have the following:
class Foo {
constructor(foo) {
this.foo = foo;
}
async execUntilStop(callback) {
const timeoutLoopCallback = () => {
if (this.stopExec) return;
callback({ data: 'data' });
setTimeout(timeoutLoopCallback, 10);
};
setTimeout(timeoutLoopCallback, 10);
return { data: 'data'};
}
stop() {
this.stopExec = true;
}
}
const myFunc = async function () {
let callbackCalled = false;
const callback = () => callbackCalled = true;
foo = new Foo('foo');
foo.execUntilStop(callback);
const hasCallbackCalled = async () => callbackCalled;
while(!(await hasCallbackCalled())) null;
foo.stop();
return 'success!';
};
myFunc().then((result) => console.log(result))
myFunc()
never resolves as it is continually waiting for callbackCalled
to be true
.
What am I missing here? I believe the event loop shouldn't be blocked since I'm calling await on an async function to check if the callback has been called. I hypothesise it has something to do with timeoutLoopCallback
being bound to a resolved promise but I'm not a javascript expert and could use some feedback.
Note: This looks a little odd but essentially this is derivative of a class I'm trying to write test cases for that will be continually executing a callback until stopped.
SOLVED
Using what I learned from @traktor53 answer, I wrote a handy dandy wait
function:
// resolves when callback returns true
const wait = callback => new Promise((resolve, reject) => {
const end = () => {
try {
if (callback()) {
resolve(true);
} else {
setTimeout(end, 0);
}
} catch(error) {
reject(error);
}
};
setTimeout(end, 0);
});
class Foo {
constructor(foo) {
this.foo = foo;
}
async execUntilStop(callback) {
const timeoutLoopCallback = () => {
if (this.stopExec) return;
callback({ data: 'data' });
setTimeout(timeoutLoopCallback, 10);
};
setTimeout(timeoutLoopCallback, 10);
return { data: 'data'};
}
stop() {
this.stopExec = true;
}
}
const myFunc = async function (num) {
let callbackCalled = false;
const callback = () => callbackCalled = true;
foo = new Foo('foo');
foo.execUntilStop(callback);
const hasCallbackCalled = () => callbackCalled;
await wait(hasCallbackCalled);
foo.stop();
return 'success!';
};
myFunc().then((result) => console.log(result)); // => success!
We can wrap setTimeout in a promise by using the then () method to return a Promise. The then () method takes upto two arguments that are callback functions for the success and failure conditions of the Promise. This function returns a promise. There can be two different values if the function called onFulfilled that’s mean promise is fulfilled.
An immediately resolved promise is processed faster than an immediate timeout. Might the promise process faster because the Promise.resolve (true).then (...) was called before the setTimeout (..., 0)? Fair enough question. Let's change slighly the conditions of the experiment and call setTimeout (..., 0) first: console.log('Timed out!');
In the general brief, Promise.race would take the output of first promise that is resolved based on it’s time of execusion there are no build-in method for a timeout so we would create a workaround for it using Promise.race How to timeout a method after some time?
Unfortunately, Promise is fully asynchronous and can’t be made to act synchronously. Except, of course, if we don’t mind some hacking! The solution is to create a recursive function. Here’s the full code: Our delay () function hasn’t changed, and neither has our delays array.
Jobs to handle promise settlement go into a "Promise Job Queue" (PJQ) described in ECMAScript standards. This nomenclature is not often used in HTML documentation.
Browsers (and at least one script engine) put jobs for the PJQ into what is commonly called the "Micro Task Queue" (MTQ). The event loop task manager checks the MTQ on return from script callout from the event loop, to see if it has any jobs in it, and will pop and executes the oldest job in the queue if there is one. The line in the original post
while(!(await callbackCalled)) null;
(which in the first call is equivalent to
while( !( await Promise.resolve( false)); // callbackCalled is false
)
puts a job to get the settled value of the promise returned by Promise.resolve, into the MTQ and continue executing by having the await
operator return the fulfilled value, which is false
.
Because browsers process the MTQ at a higher priority than tasks generated by timer expiry, execution continues after the await
operation and immediately executes another iteration of the loop and puts another job into the MTQ to await the false
value, without processing any timer call backs in between.
This sets up an asynchronous infinite loop ( congratulations BTW, I havn't seen one before!), and under these conditions I would not expect the timer call back to execute and call timeoutLoopCallback
a second time.
The infinite loop is also blocking continuation to the next line:
foo.stop()
never executes.
Note the blocking effect observed here is a consequence of the HTML implementation of the "Promise Job Queue" - The ECMAScript committed chose to not specify implimentation and priority details for real JavaScript systems. So blame HTML standards, not ECMAScript :D
Also note: replacing await calledBackCalled
with await hasCallbackCalled()
will not fix the problem - different promise jobs will be generated, but the await operator will still return false
.
while(!(await hasCallbackCalled())) null;
are:
hasCallbackCalled()
callbackCalled
(which is false
)false
.await
now adds handlers by calling .then
on the promise obtained in step 4, to let await
know the settled value and state (in this case "fulfilled"). then
on a fulfilled promise synchronously inserts a job to call the fulfilled handler with the fulfilled value into the MTQawait
back; await
returns to the event loop manager.await
operator processing which returns the value false
to user script.If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With