1

I’ve got this puzzling situation I can’t quite figure out:
-I have an array of 10 objects that have two properties: IDNum and imageURL.
-Let’s say that only 3 of these objects have actually had their imageURL property set (objects at index positions [0, 4, 9]), and I now want to retrieve their images.
-I quickly create another array called imageURLsArray that contains just those 3 URL’s, and I iterate through it using .map to attach a Promise to each - as follows:

// (there’s a bit of THREE.js code in here - but don’t let that throw you off: 
// at issue is the enumeration of Promises, which is the universal Javascript 
// problem I’m trying to figure out)

function getTextures(theImageURLsArray) {
    const loader = new THREE.TextureLoader(); // That's a THREE.js class that  
                                              // let's you load images from remote URL's

    return theImageURLsArray.map((currentURL, index) => {
       console.log("  >Inside '.map', currentURL # ", index, " is: ", currentURL);
  
       return new Promise(function(resolve, reject) {
           console.log(" >>Inside 'new Promise' —> will now call THREE.js's 'loader.load()' function!");
    
           loader.load(currentURL, function(theReturnedLoadedTexture) {
              console.log("\n  >RETURNED from 'loader.load()', index#", index, ", with currentURL = ", currentURL);
              console.log("  >Will now call 'resolve()' with this Texture!");
              resolve(theReturnedLoadedTexture)
           },

           function(err) { 
             reject(err);
           })
       }) 

     }) 
   }  

I then do the following to make everything happen:

Promise.all(getTextures(imageURLsArray))
   .then(returnedTexturesArray => {
       console.log("Here are the returned textures:");
       console.log(returnedTexturesArray);
    
       theTexturesArray = returnedTexturesArray;

       // Gonna iterate MANUALLY through these babies and see what's what:
       for(z = 0; z < theTexturesArray.length; z++) {
         tempTexture = theTexturesArray[z];   
         console.log("tempTexture # ", z, "'s IMAGE property = ", tempTexture.image.currentSrc);
       }
       // I can now use these Textures to map them onto my 3D Materials...
    })
  .catch(err => console.error(err))

So all this works perfectly well - EXCEPT, the Promises do NOT necessarily return in the order they were created.
Which means that even though I initially called them on my imageURLsArray which was ordered [0, 4, 9], they may return in the order [4, 9, 0], or [9, 0, 4], etc.

This obviously creates a mismatch between the original imageURLsArray and the resulting returnedTexturesArray.

Given that ultimately the images need to be loaded into say a simple grid-Table which has clearly enumerated Cells, we can’t have Image # 4 load into Cell # 0.

So what I’m trying to figure out is how to carry the index of the returned images (or Promises?) all the way out to the end, so that I can rearrange the results if I have to so they match the order of the originals.

And when I say index, I don’t even necessarily mean the original index of the objects I’m working with from the “master” Array - meaning 0, 4, and 9, but at least the index used by .map to operate on these objects - meaning 0, 1 and 2.

Note that the goal is to work with as many as 1000 objects - and their corresponding images, so I’m trying to find a solution that scales and would be the most efficient.

Sirab33
  • 1,247
  • 3
  • 12
  • 27
  • Promise.all() resolved order is always the same as order of promises passed to it. Something not intuitive is going on if not. Note you have bad habit of not using `let, var or const` in some of the local variables which makes them global. That bad habit can generate unexpected behaviors and be hard to debug – charlietfl Jan 09 '21 at 16:55
  • Also think you should be creating a new `loader` for each instance but am not well versed in three.js. Would help a lot if you provided a [mcve] demo that demonstrates the problem – charlietfl Jan 09 '21 at 16:57

2 Answers2

2

First of all, Promise.all() WILL order the results in the same order as the promises that you passed in to Promise.all(). So, if you pass in three promises Promise.all([p1, p2, p3]), then you will get back an array in that same order, regardless of what order they completed in. So, if that's all you need, you can use the returned order.

Second, you can pass to your function an object that contains both id and index and when you resolve with the image, you can resolve with an object that contains image data and the index. That would allow you to know the index and insert the data in the array properly.

But, I would probably approach this differently and let Promise.all() do more of the work for you because you can pass Promise.all() an array that contains either promises or values. So, if you pass it the whole array with promises in the spots of the array that don't yet have images and image data in the places that do already have image data, then you can end up with a result that is just an array that contains all your image data.

let arrayOfObjects = [{idNum: id1}, {idNum: id2, imageUrl: url2}, ...];

function fillTextures(array) {
    return Promise.all(array.map(obj => {
        if (obj.imageUrl && !obj.texture) {
             return getTexture(obj.imageUrl).then(texture => {
                 obj.texture = texture;
                 return obj;
             });
        } else {
             return obj;
        }
    }));
}

function getTexture(imageUrl) {
   return new Promise(function(resolve, reject) {
       loader.load(imageUrl, function(texture) {
          resolve(texture);
       },  function(err) { 
         reject(err);
       });
   })     
}

fillTextures(arrayOfObjects).then(resultsArray => {
    // copy of the original array of objects with .texture property
    // filled in for any object that has .imageUrl property
    console.log(resultsArray);
}).catch(err => {
    console.log(err);
});

Done this way, you're just processing the entire array and actually calling an asynchronous operation only on the elements that need it and letting Promise.all() keep the whole array in the proper order. I find this simpler than trying to process only certain elements and the re-insert them into the original array.

jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • Hey that's a really thorough answer with what seems to be a much smarter approach to handling my tasks. Definitely going to try it! Either way, you - and everyone else who chimed in - were totally right: the `Promises` DID return in the order they were created. It was the `loader()` operations (from `THREE.js`) that were coming back in different orders, every single darn time(!) Those are async as well - which is what created the confusion. So, thank you - I'll try your implementation and chime back in if any issues arise. Cheers! – Sirab33 Jan 09 '21 at 17:46
0

I feel like somthing might be missing here. Because the resolved results from Promise.all is in order with the input promise array no matter what.

see this answer for more details on this: Promise.all: Order of resolved values

Danny
  • 11
  • 2
  • 3
  • You were correct about the `Promises` returning in the order of the input-array! See my comment above. Thank you! – Sirab33 Jan 09 '21 at 18:26