JS array.map with costly async function

I have a set of users. I need to call “costlyEncryptionFunction” on each user.id, but I don’t want to call “costlyEncryptionFunction” multiple times on the same user.id.

Here is a working example:

const costlyEncryptionFunction = async (id) => {
  return new Promise((res) => {
    setTimeout(() => {
      res(id + 1000)
    }, 1000)
  })
}  

let users = [
  {id: 1, item: 1},
  {id: 1, item: 2},
  {id: 2, item: 5},
  {id: 2, item: 6}
]

let currentUserId
users.map(async (user) => {

  let encryptedUserId
  if(!currentUserId || currentUserId != user.id){
    currentUserId = user.id
    encryptedUserId = await costlyEncryptionFunction(currentUserId)
  }

  if(encryptedUserId){
    console.log(`inside: ${encryptedUserId} ${user.item} ... do more stuff` )
  }
})

The output reads

inside: 1001 1 ... do more stuff
inside: 1002 5 ... do more stuff

I am trying to have my output read:

inside: 1001 1 ... do more stuff
inside: 1001 2 ... do more stuff
inside: 1002 5 ... do more stuff
inside: 1002 6 ... do more stuff

Does anyone have a solution to this, other than calling “costlyEncryptionFunction” and repeating the same userId multiple times?

Answer

The most basic way to do this is to use an object as a cache:

const cache = {};

const costlyEncryptionFunction = async (id) => {
    const existing = cache[id];
    if (existing) return Promise.resolve(existing);
    
    return new Promise((res) => {
        setTimeout(() => {
            const encrypted = id + 1000;
            cache[id] = encrypted;
            res(encrypted);
        }, 1000)
    })
}

let users = [
    { id: 1, item: 1 },
    { id: 1, item: 2 },
    { id: 2, item: 5 },
    { id: 2, item: 6 }
]

let currentUserId
users.map(async (user) => {

    let encryptedUserId
    if (!currentUserId || currentUserId != user.id) {
        currentUserId = user.id
        encryptedUserId = await costlyEncryptionFunction(currentUserId)
    }

    if (encryptedUserId) {
        console.log(`inside: ${encryptedUserId} ${user.item} ... do more stuff`)
    }
})

If your target environment supports it, you can use a Map:

const cache = new Map();

const costlyEncryptionFunction = async (id) => {
    const existing = cache.has(id) ? cache.get(id) : undefined;
    if (existing) return Promise.resolve(existing);

    return new Promise((res) => {
        setTimeout(() => {
            const encrypted = id + 1000;
            cache.set(id, encrypted) = encrypted;
            res(encrypted);
        }, 1000)
    })
}

let users = [
    { id: 1, item: 1 },
    { id: 1, item: 2 },
    { id: 2, item: 5 },
    { id: 2, item: 6 }
]

let currentUserId
users.map(async (user) => {

    let encryptedUserId
    if (!currentUserId || currentUserId != user.id) {
        currentUserId = user.id
        encryptedUserId = await costlyEncryptionFunction(currentUserId)
    }

    if (encryptedUserId) {
        console.log(`inside: ${encryptedUserId} ${user.item} ... do more stuff`)
    }
})

This is more or less what utility libraries do under the hood with their memoize methods, though they typically offer features such as multiple parameters, which complicates the implementation a bit.

Leave a Reply

Your email address will not be published. Required fields are marked *