Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JavaScript Promises with AJAX

I am trying to write a series of AJAX requests into a dictionary. I am attempting to use promises for this, however I am either writing the promise syntax incorrectly, or what I think may be happening is that the function is actually completing (for loop is done, and AJAX requests sent) but the AJAX requests are still not returned. Therefore this is still returning an empty dictionary.

let dict = {};
let activeMachines = ["41", "42", "43"];
let dataPromise = new Promise (function (resolve, reject)
{
 for (let i = 0; i < activeMachines.length; i++)
 {
  let machineID = activeMachines[i]
  let getAPIData = new XMLHttpRequest();
  let url = 'http://127.0.0.1:8000/processes/apidata/' +machineID + '/';
  getAPIData.open('GET', url);
  getAPIData.send();
  getAPIData.onload = function()
  {
   let APIData = JSON.parse(getAPIData.responseText);
   dict['machine_' + machineID] = APIData[0].author_id;
   dict['temp' + machineID] = APIData[0].tempData; //get value
   dict['humid' + machineID] = APIData[0].humidData;
   timeValue = String((APIData[0].dateTime));
   dict['time' + machineID] = new Date(timeValue);
   console.log("done");
  }
 }
 resolve();
});

dataPromise.then(function() {console.log(dict);});

Is there a way to "sense" when all of the XMLHTTPRequests have returned?

like image 656
MattG Avatar asked Nov 01 '18 23:11

MattG


2 Answers

@Rafael's answer will work, but it doesn't illuminate much about what's going wrong, since you're trying to grok the concept of Promises and write one yourself.

Fundamentally I think your approach has two missteps: 1. creating a single Promise that handles calls to all of your arbitrary list of "activeMachines", and 2. putting your resolve() call in the wrong place.

Usually a Promise looks like this:

const myPromise = new Promise(function(resolve, reject) {
  doSomeAsyncWork(function(result) {
    // Some kind of async call with a callback function or somesuch...
    resolve(result);
  });
}).then(data => {
  // Do something with the final result
  console.log(data);
});

You can simulate some kind of arbitrary asynchronous work with setTimeout():

const myPromise = new Promise(function(resolve, reject) {
  // Resolve with "Done!" after 5 seconds
  setTimeout(() => {
    resolve("Done!");
  }, 5000);
}).then(data => {
  console.log(data); // "Done!"
});

However your original code puts the resolve() call in a weird place, and doesn't even pass it any data. It looks sorta equivalent to this:

const myPromise = new Promise(function(resolve, reject) {
  // Resolve with "Done!" after 5 seconds
  setTimeout(() => {
    // Doing some work here instead of resolving...
  }, 5000);
  resolve();
}).then(data => {
  console.log(data); // This would be "undefined"
});

Where you're doing a console.log("done"); in your original code is actually where you should be doing a resolve(someData);!

You're also trying to do side effect work inside of your Promise's async function stuff, which is really weird and contrary to how a Promise is supposed to work. The promise is supposed to go off and do its async work, and then resolve with the resulting data -- literally with the .then() chain.

Also, instead of doing multiple asynchronous calls inside of your Promise, you should generalize it so it is reusable and encapsulates only a single network request. That way you can fire off multiple asynchronous Promises, wait for them all to resolve, and then do something.

const activeMachines = ["41", "42", "43"];

// Make a reusable function that returns a single Promise
function fetchAPI(num) {
  return new Promise(function(resolve, reject) {
    const getAPIData = new XMLHttpRequest();
    const url = "http://127.0.0.1:8000/processes/apidata/" + num + "/";
    getAPIData.open("GET", url);
    getAPIData.send();
    getAPIData.onload = function() {
      const APIData = JSON.parse(getAPIData.responseText);
      const resolveData = {};
      resolveData["machine_" + num] = APIData[0].author_id;
      resolveData["temp" + num] = APIData[0].tempData; //get value
      resolveData["humid" + num] = APIData[0].humidData;
      timeValue = String(APIData[0].dateTime);
      resolveData["time" + num] = new Date(timeValue);
      resolve(resolveData);
    };
  });
}

// Promise.all() will resolve once all Promises in its array have also resolved
Promise.all(
  activeMachines.map(ea => {
    return fetchAPI(ea);
  })
).then(data => {
  // All of your network Promises have completed!
  // The value of "data" here will be an array of all your network results
});

The fetch() API is great and you should learn to use that also -- but only once you understand the theory and practice behind how Promises actually operate. :)

like image 194
jered Avatar answered Sep 30 '22 05:09

jered


Here's an example of the Fetch API which uses Promises by default:

let m_ids = [1,2,3,4];
let forks = m_ids.map(m => fetch(`http://127.0.0.1:8000/processes/apidata/${m}`));
let joined = Promise.all(forks);

joined
    .then(files => console.log('all done', files))
    .catch(error => console.error(error));

I hope this helps!

like image 25
Rafael Avatar answered Sep 30 '22 06:09

Rafael