Home > other >  Aggregate multiple calls then separate result with Promise
Aggregate multiple calls then separate result with Promise

Time:08-01

Currently I have many concurrent identical calls to my backend, differing only on an ID field:

getData(1).then(...) // Each from a React component in a UI framework, so difficult to aggregate here
getData(2).then(...)
getData(3).then(...)

// creates n HTTP requests... inefficient
function getData(id: number): Promise<Data> {
  return backend.getData(id);
}

This is wasteful as I make more calls. I'd like to keep my getData() calls, but then aggregate them into a single getDatas() call to my backend, then return all the results to the callers. I have more control over my backend than the UI framework, so I can easily add a getDatas() call on it. The question is how to "mux" the JS calls into one backend call, the "demux" the result into the caller's promises.

const cache = Map<number, Promise<Data>>()
let requestedIds = []
let timeout = null;

// creates just 1 http request (per 100ms)... efficient!
function getData(id: number): Promise<Data> {
  if (cache.has(id)) {
    return cache;
  }
  
  requestedIds.push(id)
  if (timeout == null) {
    timeout = setTimeout(() => {
      backend.getDatas(requestedIds).then((datas: Data[]) => {
        // TODO: somehow populate many different promises in cache??? but how?
        requestedIds = []
        timeout = null
      }
    }, 100)
  }
  return ???
}

In Java I would create a Map<int, CompletableFuture> and upon finishing my backend request, I would look up the CompletableFuture and call complete(data) on it. But I think in JS Promises can't be created without an explicit result being passed in.

Can I do this in JS with Promises?

CodePudding user response:

A little unclear on what your end goal looks like. I imagine you could loop through your calls as needed; Perhaps something like:

for (let x in cache){
   if (x.has(id))
      return x;
   } 

//OR

for (let x=0; x<id.length;x  ){
  getData(id[x])
}



Might work. You may be able to add a timing method into the mix if needed.

Not sure what your backend consists of, but I do know GraphQL is a good system for making multiple calls.

It may be ultimately better to handle them all in one request, rather than multiple calls.

CodePudding user response:

I think I've found a solution:

const data: Map<number, Promise<Data>> = new Map();
const requestedData: Map<number, PromiseContainer<Data>> = new Map();
let timeout = null;
function getData(id: number) {
  if (data.has(id)) {
    return data.get(id);
  }
  const promise = new Promise<Data>((resolve, reject) => requestedData.set(id, { resolve, reject }))
  if (timeout == null) {
    timeout = setTimeout(() => {
      backend.getDatas([...requestedData.keys()]).then(datas => {
        for (let [id, data] of Object.entries(datas)) {
          requestedData[id].resolve(data)
        }
        requestedData.clear()
        timeout = null
      }).catch(e => {
        Object.values(requestedData).map(promise => promise.reject(e)))
        timeout = null
      })
    }, 100)
  }
  return promise;
}

The key was figuring out I could extract the (resolve, reject) from a promise, store them, then retrieve and call them later.

  • Related