Search code examples
javascriptpromisemutex

Why does this promise resolve to a function (unlock)?


I'm following the example in this article https://spin.atomicobject.com/2018/09/10/javascript-concurrency/:

What we need here is basically a mutex: a way to say that the critical section of reading the collection, updating it, and writing it back cannot be happening simultaneously. Let’s imagine we had such a thing [...]:

const collectionMutex = new Mutex();

async function set(collection: string, key: string, value: string): {[key: string]: string} {
  return await collectionMutex.dispatch(async () => {
    const data = await fetchCollection(collection);
    data[key] = val;
    await sendCollection(collection, data);
    return data;
  });
}

Implementing this mutex requires a bit of promise-trampoline-ing, but it’s still relatively straightforward:

class Mutex {
  private mutex = Promise.resolve();

  lock(): PromiseLike<() => void> {
    let begin: (unlock: () => void) => void = unlock => {};

    this.mutex = this.mutex.then(() => {
      return new Promise(begin);
    });

    return new Promise(res => {
      begin = res;
    });
  }

  async dispatch(fn: (() => T) | (() => PromiseLike<T>)): Promise<T> {
    const unlock = await this.lock();
    try {
      return await Promise.resolve(fn());
    } finally {
      unlock();
    }
  }
}

In the dispatch function of the Mutex class, unlock is set to await this.lock().

My question is: how and why is unlock a function when lock() returns a Promise that doesn't resolve to anything; the Promise just sets begin = res.


Solution

  • Here is how it works:

    There are two promises involved that are created with new Promise. One of them is the promise that lock returns. Let's call that promise P1. The other one is created later, in the callback passed to this.mutex.then. Let's call that promise P2.

    res is a function which, when called, resolves P1. But it is not called immediately. Instead, begin is made to reference that same function (begin = res) so we can access it later.

    When the callback given to this.mutex.then gets executed (which is when the most recent lock is released), the main magic happens:

    new Promise(begin) will execute begin. It looks strange, as normally you would provide an inline callback function where you would perform the logic that has some asynchronous dependency and then have it call resolve -- the argument that this callback function gets. But here begin is that callback function. You could write the creation of the P2 promise more verbose, by providing an inline function wrapper to the constructor, like this:

    new Promise(resolve => begin(resolve));
    

    As indicated above, calling begin will resolve P1. The argument passed to begin will be the resolve function that the promise constructor provides to us so we can resolve that new P2 promise. This (function) argument thus becomes the resolution value for P1, and yes, it is a function. This is what the await-ed expression resolves to. unlock is thus a resolve function for resolving P2.