0

Currently, I have defined two functions as follows:

function getLock() {
  let promise, isPending;
  if (Math.random() > 0.5) {
    promise = new Promise((resolve, reject) => setTimeout(resolve, 1000));
    isPending = true;
  } else {
    promise = Promise.resolve();
    isPending = false;
  }
  return [promise, isPending];
};

function waitForLock() {
  const lockAndIsPending = getLock();
  const lock = lockAndIsPending[0];
  const isPending = lockAndIsPending[1];

  if (!isPending) {
    return lock;
  } else {
    return lock.then(waitForLock);
  }
}

waitForLock().then(() => console.log("done"));

This is of course for demonstration only. As a necessity of the real world application, getLock always needs to return a Promise. Also, after the lock has been released once, it has to be checked if it is still released (lock.then(waitForLock)) because the real release conditions (here modeled by Math.random()) could have changed meanwhile due to the asynchrony. This introduces recursion.

My current code works fine: waitForLock() will eventually return because it will happen sometime that getLock returns isPending=false, fulfilling the termination condition.

But my goal is to get rid of isPending as extra return variable. In the future, getLock should return a promise only. Unfortunately, it is not possible to check whether a given promise is already resolved or not (see here), so I cannot "calculate" isPending in waitForLock. So how could I change waitForLock accordingly?

Edit

The goal is to be able to use any Promise-returning function for getLock without special constraints on return or resolve values. So, if possible, the logic should all be in waitForLock, not in getLock.

Remirror
  • 692
  • 5
  • 14
  • Why don't you just `await getLock()` ? – Jeremy Thille Dec 10 '20 at 15:25
  • This wouldn't change the recursion problem, would it? I still would have to decide whether to call `waitForLock` a second time or not. – Remirror Dec 10 '20 at 15:32
  • I don't think so, see my answer below – Jeremy Thille Dec 10 '20 at 15:44
  • Isn't `waitForLock` already the function you are looking for, which returns a promise for when the lock is released? – Bergi Dec 10 '20 at 15:55
  • Yes, the current implementation works, but it relies on "additional information" (namely the `isPending` variable) which I want to get rid of. – Remirror Dec 10 '20 at 15:56
  • It would help if you could post the code of (or at least describe) your real world use case, not just a random variable. In particular, it is unclear how the condition is affected by other code, and how a lock is obtained/released (which needs to be done atomically but your `getLock` code doesn't do anything to that effect). – Bergi Dec 10 '20 at 15:56
  • @Remirror If the current code works, you can just inline the `getLock` function in the `waitForLock` function, and you have one single function that does what you want and has only a single `if` statement and no boolean variable. – Bergi Dec 10 '20 at 15:58
  • @Bergi Ok I will think of how to make the scenario more clear. It has something to due with concurrent processes, but it's really difficult to present it in a concise way. Regarding `getLock`, the idea of separation is to be able to use many different functions in place of `getLock`, not just this one. – Remirror Dec 10 '20 at 16:02
  • @Remirror Then abstract out the `testLock` part, not the timeout for retrying. – Bergi Dec 10 '20 at 16:03
  • "*the logic should all be in `waitForLock`, not in `getLock`.*" - but what logic? There's only the retry part, is it not? You require a base case for that recursion, and the only viable approach is a boolean (or null-vs-nonnull etc) result value of the promise, like in @Montecamo's answer. But tbh, I think that's not enough logic to require a separate function at all, you can achieve the same result by just adding `.then(getLock)` to the timeout promise in `getLock` itself. – Bergi Dec 10 '20 at 18:09
  • It would really help if you could post the complete actual code of what you are doing with concurrent processes here. Or at least the code that regards the lock. It looks (from the name) like you're trying to implement a [semaphore](https://en.wikipedia.org/wiki/Semaphore_(programming))? – Bergi Dec 10 '20 at 18:11

2 Answers2

0

I think you can do something like this

function getLock() {
  if (Math.random() > 0.5) {
    return new Promise((resolve, reject) => setTimeout(resolve, 1000));
  }

  return Promise.resolve('pending');
};

function waitForLock() {
  const lock = getLock();

  return lock.then((result) => {
    if (result === 'pending') {
      return waitForLock();
    }

    return result;
  })
}

waitForLock().then(() => console.log("done"));
Montecamo
  • 126
  • 3
  • 1
    Basically two solutions, as always : "Promise style" with `.then()` callbacks, or "await style". – Jeremy Thille Dec 10 '20 at 15:32
  • @JeremyThille exactly ;) – Montecamo Dec 10 '20 at 15:33
  • Ok thanks, something like that would probably work, but it is kind of cheating because `getLock` still needs to return the `isPending` state (although in another way). The goal is to use _any_ promise-returning function for `getLock`, without special considerations. Sorry for having not made this clear enough, I will update the question accordingly. – Remirror Dec 10 '20 at 15:36
0

If I understand correctly what you're trying to do, you need only one function, that returns a Promise either way, and then just await it :

function getLock() {
  let promise;

  if (Math.random() > 0.5) {
    console.log("Waiting to unlock...")
    promise = new Promise((resolve, reject) => setTimeout(() => resolve("Unlocked!"), 1000));
  } else {
    console.log("Unlocking immediately.")
    promise = Promise.resolve("Unlocked!");
  }
  return promise;
};

// Testing the above function
(async() => {
  for (let i in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) {
    console.log(await getLock())
  }
  console.log("All done")
})()

You can even await all of them in parallel :

function getLock() {
  let promise;

  if (Math.random() > 0.5) {
    console.log("Waiting to unlock...")
    promise = new Promise((resolve, reject) => setTimeout(() => resolve("Unlocked!"), 1000));
  } else {
    console.log("Unlocking immediately.")
    promise = Promise.resolve("Unlocked!");
  }
  return promise;
};

// Testing the above function
(async() => {
  await Promise.all([1,2,3,4,5,6,7,8,9,10].map(getLock))
  console.log("All done")
})()
Jeremy Thille
  • 26,047
  • 12
  • 43
  • 63
  • The problem is that after the lock has been unlocked once, it could be relocked again due to new locking conditions that have arisen during the wait, so in the "waiting" case, I have to recheck it. I admit that this is unfortunately hard to see with my toy example here, but it is not easy to present otherwise. – Remirror Dec 10 '20 at 15:45
  • Ha, it's getting more complex. I guess you need a Lock class, with a `this.locked` state, and every time this state is changed by something, an unlocking process starts automatically (`this.startUnlocking()`), which eventually will end up with `this.locked=false;` – Jeremy Thille Dec 10 '20 at 15:50
  • Nice idea, I really have to think about the approach again. Thanks so far. – Remirror Dec 10 '20 at 16:02