Polling until getting specific result?
Asked Answered
C

9

11

I am currently trying to add polling to my application using this link https://davidwalsh.name/javascript-polling (and many others).

I have access to the following already implemented api:

client.get('url')
// returns Promise with result of getting result from url
// for the application I am working on,
//     the URL returns json that looks like the following 
//     {status: DONE or IN PROGRESS, other values...}
// when status is DONE other values are what I will use in the application

client.post('url', {data: passAnyDataHere}) 
// sends a post request with result of sending data to url
// starts the specific job

One of the problems that I have run into is that while trying to adapt the JavaScript Polling code I linked to above, when I find out that the status is DONE, I have no way of returning the result to outside of the Promise. Can someone give me tips on how to do this? (polling until I find a specific value, and then return the value for use later)

Let me give you an example

export default function someFunction() {
    let a = client.get('/status');
    a.then( dataResult => 
      {
         if (dataResult.status == "DONE") {
            //** want to get other values in dataResult here 
            // and store it somewhere else for use later
         }
      });
    // ***want to work with results here.
    // need some way to get the status of what happened inside the .then(..) part
   //  eventually have to return success or failure and results to the frontend
   // (this part is already done)
 }

The base of the code is https://github.com/erikras/react-redux-universal-hot-example#server-side-data-fetching (uses React.js/Node.js/Redux/etc.)

Any tips/suggestions/help are/is appreciated. Thanks!

Also, the application I am working on does not use JQuery.

Combine answered 13/9, 2017 at 22:56 Comment(0)
P
28

Here's an more extensible solution based on the post polling with async/await

Just add the following utility methods:

const poll = async function (fn, fnCondition, ms) {
  let result = await fn();
  while (fnCondition(result)) {
    await wait(ms);
    result = await fn();
  }
  return result;
};

const wait = function (ms = 1000) {
  return new Promise(resolve => {
    setTimeout(resolve, ms);
  });
};

Then you can call it like this:

let fetchReport = () => axios.get(reportUrl);
let validate = result => !result.data.summary;
let response = await poll(fetchReport, validate, 3000);
Phylis answered 2/11, 2020 at 22:23 Comment(5)
Cleanest solution! ThanksCantonese
So does this mean if fn takes a long time to finish, then the next poll won't happen until fn returns, right? For example, let's say fetchReport takes 6 seconds to finish, and ms is 3000 (or 3s). This means the API call takes 6 seconds > wait another 3 seconds to make the next API call > takes another 6 seconds > wait another 3 seconds, etc. So this means the interval between each poll is 9s. Am I understanding this right?Tolmann
@7ball, yes, that's correct. If you have a really long running API, whatever time it takes will be included in each loop. You could fire something with an interval every n seconds, but probably better to avoid overlapping identical calls. If your API call takes 6 seconds, and you don't want to wait the other 3, you could just reduce the extra wait timePhylis
No this is actually exactly what I needed, so thanks so much! To your point, I don't want the next poll to happen if the previous one is still in flight. I just want to ensure at the minimum an API call is made every 3s.Tolmann
2 am and I can finally go to sleep. Thank you so much!Incorruptible
M
5

Recent versions of Node.js, have support for async / await.

Below is an example of using it..

One major advantage of async / await, it's very easy to follow the code, and understand it's logic. If for example you wanted to extend this, to have a max try's feature, it would be trivial to do. (hint) It's just a for loop :)

let count = 0;

var client = {
  get: function () {
    return new Promise(function (resolve, reject) {
      count ++;
      setTimeout(function () {
        if (count > 4) resolve({status:'DONE',otherStuff:'Other Stuff'});
        else resolve({status: `count: ${count}`});
      }, 1000);
    });
  }
}


async function someFunction() {
  while (true) {
    let dataResult = await client.get('/status');
    console.log(dataResult.status);
    if (dataResult.status == "DONE") {
      return dataResult;
    }
  }
}

(async () => { let r = await someFunction(); console.log(r); })();
Marchese answered 13/9, 2017 at 23:36 Comment(0)
E
5

Here's an example of a function that uses promises and polls until you get the desired result. I've also parameterized it so that you can pass in the polling interval and a timeout value:

// create a promise that resolves after a short delay
function delay(t) {
    return new Promise(function(resolve) {
        setTimeout(resolve, t);
    });
}

// interval is how often to poll
// timeout is how long to poll waiting for a result (0 means try forever)
// url is the URL to request
function pollUntilDone(url, interval, timeout) {
    let start = Date.now();
    function run() {
        return client.get(url).then(function(dataResult) {
            if (dataResult.status === "DONE") {
                // we know we're done here, return from here whatever you 
                // want the final resolved value of the promise to be
                return dataResult;
            } else {
                if (timeout !== 0 && Date.now() - start > timeout) {
                    throw new Error("timeout error on pollUntilDone");
                } else {
                    // run again with a short delay
                    return delay(interval).then(run);
                }
            }
        });
    }
    return run();
}

// sample usage
// polls every 500ms for up to 30 seconds
pollUntilDone(someUrl, 500, 30 * 1000).then(function(result) {
   // have final result here 
}).catch(function(err) {
    // handle error here
});

The key here is to chain your promises so each time you call run() again, you return its value so it is chained to the prior promise. Then, whenever you finally return a value or throw an exception, the original promise will get that value or error as the resolved value or rejected reason.

Note that I added a timeout because you really never want to be polling forever, particularly in cases where some unforeseen and recurring error might not reject the promise, but won't get you the result you want.

Embay answered 13/9, 2017 at 23:54 Comment(3)
How would this refactor to not nest the promises?Donnelly
@Jonathon - What nesting are you talking about? This is pseudo-recursive, not nested. If you are going to do conditional branching inside a .then(), you pretty much have to do it the way it's done here. I don't understand what nesting you want to remove?Embay
so sorry - posted to wrong page - impressive response time for a year old post ;)Donnelly
F
3

I just had to solve a similar problem. Below is a gist of my solution:

// api call with interval until receiving a specific data.

const callApi = () => {
  return new Promise((resolve, reject) => {
    console.log('calledAPI!');
    setTimeout(()=>{
      var myNumber = parseInt(Math.random()*10, 10);
      resolve(myNumber);
    }, 1000);
  });
}

const callIntervalFunc = () => { // need to wrap it with a function because setInterval is not a function and cannot call it from outside otherwise.
  const callInverval = setInterval(async()=>{
    console.log('checking the api...');

    var responseNumber = await callApi();
    console.log('Got response! ',responseNumber);
    if (responseNumber === 5) {
      clearInterval(callInverval);
      console.log('responseNumber is 5!! ends.');
    }
  }, 2000);
}

callIntervalFunc();
Flee answered 29/10, 2018 at 17:34 Comment(1)
Thanks! This solution worked for me. May be obvious to others, but if you're awaiting an API response in the innards of callApi, you'll need to add async before the function passed to new Promise.Three
H
1

Following continuousPromise method will do the job, it requires 2 parameters:

  1. Promise responsible for fetching data

  2. delay (in ms) in subsequent calls

    let exit = false;
    const continuousPromise = (promise, interval)  => {
        const execute = () => promise().finally(waitAndExecute);
        const waitAndExecute = () => {
            if (exit) {
                return;
            }
            setTimeout(execute, interval)
        };
        execute();
    }
    

How to use

continuousPromise(() => {
    return axios.get("something.json")
        .then((res) => {
            if (res && res.status !== 'PENDING') { // Check here for expected result
                exit = true;
            }
        })
        .catch((error) => {
            exit = true;
            console.log(error);
        })
}, 1000);
Heartsick answered 30/9, 2020 at 5:3 Comment(0)
P
1

Here's a simple alternative

(function poll(){

    //do the check - ajax, etc.

    if (condition_that_means_were_done) {
        return
    }   
        
    setTimeout(poll,timeout_ms)
})();
Pellitory answered 12/1, 2022 at 13:49 Comment(0)
G
0

One option is to alter the poll function to only resolve when your required conditions have been met:

function poll(pollFn, interval = 100) {
    var intervalHandle = null

    return {
        until(conditionFn) {
            return new Promise((resolve, reject) => {
                intervalHandle = setInterval(() => {
                    pollFn().then((data) => {
                        let passesCondition = false;
                        try {
                            passesCondition = conditionFn(data);
                        } catch(e) {
                            reject(e);
                        }
                        if (passesCondition) {
                            resolve(data);
                            clearInterval(intervalHandle);
                        }
                    }).catch(reject)
                }, interval)
            })
        }
    }
}

var counter = 0;

function getStatus() {
    if (counter++ === 5) {
       return Promise.resolve({ status: 'DONE', otherStuff: 'hi' });
    }
    console.log('not DONE, keep going')
    return Promise.resolve({ status: 'WORKING' });
}

poll(getStatus, 500)
  .until(data => data.status === 'DONE')
  .then((data) => {
    // do something with the data
    console.log('status is DONE', data)
  })
Given answered 13/9, 2017 at 23:25 Comment(0)
T
0

In someFunction(), it returning a new Promise which issued a resolve() and pass the processed result as parameter. In getpolledresult(), catch the processed result to determine whether to poll or not.

function getSearchResults(term) {
  return new Promise(resolve => {
    let timeout = 100 + Math.floor(Math.random() * 1900);
    console.log("is number < 500?", timeout);
    let result = {
      status: "",
      term_lower: term.toLowerCase(),
      term_upper: term.toUpperCase()
    };
    if (timeout < 500) {
      result.status = "DONE"
    }
    setTimeout(() => resolve(result), timeout);
  });
}

let cancelCallback = () => {};
let cancelflag = 0;

var sleep = (period) => {
  return new Promise((resolve) => {
    cancelCallback = () => {
      console.log("timeout...");
      // send cancel message...
      cancelflag = 1;
      return resolve('Canceled');
    }
    setTimeout(() => {
      resolve("tick");
    }, period)
  })
}

let asleep = async (period) => {
  let respond = await sleep(period);
  // if you need to do something as soon as sleep finished
  /* console.log("sleep just finished, do something...") */
  return respond;
}


function someFunction() {
  return new Promise((resolve) => {
    console.log("polling...");
    /* let a = client.get('/status'); */
    let a = getSearchResults('a');
    let processedResult = {
      status: ""
    };
    a.then(dataResult => {
      if (dataResult.status == "DONE") {
        //** want to get other values in dataResult here
        // and store it somewhere else for use later
        processedResult.status = "OK";
      };
      resolve(processedResult);
    });
  });
}


var getpolledresult = (promiseFn, period, timeout) => promiseFn().then((result) => {
  // ***want to work with results here.
  // need some way to get the status of what happened inside the .then(..) part
  //  eventually have to return success or failure and results to the frontend
  // (this part is already done)
  console.log(result);

  if (result.status !== "OK") {
    asleep(period).then((respond) => {
      // check if sleep canceled, if not, continue to poll
      if (cancelflag !== 1) {
        poll(promiseFn, period, timeout);
      }
    })
  }

});


var poll = (promiseFn, period, timeout) => {
  // just check if cancelCallback is empty function, 
  // if yes, set a time out to run cancelCallback()
  if (cancelCallback.toString() === "() => {}") {
    console.log("set timout...")
    setTimeout(() => {
      cancelCallback()
    }, timeout);
  }

  getpolledresult(promiseFn, period, timeout);
}


poll(someFunction, 1000, 10000);
Tellurion answered 22/11, 2019 at 16:25 Comment(0)
M
0

let count = 0;

var client = {
  get: function () {
    return new Promise(function (resolve, reject) {
      count ++;
      setTimeout(function () {
        if (count > 4) resolve({status:'DONE',otherStuff:'Other Stuff'});
        else resolve({status: `count: ${count}`});
      }, 1000);
    });
  }
}


async function someFunction() {
  while (true) {
    let dataResult = await client.get('/status');
    console.log(dataResult.status);
    if (dataResult.status == "DONE") {
      return dataResult;
    }
  }
}

(async () => { let r = await someFunction(); console.log(r); })();

ChugalKhordidi.blogspot.com

Manchester answered 4/6 at 4:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.