5

Edit: In the mere hours since asking this question, I've learned enough to realise this question is a perfect case study in egregious abuse of promises:

I'm trying to convert a workflow to using promises because it was a horrible rat-nest of callbacks, and I was going well, until I got to a particular function. I'm trying to use the promisified version of readFile() to get the contents of a series of files, but I also need to hold on to the filename of each file so it can be added into the object that will eventually be parsed from the file contents in the next step/function. This filename is only accessible while I'm iterating over the list of filenames (and creating promises to return the contents of the file). Right now I have this, which won't work:

    // Accepts a sequence of filenames and returns a sequence of objects containing article contents and their filenames
    function getAllFiles(filenames) {
      var files = []; // This will be an array of objects containing Promises on which we want to call .all()
      filenames.each( function (filename) {
        files.push({content: fsPromise.readFileAsync(articlesPath + '/' + filename, 'utf8'), 
                   filename: filename});    
      });

This is an attempt to express what I want to do next:

      Promise.all(files).then( function(files) {
        return Lazy(files);
      });
    } 

In other words, what I want to get out of the array of promises is an array of objects like {content: "FILE CONTENTS", filename: "fileN.txt"}. The first part is the result of the readFileAsync(), so it's a promise that needs to resolve before I can move on to the next step. As far as I can tell, that filenames.each() call is my only chance at associating file contents with the name of the file it came from. I can't control the contents of the files, so I can't store the filenames inside the file contents, and that would be ugly data redundancy and a bad idea anyway. The Lazy() call is just to transform the finished array into a lazy.js Sequence, as that's the library I'm using for collection handling in the rest of the project.

Half-solution I'm considering

Would it be possible to extract the promise parts out of the objects and store them in a seperate array, then call .all() on that? In other words, when all the promises are done, I go back into the original array and retrieve the matching filename by index? I feel like that would work, if I'm able to populate the second array with references to the original promises (i.e. so I know when the originals have resolved). Would this work?

Alternatively,

Could I just go synchronous inside that .each() call? E.g.

filenames.each( function(filename) {
  files.push({content: fsPromise.readFileAsync(articlesPath + '/' + filename, 'utf8')
              .then( function(fulfilledContent) {return fulfilledContent}), 
              filename: filename
});

(I know I also need to handle the possibility of these promises being rejected instead of resolved – I'll work out how to do that once I've worked out if what I'm trying to do is even possible)

Toadfish
  • 1,112
  • 1
  • 12
  • 22
  • Your last snippet looks like a syntax error, and/or has indentation issues. I'm not sure what you mean by "go synchronous". – Bergi Feb 17 '16 at 15:36
  • sorry, it's like half a solution, and it's quite probably a syntax error since this is my first time using promises. By 'go synchronous' I mean 'wait for each promise to resolve, sequentially, as the each() call iterates over the array of filenames' – meaning I end up with an array of finished objects, instead of an array of objects containing promised properties. – Toadfish Feb 17 '16 at 15:37

1 Answers1

9

There are two simple solutions:

  • Access the filenames array in the last callback. It's in the same order as the array of contents that Promise.all will fulfill with:

    Promise.all(filenames.map(function(filename) {
        return fsPromise.readFileAsync(articlesPath + '/' + filename, 'utf8');
    })).then(function(contents) {
        return Lazy(contents).map(function(content, i) {
            return {content: content, filename: filenames[i]};
        });
    })
    
  • Make promises for the object with both filename and content immediately when iterating the array:

    Promise.all(filenames.map(function(filename) {
        return fsPromise.readFileAsync(articlesPath + '/' + filename, 'utf8').then(function(content) {
            return {content: content, filename: filename};
        });
    })).then(Lazy);
    
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Right, i forgot I could actually get an `i` value while doing `map()`, I should have known that. I think I like the second one better though, thank you. – Toadfish Feb 17 '16 at 15:43
  • OK sorry to bring this back up again but what I thought would be a minor alteration has totally stumped me – what do I do if I want the initial filenames array to be a promise as well? Your solution works perfectly on an array but I'm still struggling with this conceptually and falling back on callback style patterns, and even they aren't working for me... should I be using async.js instead of bluebird so that I'd at least have proper docs to read? Maybe I'll just read the async docs instead and see what I can apply here... – Toadfish Feb 18 '16 at 14:23
  • 1
    An array of filename promises? Hit it with `Promise.all`. An promise for an array? Then you need to place the code from my answer in a `then` callback on it (and possible apply [some flattening](http://stackoverflow.com/a/22000931/1048572)). – Bergi Feb 18 '16 at 16:53
  • I think the "flattening" was the part that was giving me grief, I'll give it another go, thanks – Toadfish Feb 19 '16 at 07:29