Locking in TypeScript

If you've worked in a non-JavaScript language, you're likely familiar with the concept of a lock. Locks are useful in multi-threaded environments to ensure access to a given variable cannot be modified by two threads concurrently. This functionality is usually provided by a runtime structure such as a semaphore. Some other languages even provide a locking mechanism as first-class citizen via a language keyword, for example, C# provides the lock statement.

However, the concept of a lock in a JavaScript environment is not as common. This is due to the fact that most JavaScript environments are single threaded. There is no reason to obtain a lock because the code cannot execute simultaneously.

That being said, there are still cases where a lock-like mechanism can be useful: when working with asynchronous code. I've been working on mobile applications written in TypeScript over the last several years and this is scenario that I've ran into several times:

  • On app startup, several HTTP API calls need to be made
  • Calls are not centralized; they're made from several different locations (screens)
  • All API calls have authentication prerequisites (e.g. a cookie or JWT must have been obtained first)

Usually this means that there will be a central function used to handle the authentication logic, lets call it executeApiRequest:

export async function executeApiRequest(apiUrl: string): Promise<Response> {  
  if (!authenticated) {
    // Makes API request and stores token into storage
    await obtainToken();
  }

  // Includes a header with authentication token
  const options = getRequestOptions();

  return fetch(apiUrl, options);
}

Internally, an obtainToken method will be responsible for determining the authentication state, and if a cookie/JWT/etc is needed, it will make an API request to obtain said token. After the token is obtained, the method will issue the original request.

This works great in most cases, However if multiple call sites use the method at the same time (such as app startup), it can result in several API requests to obtain an authentication going out in parallel.

We want to ensure that only a single authentication API request is made, otherwise the user could be issued multiple tokens or experience other undefined behavior.

This is where a lock becomes useful; we want to ensure that only a single asynchronous call to a function can be occurring at a given time. All future calls should wait until the first has resolved and then execute in order after that.

We can achieve this using a list of promises and a bit of logic in a function I've called doWithLock:

export async function doWithLock<T>(lockName: string, task: () => Promise<T>): Promise<T> { ... }  

Which can be used in the original example, like this:

export async function executeApiRequest(apiUrl: string): Promise<Response> {  
  if (!authenticated) {
    // Makes API request and stores token into storage
    await doWithLock('obtainToken', () => obtainToken());
  }

  // Includes a header with authentication token
  const options = getRequestOptions();

  return fetch(apiUrl, options);
}

Now, if several calls to executeApiRequest are made at the same time, they'll all "wait" at the doWithLock call site for the obtainToken function to return. Each additional call will be executed in order.

The helper function is available as a TypeScript source file (with unit tests!) in this gist on GitHub.

Author image
Northern California
A software geek who is into IoT devices, gaming, and Arcade restoration.