Array populations don't exist outside of function they were added in?

65 Views Asked by At

I'm pretty new to JavaScript and I've run into an issue I can't find a solution to. I'm developing a game for my APCS final project and I'm trying to add a leaderboard to it. To find the top players, I'm putting all the high scores in an array, sorting it from greatest to least, and then searching for 5 usernames whos scores match the first 5 numbers in the array. I'm using AppLab to create it and AppLab has a built-in database feature, and so that's what "readRecords is for. My issue though is that when I populate the array with a for loop, the populations don't exist outside of the function even though the array variable was created outside of the function, here's the code...

function leaderGrabEasy() {
  var leaderScores = [];
  var leaders = [];
  readRecords("userData",{},function(records) {
    for (var i = 0; i < records.length; i++) {
      leaderScores.push(records[i].E_highscore);
    }
    leaderScores.sort(function(a, b){return b-a});
  });
  readRecords("userData",{E_highscore:leaderScores[0]},function(records) {
    for (var i = 0; i < records.length; i++) {
      leaders.push(records[i].username);
    }
    console.log(leaders);
  });
}

The issue occurs when I try to read the database column "E_highscores" for whatever is in "leaderScores[0]"

readRecords("userData",{E_highscore:leaderScores[0]},function(records) {

But because the array is empty outside of the first function, that spot in the array is empty. Thanks in advance for any help!

-Indoors

2

There are 2 best solutions below

3
On BEST ANSWER

readRecords is an asynchronous function, correct?

The problem is that you have a race condition. That means code needs to be executed in a very specific order in order to work, but you are not currently in control of that order.

Scores get pushed to leaderScore inside the callback that gets passed to readRecords. However, on the very next line (after calling readRecords), you are trying to call readRecords again, with a value that is populated inside the callback to the first readRecords.

Basically, your code is executing in this order:

readRecords("userData",{},callback1)
readRecords("userData",{E_highscore:leaderScores[0]},callback2)
callback1()
callback2()

In fact, there's no guarantee callback1 will happen before callback2!

There's any number of tools to address this asynchronous ordering issue, like Promises and such. I'm going to go with the simplest option, and just rearrange your code. This will create nested callbacks, but for right now, it works and will (hopefully) help you to see this principle in action.

function leaderGrabEasy() {
  var leaderScores = [];
  var leaders = [];
  readRecords("userData",{},function(records) {
    for (var i = 0; i < records.length; i++) {
      leaderScores.push(records[i].E_highscore);
    }
    leaderScores.sort(function(a, b){return b-a});

    readRecords("userData",{E_highscore:leaderScores[0]},function(records) {
     for (var i = 0; i < records.length; i++) {
        leaders.push(records[i].username);
     }
    console.log(leaders);
     });
  });
}

Now the second readRecords will only happen after you have populated the array, not any time before.


This does open up other issues with re-usability. What if you want to later do more complex things, or add more callbacks, perhaps even a loop? Well that's going to be on you haha. But you can definitely do it successfully if you keep in mind the principle we discussed here: any asynchronous callback can be called at any time really, so always right your code with that in mind.

Ask yourself the question: will my code work if this callback is called inline (synchronously, in the order it is written), in 1ms, 500ms, and 1 minute? If so, then it is logically sound.

1
On

It would probably be better to convert your function to promise returning functions instead of nesting callbacks. The code may be easy to maintain now but when you write more and more it will get harder to untangle afterwords.

//readRecords as a promise returning function
function readAsPromise(str,obj){
  return new Promise(
    function (resolve,reject){
      readRecords(
        str,
        obj,
        function(records,error) {//this readRecords cannot fail?
          if(error){
            reject(error);return;
          }
          resolve(records);
        }
      );
    }
  );
}

function leaderGrabEasy() {
  return Promise.all([
    readRecords("userData",{}),
    readRecords("userData",{E_highscore:leaderScores[0]})
  ])
  .then(
    function(results){
      return {
        leaderScores:results[0]
          .map(function(leader){return leader.E_highscore;})
          .sort(function(a, b){return b-a}),
        leaders:results[1]
          .map(function(leader){return leader.username})
      }
    }
  )
}

//use the function:
leaderGrabEasy()
.then(
  function(result){
    console.log("result:",result);
  }
)
.catch(
  function(error){
    console.log("there was an error:",error);
  }
)

//arrow function syntax:
leaderGrabEasy()
.then(result=>console.log("result:",result))
.catch(error=>console.log("there was an error:",error));

In modern syntax it'll look like this:

//readRecords as a promise returning function
const readAsPromise = (str,obj) =>
  new Promise(
    function (resolve,reject){
      readRecords(
        str,
        obj,
        function(records,error) {//this readRecords cannot fail?
          if(error){
            reject(error);return;
          }
          resolve(records);
        }
      );
    }
  );

const leaderGrabEasy = () =>
  Promise.all([
    readRecords("userData",{}),
    readRecords("userData",{E_highscore:leaderScores[0]})
  ])
  .then(
    ([score,name])=>({
      leaderScores:score.map(leader=>leader.E_highscore).sort((a, b)=>b-a),
      leaders:name.map(name=>nae.username)
    })
  )

//use the function:
leaderGrabEasy()
.then(result=>console.log("result:",result))
.catch(error=>console.log("there was an error:",error));