Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

timeout loop in promise never executes after promise is resolved?

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!
like image 970
rudolph9 Avatar asked Apr 25 '18 22:04

rudolph9


People also ask

How to wrap setTimeout in a promise in JavaScript?

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.

Is an immediately resolved promise faster than an immediate timeout?

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!');

Is it possible to timeout a promise in promise race?

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?

Is it possible to synchronize promise with a recursive function?

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.


1 Answers

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.


(Update) Since you ask, the actual steps for
 while(!(await hasCallbackCalled())) null;

are:

  1. Evaluate hasCallbackCalled()
  2. 'hasCallbackCalled` is an async function and returns a promise fulfilled with the return value of the function body.
  3. The function body is synchronous code, and fulfills the returned promise on first call by synchronously returning the value of callbackCalled (which is false)
  4. The promise returned by the async function has so far been synchronously fulfilled with the value false.
  5. 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").
  6. But calling then on a fulfilled promise synchronously inserts a job to call the fulfilled handler with the fulfilled value into the MTQ
  7. the MTQ now has a job to call code for this particular await back;
  8. await returns to the event loop manager.
  9. the MTQ job now executes the then handler added in step 5,
  10. the then handler resumes await operator processing which returns the value false to user script.
  11. the while loop test continues execution from step 1.
like image 58
traktor Avatar answered Sep 30 '22 04:09

traktor