Wait until a condition is true?
Asked Answered
J

9

32

I'm using navigator.geolocation.watchPosition in JavaScript, and I want a way to deal with the possibility that the user might submit a form relying on location before watchPosition has found its location.

Ideally the user would see a 'Waiting for location' message periodically until the location was obtained, then the form would submit.

However, I'm not sure how to implement this in JavaScript given its lack of a wait function.

Current code:

var current_latlng = null;
function gpsSuccess(pos){
    //console.log('gpsSuccess');  
    if (pos.coords) { 
        lat = pos.coords.latitude;
        lng = pos.coords.longitude;
    }
    else {
        lat = pos.latitude;
        lng = pos.longitude;
    }
    current_latlng = new google.maps.LatLng(lat, lng);
}
watchId = navigator.geolocation.watchPosition(gpsSuccess,
                  gpsFail, {timeout:5000, maximumAge: 300000});
$('#route-form').submit(function(event) {
    // User submits form, we need their location...
    while(current_location==null) {
        toastMessage('Waiting for your location...');
        wait(500); // What should I use instead?
    }
    // Continue with location found...
});
Josi answered 25/8, 2011 at 15:40 Comment(5)
Think asynchronously. Look up setTimeout.Immaterialism
Asynchronously and recursively maybe - recursively call setTimeout until current_latlng has some value?Josi
understand the language first before blaming it. There is no "lack" of a wait function for sure.Glare
@Josi yes, but whatever you do use a timeout. You'd be surprised how much of the CPU's resources Javascript will use when runs continuously in any way (nearly all).Bubble
@Richard: Exactly. It's not strictly function call recursion because of the asynchronicity but, from a code point of view, sure.Immaterialism
T
8

You could use a timeout to try to re-submit the form:

$('#route-form').submit(function(event) {
    // User submits form, we need their location...
    if(current_location==null) {
        toastMessage('Waiting for your location...');
        setTimeout(function(){ $('#route-form').submit(); }, 500); // Try to submit form after timeout
        return false;
    } else {
        // Continue with location found...
    }
});
Tintinnabulum answered 25/8, 2011 at 15:45 Comment(4)
It needs to be a string, otherwise the timeout will get the value of .submit. I suppose you could do function(){ return $('#route-form').submit(); }. I think that would work.Tintinnabulum
I don't understand your first sentence at all. And, yes, a function expression is the proper way to go. :)Immaterialism
May be over-optimizing but instead of function(){ $('#route-form').submit(); }, couldn't you do $('#route-form').submit?Bubble
Yeah, you could also do that, might be a bit cleaner.Tintinnabulum
S
42

Modern solution using Promise

function waitFor(conditionFunction) {

  const poll = resolve => {
    if(conditionFunction()) resolve();
    else setTimeout(_ => poll(resolve), 400);
  }

  return new Promise(poll);
}

Usage

waitFor(_ => flag === true)
  .then(_ => console.log('the wait is over!'));

or

async function demo() {
  await waitFor(_ => flag === true);
  console.log('the wait is over!');
}

References
Promises
Arrow Functions
Async/Await

Stater answered 4/10, 2018 at 17:47 Comment(0)
P
29

Personally, I use a waitfor() function which encapsulates a setTimeout():

//**********************************************************************
// function waitfor - Wait until a condition is met
//        
// Needed parameters:
//    test: function that returns a value
//    expectedValue: the value of the test function we are waiting for
//    msec: delay between the calls to test
//    callback: function to execute when the condition is met
// Parameters for debugging:
//    count: used to count the loops
//    source: a string to specify an ID, a message, etc
//**********************************************************************
function waitfor(test, expectedValue, msec, count, source, callback) {
    // Check if condition met. If not, re-check later (msec).
    while (test() !== expectedValue) {
        count++;
        setTimeout(function() {
            waitfor(test, expectedValue, msec, count, source, callback);
        }, msec);
        return;
    }
    // Condition finally met. callback() can be executed.
    console.log(source + ': ' + test() + ', expected: ' + expectedValue + ', ' + count + ' loops.');
    callback();
}

I use my waitfor() function in the following way:

var _TIMEOUT = 50; // waitfor test rate [msec]
var bBusy = true;  // Busy flag (will be changed somewhere else in the code)
...
// Test a flag
function _isBusy() {
    return bBusy;
}
...

// Wait until idle (busy must be false)
waitfor(_isBusy, false, _TIMEOUT, 0, 'play->busy false', function() {
    alert('The show can resume !');
});
Photoelasticity answered 11/2, 2013 at 12:16 Comment(2)
don't forget the ); at the end of the waitfor() functionQktp
But if there's more code below the final (aka the initial) waitfor call, won't that code continue to execute, until it pauses because it's time to run the first scheduled setTimeout call? initially waitfor is just a function that sets up a setTimeout call and then returns. your code will watch until the value changes, but it won't block until the value changes.Merit
M
15

This is precisely what promises were invented and implemented (since OP asked his question) for.

See all of the various implementations, eg promisejs.org

Merit answered 5/2, 2015 at 23:39 Comment(2)
this is the only right answer to the question and it is really worrysome that it had 0 votes until nowBrattice
@HansWesterbeek probably because the concept of promises is a broad subject. I already had a feeling that I should use a promise, but this answer is too vague to help me.Meed
B
14

You'll want to use setTimeout:

function checkAndSubmit(form) {
    var location = getLocation();
    if (!location) {
        setTimeout(checkAndSubmit, 500, form); // setTimeout(func, timeMS, params...)
    } else {
        // Set location on form here if it isn't in getLocation()
        form.submit();
    }
}

... where getLocation looks up your location.

Bubble answered 25/8, 2011 at 15:43 Comment(1)
You can also put flag in gpsFail then check for this in checkAndSubmit and showing proper message.Poore
T
8

You could use a timeout to try to re-submit the form:

$('#route-form').submit(function(event) {
    // User submits form, we need their location...
    if(current_location==null) {
        toastMessage('Waiting for your location...');
        setTimeout(function(){ $('#route-form').submit(); }, 500); // Try to submit form after timeout
        return false;
    } else {
        // Continue with location found...
    }
});
Tintinnabulum answered 25/8, 2011 at 15:45 Comment(4)
It needs to be a string, otherwise the timeout will get the value of .submit. I suppose you could do function(){ return $('#route-form').submit(); }. I think that would work.Tintinnabulum
I don't understand your first sentence at all. And, yes, a function expression is the proper way to go. :)Immaterialism
May be over-optimizing but instead of function(){ $('#route-form').submit(); }, couldn't you do $('#route-form').submit?Bubble
Yeah, you could also do that, might be a bit cleaner.Tintinnabulum
T
2
export default (condition: Function, interval = 1000) =>
  new Promise((resolve) => {
    const runner = () => {
      const timeout = setTimeout(runner, interval);

      if (condition()) {
        clearTimeout(timeout);
        resolve(undefined);
        return;
      }
    };

    runner();
  });
Tice answered 26/12, 2021 at 18:55 Comment(0)
D
0
class App extends React.Component {

componentDidMount() {
   this.processToken();

}
 processToken = () => {
    try {
        const params = querySearch(this.props.location.search);
        if('accessToken' in params){
            this.setOrderContext(params);
            this.props.history.push(`/myinfo`);
        }
    } catch(ex) {
      console.log(ex);
    }
    }

   setOrderContext (params){
     //this action calls a reducer and put the token in session storage
     this.props.userActions.processUserToken({data: {accessToken:params.accessToken}});
}

render() {
    return (

        <Switch>
            //myinfo component needs accessToken to retrieve my info
            <Route path="/myInfo" component={InofUI.App} />
        </Switch>

    );
}

And then inside InofUI.App

componentDidMount() {
        this.retrieveMyInfo();
    }

    retrieveMyInfo = async () => {
        await this.untilTokenIsSet();
        const { location, history } = this.props;
        this.props.processUser(location, history);
    }

    untilTokenIsSet= () => {
        const poll = (resolve) => {
            const { user } = this.props;
            const { accessToken } = user;
            console.log('getting accessToken', accessToken);
            if (accessToken) {
                resolve();
            } else {
                console.log('wating for token .. ');
                setTimeout(() => poll(resolve), 100);
            }
        };
        return new Promise(poll);
    } 
Doable answered 13/1, 2019 at 20:20 Comment(0)
E
0

Try using setInterval and clearInterval like this...

var current_latlng = null;

function gpsSuccess(pos) {
    //console.log('gpsSuccess');  
    if (pos.coords) {
        lat = pos.coords.latitude;
        lng = pos.coords.longitude;
    } else {
        lat = pos.latitude;
        lng = pos.longitude;
    }
    current_latlng = new google.maps.LatLng(lat, lng);
}
watchId = navigator.geolocation.watchPosition(gpsSuccess,
    gpsFail, {
        timeout: 5000,
        maximumAge: 300000
    });
$('#route-form').submit(function (event) {
    // User submits form, we need their location...
    // Checks status every half-second
    var watch = setInterval(task, 500)

    function task() {
        if (current_latlng != null) {
            clearInterval(watch)
            watch = false
            return callback()
        } else {
            toastMessage('Waiting for your location...');

        }
    }

    function callback() {
        // Continue on with location found...
    }
});
Eberta answered 5/10, 2019 at 1:41 Comment(0)
I
0

This accepts any function, even if it's async, and when it evaluates to a truthy value (checking every quarter-second by default), resolves to it.

function waitFor(condition, step = 250, timeout = Infinity) {
  return new Promise((resolve, reject) => {
    const now = Date.now();
    let running = false;
    const interval = setInterval(async () => {
      if (running) return;
      running = true;
      const result = await condition();
      if (result) {
        clearInterval(interval);
        resolve(result);
      } else if (Date.now() - now >= timeout * 1000) {
        clearInterval(interval);
        reject(result);
      }
      running = false;
    }, step);
  });
}

Example

example();

async function example() {
  let foo = 'bar';
  setTimeout(() => foo = null, 2000);
  console.log(`foo === "${foo}"`);
  
  await waitFor(() => foo === null);
  console.log('2 seconds have elapsed.');
  
  console.log(`foo === ${foo}`);
}

function waitFor(condition, step = 250, timeout = Infinity) {
  return new Promise((resolve, reject) => {
    const now = Date.now();
    let running = false;
    const interval = setInterval(async () => {
      if (running) return;
      running = true;
      const result = await condition();
      if (result) {
        clearInterval(interval);
        resolve(result);
      } else if (Date.now() - now >= timeout * 1000) {
        clearInterval(interval);
        reject(result);
      }
      running = false;
    }, step);
  });
}
Intersect answered 2/8, 2022 at 5:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.