okgr

Mutex in Javascript (Typescript if you will)

Motivation

Let’s talk about the problem statement. We have to report some transactions to some analytics service. When authenticating with this service, we need to hit an endpoint with a clientId and secret to get an authentication token then use this token to make the subsequent requests.

This token for making authenticated requests does expire and the time it takes to expire is stated in the response when retrieving the token. To make this vivid, here’s what the code looks like:

const clientSecret = env.CLIENT_SECRET
const clientId = env.CLIENT_ID

async function getToken(): { token: string, expiresOn: number } {
  const res = await fetch(constants.ANALYTICS_AUTHENTICATION_ENDPOINT, {
    method: "POST",
    body: JSON.stringify({ clientId, clientSecret })
  })

  const data = await res.json()

  return data
}

Now when reporting these transactions, we can implement a function to report each transaction like this:

async function reportTransaction(transaction: T) {
  const token = await getToken()
  const res = await fetch(constants.ANALYTICS_REPORT, {
    method: "POST",
    body: JSON.stringify({ transaction })
  })

  return await res.json()
}

// report each transaction
for (const transaction of transactions) {
  await reportTransaction(transaction)
}

Before we continue, there’s an issue. Our implementation of reporting one by one retrieved a new token every time. We don’t want this. We should get the token once and if it’s expired we request a new one. Let’s do this:

const Authenticator = {
  token: null as string | null,
  expires: null as  Date | null,
  async getToken() {
    if (!token || this.expired) {
      console.log('[Authenticator] retrieving new token…')
      const res = await fetch(constants.ANALYTICS_AUTHENTICATION_ENDPOINT, {
        method: "POST",
        body: JSON.stringify({ clientId, clientSecret })
      })

      const data = await res.json()
      this.token = data.token
      this.expires = new Date(data.expiresOn)
    }

    return this.token
  },
  get expired() {
    // npm install dayjs
    return this.expires === null || dayjs().isAfter(expires)
  }
}

Then we update our reporting code like:

async function reportTransaction(transaction: T) {
  const token = await getToken()
  const token = await Authenticator.getToken()
  // ...
}

This will only get a new token when necessary.

But if we have thousands of transactions, we wouldn’t want to do this one by one. It’ll be slow. We can parallelize them. We could use Promises or queues or parallel.

For simplicity, let’s try to use Promises but with a slice of the transactions:

const sliced = transactions.slice(10)
await Promise.all(sliced.map((transaction) => reportTransaction(transaction)))

If you attempt to do this, you’ll notice that the console will log

[Authenticator] retrieving new token… (10)

The (10) at the end signifiying it has been logged 10 times.

The reason this is happening is because the Authenticator.get was triggered the same time 10 times. Because it takes time to resolve, by the time each promise tries to access .token, it would be null so it attempts to request for the token. This same concept applies to using the other methods: queues, parallel, etc.

This is not what we want. We only want to request for tokens once until they’re expired.

Mutex

One blessing learning the Go gave me was the idea of Mutex. Is simpler words, mutex will allow access to a resource one at a time. So let’s use a mutex:

npm i async-mutex

The we rewrite our Authenticator like this:

import { Mutex } from 'async-mutex'

const mutex = new Mutex()

const Authenticator = {
  // ...
  async getToken() {
    await mutex.runExclusive(async () => {
      if (!token || this.expired) {
        console.log('[Authenticator] retrieving new token…')
        const res = await fetch(constants.ANALYTICS_AUTHENTICATION_ENDPOINT, {
          method: "POST",
          body: JSON.stringify({ clientId, clientSecret })
        })

        const data = await res.json()
        this.token = data.token
        this.expires = new Date(data.expiresOn)
      }
    })

    return this.token
  },
  // ...
}

This saves the day. Tokens will only be requested when they expire and only just once.

I hope this saves your day one day.