skip to Main Content

If I have the following code

const sleep = (ms) => new Promise((resolve) => {
  setTimeout(resolve, ms);
});

// myFunction is some kind of network operation, a database fetch etc.           
const myFunction = async (label) => {
  console.log("enter " + label);
  await sleep(500);
  console.log("leave " + label);
}

// somewhere in the code, in some event handler
myFunction("a");

// somewhere ELSE in the code, in some event handler
myFunction("b");

The logging output will almost certainly be

enter a
enter b
leave a
leave b

So the second function call will be executed before the first finishes. I think I do understand why. await is only syntactic sugar and not actually blocking.

Question

How can I guarantee that the nth call to myFunction is being executed completely before the (n+1)th call starts? The logging output should be

enter a
leave a
enter b
leave b
  • Is there a pattern?
  • Is there any library I could use? (this node package seems to be unmaintained.)

Note: The function myFunction might be a library function I cannot change.

Why await myFunction does not work

The calls to myFunction could be anywhere else, e.g. in a click event handler, so we cannot just await myFunction. Which ever call enters first, should also leave the function body first (FIFO). As per this note, this is also NOT A DUPLICATE OF How can I use async/await at the top level?

If you use myFunction inside a click handler like this

<button onClick="() => myFunction('button a')">hit me</button>

you cannot simply await myFunction because if the user clicks several times myFunction will be called several times before the first call returns.

3

Answers


  1. Chosen as BEST ANSWER

    Solution (Wrapper Function)

    My take on a wrapper function awaits the currently running function before another call to that function is being processed.

    When the first call is being processed and the wrapped function (read "Promise") is in pending state, any subsequent call will end up in the while loop and the until the first function returns. Then, the next wrapper function call on the stack leaves the while loop and calls the wrapped function. When the wrapped function is pending, the while loop runs one iteration for each message waiting on the stack.

    const synchronized = (fn) => {
      let locked = false;
      const _this = this; // useful for class methods.
      return async function () {
        while (locked) {
          // wait until the currently running fn returns.
          await locked;
        }
        locked = fn.apply(_this, arguments);
        const returnValue = await locked;
        locked = false; // free the lock
        return returnValue;
      }
    }
    

    General Usage

    You would use it like this:

    const mySynchronizedFunction = synchronized(
      async (label) => {
        console.log("enter synchronized " + label);
        await sleep(500);
        console.log("leave synchronized " + label);
        return "return synchronized " + label;
      }
    }
    

    However, I can't say whether this is actually strictly FIFO. Is it guaranteed that the (n+1)-th wrapper function call will actually call the wrapped function as the (n+1)-th in line? I think that requires a deep understanding of the event loop.

    Class Methods

    What I also don't like about this: Class methods can only be defined in the constructor (or is there another way?):

    class MyClass {
      constructor (param_class_label) {
        this._class_label = param_class_label;
        this.synchronizedMethod = synchronized(
          // sychronized is arrow function, so "this" is the MyClass object.
          async (label) => {
            console.log("enter synchronized method " + this.class_label + "." + label);
            await sleep(500);
            console.log("leave synchronized method " + this.class_label + "." + label);
            return "return synchronized method " + this.class_label + "." + label;
          }
        )
      }
    }
    

    However, each method is being executed completely before the method is being called again (on the same object - which makes sense).

    Library Functions

    Also, this works with library functions as well:

    import { libraryFunction } from 'library';
    const synchronizedLibraryFunction = syncronized(libraryFunction);
    

  2. One idea like mentioned is create a promise queue.

    example.

    const sleep = (ms) => new Promise((resolve) => {
      setTimeout(resolve, ms);
    });
              
    const myFunction = async (label) => {
      console.log("enter " + label);
      await sleep(500);
      console.log("leave " + label);
    }
    
    let chain = [];
    function chained(p) {
      async function drain() {
        const c = chain;
        chain = [];
        for (const p of c) await p();
      }
      drain().then(() => chain.push(p)).then(drain);
    }
    
    chained(async () => myFunction("a"));
    chained(async () => myFunction("b"));
    Login or Signup to reply.
  3. If you have multiple things being triggered separately you want to build a queue type of system. There are a few ways to write that type of way, but basic idea is to register functions into an array and call them after the previous one finishes.

    const sleep = async (ms) => new Promise((resolve) => {
      setTimeout(resolve, ms);
    });
              
    const myFunction = async (label) => {
      console.log("enter " + label);
      await sleep(500);
      console.log("leave " + label);
    }
    
    const basicQueue = () => {
      const queue = [];
      let isRunning = false;
      
      const run = async () => {
        await queue.shift()();
        if (queue.length) await run();
        else isRunning = false;
      };
      
      return fnc => {
        queue.push(fnc);
        if (isRunning) return;
        isRunning = true;
        run();
      };
    }
    
    const myQueue = basicQueue();
    
    const registerClick = () => myQueue(async () => await myFunction(`2 - ${Date.now()}`))
    
    document.querySelector("#btn").addEventListener('click', registerClick);
    <button onclick="myQueue(async () => await myFunction(`1 - ${Date.now()}`))">Click 1</button>
    
    <button id="btn">Click 2</button>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search