1

I am looping through a basic JSON Object with a for loop. The JSON looks like:

{
  "1": {
    "project_name": "Example Project Name 1",
    "client_name": "John Doe"
  },
  "2": {
    "project_name": "Example Project Name  2",
    "client_name": "John Doe"
  },
  /// -------
}

The issue I am having is when looping through it. I am attempting to loop through using .when() and .then() -- The ajax() calls go synchronously as expected, but the input for said ajax() is always the last index of the JSON.

function bulkUploadGo() {
    var def = $.when();

    for (var i = 1; i < Object.keys(projects_json).length + 1; i++) {
        var project = Object(projects_json)[i];

        // THIS WILL LOOP INSTANTLY -- SO I WILL SEE 1 - X INSTANTLY
        console.log(project); 

        def = def.then(function () {

             // THIS DISPLAYS SYNCHRONOUSLY, BUT IS ALWAYS SET TO THE LAST INDEX BECAUSE OF INSTANT LOOP ABOVE
            console.log(project); 

            return prepareLayer(project);
        });
    }
}

function prepareLayer(project) {
    return $.ajax({
        type: "POST",
        url: "/app/ajax/calls/process_project.php",
        dataType: 'html',
        data: {project: project}
    }).then(function (data) {
        var obj = JSON.parse(data); 
        // Do stuff
    });
}

Obviously I am wrong in thinking that because the def = def.then(function () { is directly inside the for loop that it would "hold it" until the return is satisfied. I just don't understand why I am wrong, and what the solution is! How do I correctly pass project into prepareLayer(project) synchronously with the rest of the script? I know my logic is flawed, I just can't see the forest through the trees.

For reference in explaining the outcome, here is what the console.log() looks like -- Where the area in blue is what happens instantly, and the rest happens with the def.then(function () {

CONSOLE LOG

Zak
  • 6,976
  • 2
  • 26
  • 48
  • Forgive the silly question maybe, but why would you not use [Promise.all()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all)? – Mike 'Pomax' Kamermans Mar 07 '20 at 00:36
  • you can use [$.ajaxQueue](https://stackoverflow.com/a/4785886/4017803) – Sagar Mar 07 '20 at 00:46
  • @Mike Not silly at all .. I have a progress bar that I am creating and running with this as well .. When I looked into `Promise_all()` it didn't look practical (possible) so I went with this approach. I basically take 100, divide by the number of indexes and every iteration increase the width of my progress bar. Is that possible with `Promise_all()` in the way it's described on Moz ? – Zak Mar 07 '20 at 00:48
  • use async-await. – Daniel A. White Mar 07 '20 at 00:50
  • Sure, `Promise.all` simply takes an array of promises and does not itself resolve until all of those promises are done. So you are free to have each of those promises call an update to your progress bar on resolve or reject: `new Promise((resolve,reject) => { doblah().then(e => { updateProgressBar(true); resolve(); }).catch(e => { updateProgressBar(false); reject(); })` So every time a promise resolves, you can increment your progress bar, and then your "do this at 100%" goes in `Promise.all([...]).then( your code here )` – Mike 'Pomax' Kamermans Mar 07 '20 at 00:52
  • @Mike -- Perfect explanation! Please write a quick answer. and I'll be sure to accept and upvote. Thanks for helping out! – Zak Mar 07 '20 at 01:21
  • I've posted an answer for you (and removed all jquery because modern JS kind of no longer needs it at all) – Mike 'Pomax' Kamermans Mar 07 '20 at 17:34
  • Do you want the requests to be sent in rapid succession, and then handled as and when they return, and then continue, or do you want to send one, wait for it to complete, and then send the next? – Ben Aston Mar 07 '20 at 17:48

2 Answers2

2

You probably want to work with Promise.all instead, while tracking progress on individual tasks. Also note that modern browsers don't need jQuery here in the slightest, everything you're doing already has plain JS APIs:

const APIEndpoint = `/app/ajax/calls/process_project.php`;
const yourData = { ...... };
const dataKeys = Object.keys(yourData);
const progressBar = new IncrementalProgressBar(dataKeys.length);

/**
 * You're constantly posting to the same thing: let's make that a function.
 */
function postData(data = {}) {
  return fetch(APIEndpoint, {
    method: `POST`,
    headers: {
      "Content-Type": `application/json`
    },
    body: JSON.stringify(data)
  });
}

/**
 * Turn {yourData,key} into a promise around posting your data.
 */
function keyToPromise(key) {
  return new Promise((resolve, reject) => {
    const data = yourData[key];

    postData(data)
      .then(result => {
        progressBar.increment();
        resolve(result);
      })
      .catch(error => {
        progressBar.increment({error: `${key} failed`});
        reject(error);
      });
  };
}

// And now we just... run through all the things
Promise
  .all(dataKeys.map(keyToPromise)))
  .then(result => moveOnWhenDone())
  .catch(error => handleException());
Mike 'Pomax' Kamermans
  • 49,297
  • 16
  • 112
  • 153
1

Here are three ways to do this, with varying levels of parallelization.

To upload one-by-one and process one-by-one:

const bulkUpload = async(arr,processResponse=identity)=>{
    for (let el in arr) {
        const response = await upload(el)
        await processResponse(response)
    }
}
bulkUpload(projects.values, processResponse).then(...)

To upload in parallel and process one-by one once we have all the responses:

const bulkUpload = async(arr)=>await Promise.allSettled(arr.map(upload))
bulkUpload(projects.values).then((allResponses) => /* process the responses */)

To upload in parallel and process each response as it comes in:

const uploadAndProcess = (el) => upload(el).then(processResponse)
const bulkUpload = async(arr)=>await Promise.allSettled(arr.map(uploadAndProcess))
bulkUpload(projects.values)

Boilerplate:

const projects = { "1": { "project_name": "P1", "client_name": "C1" }, "2": { "project_name": "P2", "client_name": "C2" }, }
const identity = (x)=>x
cont URL = '/app/ajax/calls/process_project.php'
const upload = (item)=>fetch(URL, requestOptions(item))
const requestOptions = (project)=>({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ project }) })
Ben Aston
  • 53,718
  • 65
  • 205
  • 331