0

I have this code to get some data from an API:

const data = async () => {
  const apiResponse = await dataFromAPI();
  return apiResponse;
}

The apiResponse will always be the same for a session where this code is run. I chose this design because I would like the opportunity to chain multiple functions to the API.

I added a cache variable to short-circuit future requests.

let dataCache;

const data = async () => {
  if (dataCache) return dataCache;
  const apiResponse = await dataFromAPI();
  dataCache = apiResponse;
  return dataCache;
}

However, calling data() twice in succession will make still two API requests.

data().then(response => console.log(response))
data().then(response => console.log(response))

This makes one API request, then uses the cache variable:

data().then(response => console.log(response))
setTimeout(() => {
  data().then(response => console.log(response))
}, 100)

How can I design a singleton for this?

henryaaron
  • 6,042
  • 20
  • 61
  • 80

2 Answers2

1

I think you can work with the Promise as the thing you cache like this:

let dataCache;

const data = async () => {
  if (!dataCache) {
      dataCache = dataFromApi();
  }
  return await dataCache;
}

That way dataCache will be set to the Promise returned from the API call. If it's set, then it will be resolved already or it won't be; in either case, a second API call won't be made.

Pointy
  • 405,095
  • 59
  • 585
  • 614
  • Something to consider is how errors should be handled. Currently, they're cached as well, and the `dataFromApi()` call is never retried. – Bergi Mar 22 '23 at 22:38
  • 1
    Btw, no need to `await` before the `return`. You don't even need to make the function `async`. – Bergi Mar 22 '23 at 22:45
  • Sure, this was off the top of my head. You can move the async stuff wherever you want in this. – Pointy Mar 22 '23 at 22:57
0

Pointy's answer is classical, you can even optionally wrap everything inside an iife or so. If you want something else to experiment with you can try async generators:

async function* data() {
    const res = await dataFromAPI();
    while(true){
        yield res
    }
}

let myData = data();
myData.next() //Promise with res
myData.next() //Different Promise with same res
ibrahim tanyalcin
  • 5,643
  • 3
  • 16
  • 22
  • 1
    Not my downvote, but this does indeed look experimental and not production-ready. The classical approach is much easier to understand and use. What's the advantage of using a generator and having to call a `next()` method? – Bergi Mar 22 '23 at 23:29
  • 1- it does not need a cache variable or pollute outer scope. 2-it does not add a new call to the stack with the cost of keeping the same scope in memory (if we do not count calling `next`, which I think would be optimized by engines). But someone of course needs to profile this to make sure. – ibrahim tanyalcin Mar 22 '23 at 23:46
  • The outer scope is "polluted" by `myData`. Sure, it's easy to solve with an IIFE, but so is the traditional solution. And you absolutely *do* need to count calling `.next()`, which does run code in the scope of the generator function. I doubt this is any more efficient, neither memory nor speed wise. – Bergi Mar 22 '23 at 23:55