Reputation: 561
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
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
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
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