Search code examples
javascriptasynchronouspromisechaining

Chaining unlimited promises


I am trying to do an (expensive) computation from a function which gets called very often and needs to return as quickly as possible. The computation itself doesn't need to be done quickly, it's more important for the computations to occur after each other.

I had the idea of chaining promises together with this and the following seems to work, but I am unsure whether this is a bad way of doing it:

let promiseChain = Promise.resolve();
function func() {
  (async () => {
    promiseChain = promiseChain.then(() => {
      doComputation(); // returns nothing
    });
  })();
}

(Edit)
Some more details:
The func I'm writing looks actually more like this:

function func() { // gets called everytime the DOM changes
  const node = capture(document); // Essentially this clones the DOM
  (async () => {
    promiseChain = promiseChain.then(() => {
      doComputation(node); // returns nothing
    });
  })();
}

The reason func needs to exit so quickly is that DOM changes may occur in quick succession and I want to capture all changes. This is why I thought exiting func quickly by running it's content asynchronously might solve that. The computation itself can be synchronous (and needs to be in order) and I thought chaining them together with then would achieve that.


Solution

  • JS is a single thread language. You cannot make sync calculations on the main thread as async. So your promise chain doesn't make sense in this particular case. YOu can just use an ordinary sync code, for example loop through your calculation functions executing them.

    If you want execute a chain of async tasks you can just loop through them with async/await.

    If you want to do expensive calculations without blocking the UI use Web workers: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers

    A simple solution to execute computations in a worker queued could be like that:

    let taskId = 1;
    
    const executeFunctionInWorker = function(fn){
    
      const id = taskId++;
      
      return new Promise(resolve => {
        const blob = new Blob([`
        let start = performance.now();
        (${fn.toString()})();
        postMessage({duration: performance.now() - start});
        `], {type: 'application/javascript'});
        const worker = new Worker(URL.createObjectURL(blob));
        worker.addEventListener('message', e => {
          resolve(`Finished task ${id} in ${e.data.duration}ms`);
        });
      });
      
    };
    
    const doComputation = () => {
      let count = 1000;
      while(count--){
        structuredClone(Array.from({length: 5000}, () => Math.random()));      
      }
    };
    
    class PromiseQueue{
      tasks = [];
      async push(task){
        const starting = this.tasks.length === 0;
        this.tasks.push(task);
        if(starting){
          let task;
          while(task = this.tasks[0]){
            const result = await task();
            this.tasks.shift();
            console.log(result);
          }
        }
      }
    };
    
    const queue = new PromiseQueue;
    
    const push = () => Array.from({length: 5}, () => queue.push(() => executeFunctionInWorker(doComputation)));
    <button onclick="push()">Queue 5 tasks</button>
    <div>You can press any number you want without waiting the previous tasks to complete</div>