Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to delay execution of functions, JavaScript

Background

I am trying to create a factory function that executes a specific async function with a given delay.

For the purposes of this question, this will be the async function I refer to:

/*
 *  This is a simulation of an async function. Be imaginative! 
 */
let asyncMock = function(url) {
    return new Promise(fulfil => {

        setTimeout(() => {
            fulfil({
                url,
                data: "banana"
            });
        }, 10000);

    });
};

This function takes an url and it returns a JSON object containing that URL and some data.

All around my code, I have this function called in the following way:

asyncMock('http://www.bananas.pt')
.then(console.log);

asyncMock('http://www.berries.com')
.then(console.log);

//... badjillion more calls

asyncMock('http://www.oranges.es')
.then(console.log);

Problem

The problem here is that all these calls are made at exactly the same time, thus overloading the resources that asyncMoc is using.

Objective

To avoid the previous problem, I wish to delay the execution of all calls to asyncMoc by Xms.

Here is a graphic with what I pretend:

delayed_requests

To achieve this I wrote the following approaches:

  1. Using Promises
  2. Using setInterval

Using Promises

let asyncMock = function(url) {
  return new Promise(fulfil => {

    setTimeout(() => {
      fulfil({
        url,
        data: "banana"
      });
    }, 10000);

  });
};

let delayFactory = function(args) {

  let {
    delayMs
  } = args;

  let promise = Promise.resolve();

  let delayAsync = function(url) {
    return promise = promise.then(() => {

      return new Promise(fulfil => {
        setTimeout(() => {
          console.log(`made request to ${url}`);
          fulfil(asyncMock(url));
        }, delayMs);
      });
    });
  };

  return Object.freeze({
    delayAsync
  });
};

/*
 *  All calls to any of its functions will have a separation of X ms, and will
 *  all be executed in the order they were called. 
 */
let delayer = delayFactory({
  delayMs: 500
});

console.log('running');

delayer.delayAsync('http://www.bananas.pt')
  .then(console.log)
  .catch(console.error);

delayer.delayAsync('http://www.fruits.es')
  .then(console.log)
  .catch(console.error);

delayer.delayAsync('http://www.veggies.com')
  .then(console.log)
  .catch(console.error);

This factory has a function called delayAsync that will delay all calls to asyncMock by 500ms.However, it also forces the nest execution of the call to wait for the result of the previous one - which in not intended.

The objective here is to make three calls to asyncMock within 500ms each, and 10s after receive three responses with a difference of 500ms.

Using setInterval

In this approach, my objective is to have a factory which has an array of parameters. Then, every 500ms, the timer will run an executor which will take a parameter from that array and return a result with it:

/*
 *  This is a simulation of an async function. Be imaginative! 
 */
let asyncMock = function(url) {
  return new Promise(fulfil => {

    setTimeout(() => {
      fulfil({
        url,
        data: "banana"
      });
    }, 10000);

  });
};


let delayFactory = function(args) {

  let {
    throttleMs
  } = args;

  let argsList = [];
  let timer;

  /*
   *  Every time this function is called, I add the url argument to a list of 
   *  arguments. Then when the time comes, I take out the oldest argument and 
   *  I run the mockGet function with it, effectively making a queue.
   */
  let delayAsync = function(url) {
    argsList.push(url);

    return new Promise(fulfil => {

      if (timer === undefined) {

        console.log('created timer');
        timer = setInterval(() => {

          if (argsList.length === 0) {
            clearInterval(timer);
            timer = undefined;
          } else {
            let arg = argsList.shift();

            console.log('making  request ' + url);
            fulfil(asyncMock(arg));
          }
        }, throttleMs);

      } else {
        //what if the timer is already running? I need to somehow 
        //connect it to this call!
      }
    });
  };



  return Object.freeze({
    delayAsync
  });
};

/*
 *  All calls to any of its functions will have a separation of X ms, and will
 *  all be executed in the order they were called. 
 */
let delayer = delayFactory({
  delayMs: 500
});

console.log('running');

delayer.delayAsync('http://www.bananas.pt')
  .then(console.log)
  .catch(console.error);

delayer.delayAsync('http://www.fruits.es')
  .then(console.log)
  .catch(console.error);

delayer.delayAsync('http://www.veggies.com')
  .then(console.log)
  .catch(console.error);
// a ton of other calls in random places in code

This code is even worse. It executes asyncMoch 3 times without any delay whatsoever, always with the same parameter, and then because I don't know how to complete my else branch, it does nothing.

Questions:

  1. Which approach is better to achieve my objective and how can it be fixed?
like image 281
Flame_Phoenix Avatar asked May 27 '26 16:05

Flame_Phoenix


2 Answers

I'm going to assume you want the promises returned by delayAsync to resolve based on the promises from asyncMock.

If so, I would use the promise-based approach and modify it like this (see comments):

// Seed our "last call at" value
let lastCall = Date.now();
let delayAsync = function(url) {
  return new Promise(fulfil => {
    // Delay by at least `delayMs`, but more if necessary from the last call
    const now = Date.now();
    const thisDelay = Math.max(delayMs, lastCall - now + 1 + delayMs);
    lastCall = now + thisDelay;
    setTimeout(() => {
      // Fulfill our promise using the result of `asyncMock`'s promise
      fulfil(asyncMock(url));
    }, thisDelay);
  });
};

That ensures that each call to asyncMock is at least delayMs after the previous one (give or take a millisecond thanks to timer vagaries), and ensures the first one is delayed by at least delayMs.

Live example with some debugging info:

let lastActualCall = 0; // Debugging only
let asyncMock = function(url) {
  // Start debugging
  // Let's show how long since we were last called
  console.log(Date.now(), "asyncMock called", lastActualCall == 0 ? "(none)" : Date.now() - lastActualCall);
  lastActualCall = Date.now();
  // End debugging
  return new Promise(fulfil => {

    setTimeout(() => {
      console.log(Date.now(), "asyncMock fulfulling");
      fulfil({
        url,
        data: "banana"
      });
    }, 10000);

  });
};

let delayFactory = function(args) {

  let {
    delayMs
  } = args;

  // Seed our "last call at" value
  let lastCall = Date.now();
  let delayAsync = function(url) {
    // Our new promise
    return new Promise(fulfil => {
      // Delay by at least `delayMs`, but more if necessary from the last call
      const now = Date.now();
      const thisDelay = Math.max(delayMs, lastCall - now + 1 + delayMs);
      lastCall = now + thisDelay;
      console.log(Date.now(), "scheduling w/delay =", thisDelay);
      setTimeout(() => {
        // Fulfill our promise using the result of `asyncMock`'s promise
        fulfil(asyncMock(url));
      }, thisDelay);
    });
  };

  return Object.freeze({
    delayAsync
  });
};

/*
 *  All calls to any of its functions will have a separation of X ms, and will
 *  all be executed in the order they were called. 
 */
let delayer = delayFactory({
  delayMs: 500
});

console.log('running');

delayer.delayAsync('http://www.bananas.pt')
  .then(console.log)
  .catch(console.error);

delayer.delayAsync('http://www.fruits.es')
  .then(console.log)
  .catch(console.error);

// Let's hold off for 100ms to ensure we get the spacing right
setTimeout(() => {
  delayer.delayAsync('http://www.veggies.com')
    .then(console.log)
    .catch(console.error);
}, 100);
.as-console-wrapper {
  max-height: 100% !important;
}
like image 159
T.J. Crowder Avatar answered May 30 '26 06:05

T.J. Crowder


Okay, so here's my solution to your problem. Sorry I had to rewrite your code to better be able to understand it. I hope you can interpret it anyway and get something out of it.

Calls 500ms between eachother using Promises (JSFiddle):

function asyncFunc(url) {
    return new Promise(resolve => {
    setTimeout(function() {
        resolve({ url: url, data: 'banana' });
    }, 2000);
  });
}

function delayFactory(delayMs) {
  var delayMs = delayMs;
  var queuedCalls = [];
  var executing = false;

  this.queueCall = function(url) {
    var promise = new Promise(function(resolve) {
        queuedCalls.push({ url: url, resolve: resolve });
        executeCalls();
    });
    return promise;
  }

  var executeCalls = function() {
    if(!executing) {
      executing = true;
      function execute(call) {
        asyncFunc(call.url).then(function(result) {
            call.resolve(result);
        });
        setTimeout(function() {
            queuedCalls.splice(queuedCalls.indexOf(call), 1);
          if(queuedCalls.length > 0) {
            execute(queuedCalls[0]);
          } else {
            executing = false;
          }
        }, delayMs)
      }
      if(queuedCalls.length > 0) {
        execute(queuedCalls[0]);
      }
    }
  }
}

var factory = new delayFactory(500);
factory.queueCall('http://test1').then(console.log); //2 sec log {url: "http://test1", data: "banana"}
factory.queueCall('http://test2').then(console.log); //2.5 sec log {url: "http://test2", data: "banana"}
factory.queueCall('http://test3').then(console.log); //3 sec log {url: "http://test3", data: "banana"}
factory.queueCall('http://test4').then(console.log); //3.5 sec log {url: "http://test4", data: "banana"}
like image 26
Arg0n Avatar answered May 30 '26 04:05

Arg0n



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!