-2

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);
  return "return " + label;
}

// somewhere in the code, in some event handler
var varA = myFunction("a");
// ... do something with varA

// somewhere ELSE in the code, in some event handler
var varB = myFunction("b");
// ... do something with varB

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="() => { var varA = myFunction('button a'); console.log(varA) }">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.

nCessity
  • 735
  • 7
  • 23
  • 2
    If you want synchronous, you need to build your code to use await. Code it to work with await.... – epascarello May 23 '23 at 13:17
  • 2
    JavaScript is single-threaded so the concept doesn't make sense. Your `myFunction` is declared as `async`, so you have to treat it as a Promise-based API (because that's what it is). Thus you can `await` its results or use the old Promise `.then()` (which I would recommend against). – Pointy May 23 '23 at 13:17
  • 1
    The reason why there is no synchronized in JS is that because JS engine are monothread, compared to java. You are confusing multi threading and asynchronous (Future vs synchronized in java) – Julien Antony May 23 '23 at 13:19
  • @epascarello I added a subsection as to "Why not `await myFunction`". Do you understand my question now? I actually want to use asynchronous functions (e.g. fetch). But I need to wait until after they return before I call them again. – nCessity May 23 '23 at 13:46
  • So you call make the call to the function asynchronous or you make a function that works off a queue. – epascarello May 23 '23 at 13:47
  • @Pointy I updated my question and added the "Why not `await myFunction`. Do you understand my question now? – nCessity May 23 '23 at 13:49
  • 1
    `const sleep = async (ms) => new Promise` That `async` doesn't do anything here, just `ms => new Promise` is fine. – Keith May 23 '23 at 13:52
  • @epascarello 1) How does `onClick="async () => await myFunction('button a')"` prevent myFunction from being executed a (n+1)th time before the nth execution finisched? It doesn't, does it? 2) I would be very interested in your "queue solution" could you add an actual answer? – nCessity May 23 '23 at 13:54
  • I do, but if `myFunction()` is truly asynchronous (like, some kind of network operation), then it's going to be pretty much impossible to guarantee order of completion no matter what you do. A network operation takes as long as it takes, and the browser will continue processing stuff in the order that the Promise instances happen to resolve. – Pointy May 23 '23 at 13:56
  • @JulienAntony Actually, I'm not confusing it. I'm just using java's `synchronized` to illustrate my intention. I think that fly, tho :( – nCessity May 23 '23 at 13:57
  • @Pointy Ok, yes, I think you get me. Now: Please add your answer and argumentation as an actual answer, even if your answer is: You cannot guarantee the order of completion. However, I think, you actually *can* guarantee that `myFunction` is not being executed twice at the same time, but only one call after another - your queue solution is a candidate and my own answer is as well. – nCessity May 23 '23 at 14:03

4 Answers4

1

One idea like mentioned is create a promise queue.

Update, ok. slightly over complicated things, probably easier than a queue, is just create a promise chain.

So in summary, creating a global promise chain to serialise promises is just these 2 lines of code.

let pchain = Promise.resolve();
const chained = async p => pchain = pchain.then(() => p());

And then to use it.

chained(async () => somePromise());

The result of chained can also be awaited or use then / catch / finally.. You of course could create multiple ones, eg. lets say you had 2 databases and you wanted to be able to serialise to each one at the same time, but then serialise each DB.

Below is a runnable snippet using it action.

const sleep = (ms) => new Promise((resolve) => {
  setTimeout(resolve, ms);
});
          
const myFunction = async (label) => {
  console.log("enter " + label);
  await sleep(500);
  console.log("leave " + label);
  return label + label;
}


//very simple promise chain
let pchain = Promise.resolve();
const chained = async p => pchain = pchain.then(() => p());


//TESTING...

//lets queue up 2 promises, and get result
chained(async () => myFunction("a"))
  .then(r => console.log(`result of a = ${r}`));
chained(async () => myFunction("b"))
  .then(r => console.log(`result of b = ${r}`));
  
//and of course will work with await.
(async () => {
  const r = await chained(async () => myFunction("c"));
  console.log(`result of c = ${r}`);
})();

//How about attaching to events.
makeBtn = caption => {
  const btn = document.createElement('button');
  btn.innerText = caption;
  document.body.appendChild(btn);
  btn.onclick = () => chained(async () => myFunction(caption));
}

makeBtn('A');
makeBtn('B');
makeBtn('C');
makeBtn('D');
Keith
  • 22,005
  • 2
  • 27
  • 44
  • How would you return the return value of myFunction (given there is one) to the caller? E.g. like `var resultOfMyFunction = await chained(async () => myFunction("a"));` – nCessity May 23 '23 at 15:06
  • @nCessity Ah good point, updated so that you can consume any results. – Keith May 23 '23 at 15:10
1

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>
epascarello
  • 204,599
  • 20
  • 195
  • 236
  • How would you return the return value of myFunction (given there is one) to the caller? E.g. like ```var resultOfMyFunction = await myQueue(async () => await myFunction(1 - ${Date.now()}`));``` – nCessity May 23 '23 at 15:08
  • The way this queue is set up which is based on the concept of click triggering something, it would not be able to do this. If the return value is needed, it would require more logic in the queue. – epascarello May 23 '23 at 15:13
1

Looks like you need some kind of locking mechanism. Write a wrapper function that acquires the lock, invokes the target and then releases. Other invocations of the wrapper will have to wait until the lock is released.

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 _mutex = 0

async function acquire() {
    while (1) {
        if (_mutex === 0)
            return _mutex = 1
        // console.log('waiting for lock')
        await sleep(10)
    }
}

async function release() {
    return _mutex = 0
}

//

function synchronized(f) {
    return async function (...args) {
        await acquire()
        let res = await f(...args)
        await release()
        return res
    }
}

//

//let f = myFunction // <-- uncomment to see the difference
let f = synchronized(myFunction)

f('a')
f('b')
f('c')
gog
  • 10,367
  • 2
  • 24
  • 38
0

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 currently_executed_call = undefined; // this acts as a lock
  const _this = this; // useful for class methods.
  return async function () {
    while (currently_executed_call) {
      // wait until the currently running fn returns.
      await currently_executed_call;
    }
    currently_executed_call = fn.apply(_this, arguments);
    const return_value = await currently_executed_call;
    currently_executed_call = false; // free the lock
    return return_value;
  }
}

General Usage

You would use it like this:

const mySynchronizedFunction = synchronized(myFunction);

// then call the wrapper function as if it where the original:
var varA = await mySynchronizedFunction("a");

// ... and somewhere deep down in some event handler
var varB = await mySychronizedFunction("b");

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);
nCessity
  • 735
  • 7
  • 23
  • 1
    This seems like a lot of work just to avoid using `await` when calling `myFunction()`... – David May 23 '23 at 13:17
  • @David The calls to myFunction could be anywhere else, e.g. in a click event handler, so we cannot just await myFunction. – nCessity May 23 '23 at 13:20
  • 1
    @Pointy The `locked` variable is a promise or a boolean – nCessity May 23 '23 at 13:22
  • 3
    @nCessity: It's an `async` function, why can't you `await` it? Perhaps the example provided in the question doesn't sufficiently demonstrate the problem? – David May 23 '23 at 13:23
  • Using your `synchronized` function, and your example question -> `synchronized(myFunction('a')); synchronized(myFunction('b'));` will give you `enter a; enter b; leave a; leave b`, I thought you said you wanted `enter a; leave a; enter b; leave b;` – Keith May 23 '23 at 16:11
  • @Keith No, you would define a new function or override the original one with `mySynchronizedFunction = synchronized(myFunction)`. Then call that one: `mySynchronizedFunction("a"); mySynchronizedFunction("b")` - that will give the desired behavior. I updated my answer to make this clearer. – nCessity May 23 '23 at 20:48
  • @nCessity Oh, right that makes more sense now. So you would only use this to sync a single type of function, if you had different functions they would sync independently. But yeah, I think this would be a useful function. Certainly worth an up-vote.. :) – Keith May 24 '23 at 09:23