Angry Coder
Angry Coder

Reputation: 167

return after all promises in Array.forEach block has been resolved

I'm trying the es6-promisify module in nodejs in a simple example similiar to the example on the npm page of es6-promisify. But I want to use a forEach loop to check the stats of the entries and push them to a separate array if they are directories:

var fs=require('fs');
var promisify=require('es6-promisify');
var path='../test_folder/';
function listSubfolderNamesSync(fpath){
    var arrayOfFolders=[];
    var array=fs.readdirSync(fpath);
    array.forEach(function(element,index,array){
        var stat=promisify(fs.stat);
        stat(path+element).then(function(stats){
            if (stats.isDirectory()){
                arrayOfFolders.push(element);
                console.log('pushed');
            }
        })
    });
    return arrayOfFolders;
}

console.log(listSubfolderNamesSync(path));

With 2 subfolders, the output would be:

[]
pushed
pushed

I know why my code doesn't work like I want, but I can't figure out how to solve this problem elegantly. I have seen code samples with Promise.all() and the usage of Array.map() to have an array of promises and wait for all of them to resolve before go forward in the chain. The problem I see is that I can't/don't want to create a promise explicitly for each time I check for the stats and put those promises in an array to use Promise.all(array). I have the feeling that the solution would require quite a work-around and would greatly reduce code readability. Maybe it's better just to use this in the forEach-loop, which is my working code before I wanted to try to promisify:

if(fs.statSync(path+element).isDirectory()){
        arrayOfFolders.push(element);
}

Probably it is due to my lack of programming experience, but it seems like mixing promises, callbacks and synchronous code is not as simple as it is written in books?

Upvotes: 0

Views: 922

Answers (1)

Roamer-1888
Roamer-1888

Reputation: 19288

As stat() returns a promise, so must your listSubfolderNamesSync() function, making it listSubfolderNamesAsync(). Unless a synchronous alternative to stat() is available, this is unavoidable.

Inside listSubfolderNamesAsync(), you need to map the array returned by fs.readdirSync() to an array of promises, then use Promise.all() to aggregatge those promises (and the data they deliver) into a single promise.

var fs = require('fs');
var promisify = require('es6-promisify');
var stat = promisify(fs.stat);

var path = '../test_folder/';

function listSubfolderNamesAsync(fpath) {
    var promises = fs.readdirSync(fpath).map(function(element) {
        return stat(path + element).then(function(stats) {
            return stats.isDirectory() ? element : null;
        });
    });
    return Promise.all(promises).then(function(results) {
        return results.filter(function(res) {
            return res !== null;
        });
    });
}

Now handle the results with .then() :

listSubfolderNamesAsync(path).then(function(folders) {
    console.log(folders);
});

There's no great performance penalty in having a more general function that returns separate lists of folders and files. You could write something like this :

function listSubElementsAsync(fpath) {
    var promises = fs.readdirSync(fpath).map(function(element) {
        return stat(path + element).then(function(stats) {
            var type = stats.isDirectory() ? 'folder' : 'file';
            return {type:type, element:element};
        });
    });
    return Promise.all(promises).then(function(results) {
        var files = results.filter(obj => obj.type === 'file').map(obj => obj.element);
        var folders = results.filter(obj =>  obj.type === 'folder').map(obj => obj.element);
        return {files:files, folders:folders};
    });
}
listSubElementsAsync(path).then(function(list) {
    console.log(list.files);
    console.log(list.folders);
});

Upvotes: 1

Related Questions