jfix
jfix

Reputation: 561

How to use an async function as compare function of Array.filter()?

For a Node JS module I'm writing I would like to use the async function Stats.isFile() as the callback function of the Array.filter() function. Below I have a working example of what I want to achieve, but using snychronous equivalents. I can't get my head around how to wrap the async function so that i becomes usable inside the Array.filter() function.

const fs = require('fs')

exports.randomFile = function(dir = '.') {
    fs.readdir(dir, (err, fileSet) => {
        const files = fileSet.filter(isFile)
        const rnd = Math.floor(Math.random() * files.length);
        return files[rnd])
    })
}

function isFile(item) {
    return (fs.statSync(item)).isFile()
}

Upvotes: 2

Views: 2646

Answers (3)

user3093605
user3093605

Reputation: 189

Using async/await, you can write your own async filter function:

const asyncFilter = async (list, action) => {
    const toFilter = [];
    await Promise.all(list.map(async (item, index) => {
        const result = await action(item);
        if (!result) toFilter.push(index);
    }));
    return list.filter((_, index) => !toFilter.includes(index));
}

In your case, you can write isFile asynchronously (returning a Promise) and the line:

const files = fileSet.filter(isFile)

Becomes:

const files = await asyncFilter(fileSet, isFile);

Don't forget to add the async keyword before the callback of fs.readdir

Upvotes: 0

jfriend00
jfriend00

Reputation: 707696

You can't use an async callback with .filter(). .filter() expects a synchronous result and there is no way to get a synchronous result out of an asynchronous operation. So, if you're going to use the asynchronous fs.stat(), then you will have to make the whole operation async.

Here's one way to do that. Note even randomFile() needs to communicate back it's result asynchronously. Here we use a callback for that.

const path = require('path');
const fs = require('fs');

exports.randomFile = function(dir, callback)  {
    fs.readdir(dir, (err, files) => {
        if (err) return callback(err);

        function checkRandom() {
            if (!files.length) {
                // callback with an empty string to indicate there are no files
                return callback(null, "");
            }
            const randomIndex = Math.floor(Math.random() * files.length);
            const file = files[randomIndex];
            fs.stat(path.join(dir, file), (err, stats) => {
                if (err) return callback(err);
                if (stats.isFile()) {
                    return callback(null, file);
                }
                // remove this file from the array
                files.splice(randomIndex, 1);
                // try another random one
                checkRandom();
            });
        }

        checkRandom();
    });
}

And, here's how you would use that asynchronous interface form another module.

// usage from another module:
var rf = require('./randomFile');
fs.randomFile('/temp/myapp', function(err, filename) {
   if (err) {
       console.log(err);
   } else if (!filename) {
       console.log("no files in /temp/myapp");
   } else {
       console.log("random filename is " + filename);
   }
});

Upvotes: 1

squiddle
squiddle

Reputation: 1317

if you go async you have to go async from the entry point on, so randomFile needs some way to "return" an async value (typically via callbacks, promises, or as a stream).

i do not know how your file structure looks like but instead of checking all entries for being a file i would select a random entry, check if it is a file, and if not try again.

this could look like this

const fs = require('fs');
const path = require('path');

exports.randomFile = (dir = '.', cb) => {
    fs.readdir(dir, (err, files) => {
        if (err) { return cb(err); }
        pickRandom(files.map((f) => path.join(dir, f)), cb);
    });
}

function pickRandom (files, cb) {
    const rnd = Math.floor(Math.random() * files.length);
    const file = files[rnd];
    fs.stat(file, (err, stats) => {
        if (err) {
            return cb(err);
        }
        if (stats.isFile()) {
            return cb(null, file);
        } else {
            return pickRandom(files, cb);
        }
    })
}

Upvotes: 1

Related Questions