Recursive Promise-based directory reading - javascript

I have a library that scans a directory for files on a remote server. It returns a Promise like this:
client.scanRemoteDirectory(path)
.then(files => {
console.log(files)
})
I'm trying to write a recursive method to scan directories and subdirectories too. But I'm running into some async issues. My function is like this:
const scanDir(path) {
// Scan the remote directory for files and sub-directories
return client.scanRemoteDirectory(path)
.then(files => {
for (const file of files) {
// If a sub-directory is found, scan it too
if (file.type === 'directory') {
return scanDir(file.path) // Recursive call
}
}
})
}
const scanDir('some/path')
.then(() => {
console.log('done')
})
This works however because of the return in front of the scanDir() recursive method call, this results in the method only scanning the first subdirectory in each directory and skipping the rest.
So for example if the structure is something like this:
/some/path
/some/path/dirA
/some/path/dirA/subdirA
/some/path/dirB
/some/path/dirB/subdirB
The above method will only scan:
/some/path
/some/path/dirA
/some/path/subdirA
It will skip dirB and it's children altogether since the method finds dirA first.
If I simply remove the return from the return scanDir(...) call, then it scans everything just fine. But then my final console.log('done') happens too soon because it's async.
So how do I solve this problem? What is the proper recursive Promise approach where I can still preserve async but also scan every subdirectory recursively?

You might want to use Promise.all in this situation to run your 'sub' promises in parallel, for example:
function scanDir(path) {
return client.scanRemoteDirectory(path)
.then(all => {
const files = all.where(file => file.type !== 'directory);
const dirs = all.where(file => file.type === 'directory);
return Promise.all(dirs.map(dir => scanDir(dir.path)) // Execute all 'sub' promises in parallel.
.then(subFiles => {
return files.concat(subFiles);
});
});
}
Alternatively you could use the reduce function to run your 'sub' promises in sequence:
function scanDir(path) {
return client.scanRemoteDirectory(path)
.then(all => {
const files = all.where(file => file.type !== 'directory);
const dirs = all.where(file => file.type === 'directory);
return dirs.reduce((prevPromise, dir) => { // Execute all 'sub' promises in sequence.
return prevPromise.then(output => {
return scanDir(dir.path)
.then(files => {
return output.concat(files);
});
});
}, Promise.resolve(files));
});
}
Async / await is definitely the easiest solution to read:
async function scanDir(path) {
const output = [];
const files = await client.scanRemoteDirectory(path);
for (const file of files) {
if (file.type !== 'directory') {
output.push(file);
continue;
}
const subFiles = await scanDir(file.path);
output = output.concat(subFiles);
}
return output;
}

I would make the then handler async so you can use await in the loop:
const scanDir(path) {
// Scan the remote directory for files and sub-directories
return client.scanRemoteDirectory(path)
.then(async files => {
for (const file of files) {
// If a sub-directory is found, scan it too
if (file.type === 'directory') {
await scanDir(file.path) // Recursive call
}
}
})
}

Related

Cant write to files inside of .then()?

I had a question about some code that i had working earlier, but now decides it doesnt want to work at all. Basically, I have a promise that returns me an array of data. I'll explain whats happening at the bottom of the code.
function mkdir(path){
return new Promise(function(resolve,reject){
fs.mkdir(path, { recursive: true },function(err){
if(err){console.log(err)}
})
return resolve()
})
}
function writeFile(file,content){
return new Promise(function(resolve,reject){
fs.writeFile(file, content, function(err){
if(err){console.log(err)}
return resolve()
})
})
}
function rawData(socket){
var mintDataArray = []
return new Promise(function(){
for (let i = 1; i < 13; i+= 1) {
getdat(i).then(function(r){
//mintDataArray.push(r)
for(o in r){
var dataurls = []
dataurls.push(r[o].discord_url,r[o].magic_eden_url,r[o].twitter_url)
//socket.emit('twitterFollowers',r[o])
const ProjectData = {
"mintDate": 0 || "",
"name":"",
"stock":0,
"links":[],
"mintTime": 0 || "",
"PricePlusDescription":0 || ""
}
if(r[o].mintDate != null){
ProjectData.mintDate = moment.utc(r[o].mintDate).format("MMMM Do")
}else{
ProjectData.mintDate = "No date specified yet"
}
ProjectData.name = r[o].name
ProjectData.stock = r[o].supply
ProjectData.links.push(dataurls)
ProjectData.PricePlusDescription = r[o].price
mintDataArray.push(ProjectData)
}
}).then(function(socket){
//CollectionSorter(mintDataArray)
for(i in mintDataArray){
var data = mintDataArray[i]
//console.log(data) // <----- This prints out the data that needs to be written to files.
var MintDateFolder = __dirname + "/UpcomingCollections/" +data.mintDate
if(!fs.existsSync(MintDateFolder)){
console.log('huh?')
mkdir(__dirname + '/UpcomingCollections/'+data.mintDate)
}
writeFile(MintDateFolder +"/"+data.name,JSON.stringify(data))
}
})
}
//socket.emit('twitterFollowers',mintDataArray)
})
}
So what the code is supposed to do, is check to see if that directory first exists in general. If it doesnt, then create the new directory. Then after that, its supposed to write files to it (not just that directory, but to other directories as well). It doesnt create the directory if it doesnt exists and it doesnt even write to it if i manually create the directory, however it does write to the other directories. I'm really not sure with this one because I had this working earlier, where it was creating the directory if it didn't exist. so i'm not sure what i messed up on.
I recently made mkdir and writefile functions and i thought they were the issue, because when i had this working I was just using fs.mkdir and fs.writefile. However, i went and tried again without those functions and i was still having the same troubles. I thought about making another promise to check if the directory existed but I already have quite a few nested promises.
read() function:
function read(i){
return new Promise(function(resolve,reject){
var r = https.request(options, function (res) {
var data = []
res.on('data', function (d) {
data.push(d)
}).on('end', function () {
var NFTTokenData = []
console.log(`STATUS: ${res.statusCode}`);
var info = Buffer.concat(data)
zlib.gunzip(info,function(err,buf){
var NFTData = []
var x = buf.toString()
var dat = JSON.parse(x)
var collectionList = dat.pageProps.__APOLLO_STATE__
for(keys in collectionList){
if(collectionList[keys].__typename.includes('Nft')){
collections.push(collectionList[keys])
resolve(collections)
}
}
})
})
})
r.end()
})
}
FINAL SOLUTION
function Project(fromApi) {
return {
mintDate: moment.utc(fromApi.mintDate).format("MMMM Do"),
name: fromApi.name,
imageLink:fromApi.project_image_link,
stock: fromApi.supply,
links: [
fromApi.discord_url,
fromApi.magic_eden_url,
fromApi.twitter_url
],
price: fromApi.price
}
}
function write(project, dir) {
const dest = resolve(dir, join(project.mintDate, project.name))
console.log(dest)
return stat(dirname(dest))
.catch(_ => mkdir(dirname(dest), {recursive: true}))
.then(_ => writeFile(dest, JSON.stringify(project)))
.then(_ => project)
}
function rawData(socket){
return new Promise(function(resolve,reject){
[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17].map(i =>
read(i).then(function(r){
var data = []
for(i in r){
if(r[i].__typename == "Nft"){
data.push(r[i])
}
}
data.map(item => {
var project = Project(item)
write(project,"./UpcomingCollections")
})
})
)
})
}
This is a modification of the answer down below!
You can do anything inside of .then – its only role is to sequence functions.
Let's talk about some other issues in your code -
function mkdir(path){
return new Promise(function(resolve,reject){
fs.mkdir(path, { recursive: true },function(err){
if(err){console.log(err)} // ❌ reject(err); do not log
})
return resolve() // ❌ resolve() outside of mkdir callback
})
}
function writeFile(file,content){
return new Promise(function(resolve,reject){
fs.writeFile(file, content, function(err){
if(err){console.log(err)} // ❌ reject(err); do not log
return resolve() // ⚠️ "return" not needed
})
})
}
Did you find it tedious to implement wrappers for each fs function in Node? You will be happy to know the fs/promises module already provides Promise-based APIs for each -
import { mkdir, writeFile } from "fs/promises" // ✅
// mkdir(path[, options])
// Returns: <Promise> Upon success, fulfills with undefined if recursive is false, or the first directory path created if recursive is true.
// writeFile(file, data[, options])
// Returns: <Promise> Fulfills with undefined upon success.
Next we'll look over the other issues in the main program -
function rawData(socket){
var mintDataArray = [] // ⚠️ state "outside" of Promise context
return new Promise(function(){
for (let i = 1; i < 13; i+= 1) {
getdat(i).then(function(r){
for(o in r){ // ⚠️ global "o"; don't use for..in, use for..of instead
// ,,,
mintDataArray.push(Project) // ⚠️ attempting to send state "out" of Promise
}
// ⚠️ missing "return"
// implicitly returning "undefined"
}).then(function(socket){ // ⚠️ function parameter (socket) receives resolved Promise
for(i in mintDataArray){ // ⚠️ global "i", for..in again, accessing external state
var data = mintDataArray[i]
var MintDateFolder = __dirname + "/UpcomingCollections/" +data.mintDate // ⚠️ use "path" module
if(!fs.existsSync(MintDateFolder)){ // ⚠️ async methods until this point, why Sync here?
console.log('huh?')
mkdir(__dirname + '/UpcomingCollections/'+data.mintDate) // ⚠️ asynchronous
}
writeFile(MintDateFolder +"/"+data.name,JSON.stringify(data)) // ⚠️ attempts write before mkdir completes
}
})
}
socket.emit('twitterFollowers',mintDataArray) // ⚠️ accessing external state
})
}
read and transform data
It's quite a bit of work, but don't worry. By breaking big problems down into small ones, we can work smarter, not harder. We'll start by renaming getdat to read -
read(i).then(r => {
const mintData = [] // state *inside* promise context
for (const v of r) {
// for all v of r, add project data to mintData
mintData.push({
mintDate: v.mintDate,
name: v.name,
stock: v.supply,
links: [
v.discord_url,
v.magic_eden_url,
v.twitter_url
],
price: v.price
})
}
return mintData // "return" resolves the promise
}).then(...)
Already our .then function is getting big. There's quite a bit of code for extracting and constructing the project data, so make that its own function, Project -
function Project(fromApi) {
// add date logic, or more
return {
mintDate: fromApi.mintDate,
name: fromApi.name,
stock: fromApi.supply,
links: [
fromApi.discord_url,
fromApi.magic_eden_url,
fromApi.twitter_url
],
price: fromApi.price
}
}
read(i).then(r => {
const mintData = []
for (const v of r) {
mintData.push(Project(v)) // ✅
}
return mintData
}).then(...)
Which is the same thing as -
read(i)
.then(r => r.map(Project)) // ✨ no for loop needed!
.then(...)
write
Let's check back in with your code and see our progress -
function rawData(socket){
// var mintDataArray = [] // ✅ remove external state
return new Promise(function(){
for (let i = 1; i < 13; i+= 1) {
read(i)
.then(r => r.map(Project)) // ✨
.then(function(socket) {
// ⚠️ remember, "socket" gets the resolved promise
// since `read` resolves an array of Projects, we should rename it
})
}
// ⚠️ we'll come back to this
// socket.emit('twitterFollowers',mintDataArray)
})
}
We continue with the .then(function(socket){ ... }) handler. There's a good amount of code here for creating the path, making a directory, and writing JSON to a file. Let's make that its own function called write -
import { stat, mkdir, writeFile } from "fs/promises"
import { join, resolve, dirname } from "path" // 🔍 unrelated to Promise "resolve"
function write(project, dir) {
const dest = resolve(dir, join(project.mintDate, project.name)) // ✅ sanitary path handling
return stat(dirname(dest))
.catch(_ => mkdir(dirname(dest), {recursive: true})) // create dir if not exists
.then(_ => writeFile(dest, JSON.stringify(project))) // write project JSON data
.then(_ => project) // return project
}
Our rawData function is cleaning up nicely, but we still have an outstanding issue of running async operations inside two separate loops -
function rawData(socket){
return new Promise(function(){
for (let i = 1; i < 13; i+= 1) {
read(i) //❓ how to resolve promise for each read?
.then(r => r.map(Project))
.then(projects => { // ✅ "projects", not "socket"
for (const p of projects) {
write(p, "./UpcomingCollections") //❓ how to resole promise for each write?
}
// ❓ what should we return?
})
}
// socket.emit('twitterFollowers',mintDataArray)
})
}
promises in a loop
Promise has another function we must become familiar with. Promise.all takes an array of promises and resolves only when all promises have resolved -
function myfunc(x) {
return Promise.resolve(x * 100)
}
Promise.all([myfunc(1), myfunc(2), myfunc(3)]).then(console.log)
// Promise{ [ 100, 200, 300 ] }
Promise.all([1,2,3].map(n => myfunc(n))).then(console.log)
// Promise{ [ 100, 200, 300 ] }
We can use Promise.all to clean up the two loops in rawData. And look at that, we can sequence the data directly into the socket -
function rawData(socket){
return Promise.all(
[1,2,3,4,5,6,7,8,9,10,11,12].map(i =>
read(i).then(r => r.map(Project))
)
)
.then(projects => {
Promise.all(projects.map(p => write(p, "./UpcomingCollections"))) // ✨
})
.then(projects => socket.emit("twitterFollowers", projects)) // ✨
}
all together now
We can see a lot of pain points are eliminated by using Promise-based code in an effective, idiomatic way. Until this point we have not addressed the issue of error handling, but now there's nothing left to say. Because we used Promises correctly, any errors will bubble up and the caller can .catch them and respond appropriately -
import { stat, mkdir, writeFile } from "fs/promises"
import { join, resolve, dirname } from "path"
function Project(fromApi) {
return {
mintDate: fromApi.mintDate,
name: fromApi.name,
stock: fromApi.supply,
links: [
fromApi.discord_url,
fromApi.magic_eden_url,
fromApi.twitter_url
],
price: fromApi.price
}
}
function write(project, dir) {
const dest = resolve(dir, join(project.mintDate, project.name))
return stat(dirname(dest))
.catch(_ => mkdir(dirname(dest), {recursive: true}))
.then(_ => writeFile(dest, JSON.stringify(project)))
.then(_ => project)
}
function rawData(socket){
return Promise.all(
[1,2,3,4,5,6,7,8,9,10,11,12].map(i =>
read(i).then(r => r.map(Project))
)
)
.then(projects => {
Promise.all(projects.map(p => write(p, "./UpcomingCollections")))
})
.then(projects => socket.emit("twitterFollowers", projects))
}
async..await
Modern JavaScript provides async..await syntax allowing us to blur the lines between synchronous and asynchronous code. This allows us to remove many .then calls, flattens our code, reduces cognitive load, and shares asynchronous values in the same scope -
async function rawData(socket) {
const r = await Promise.all([1,2,3,4,5,6,7,8,9,10,11,12].map(read))
const projects = await Promise.all(
r.flatMap(Project).map(p => write(p, "./UpcomingCollections"))
)
return socket.emit("twitterfollowers", projects)
}
Some of the complications of this program stems from the use of nested data. Promise.all is efficient and runs the promises in parallel, but keeping the data nested in the array makes it a bit harder to work with. To show that async..await truly blurs the line between sync and async, we will bring back the two for..of loops and call await in the loop. This results in a serial order processing but readability is terrific. Maybe your use-case isn't hyper demanding and so this style is completely adequate -
async function rawData(socket) {
for (const i of [1,2,3,4,5,6,7,8,9,10,11,12]) {
const result = await read(i)
for (const r of result) {
const project = Project(r)
await write(project, "./UpcomingCollections")
socket.emit("twitterFollowers", project)
}
}
}
Mulan's answer was correct, I just needed to change the for loop in my other function, to a .map. After that, everything worked perfectly. I had to do some modification to the rawData function as seen below.
function Project(fromApi) {
return {
mintDate: moment.utc(fromApi.mintDate).format("MMMM Do"),
name: fromApi.name,
imageLink:fromApi.project_image_link,
stock: fromApi.supply,
links: [
fromApi.discord_url,
fromApi.magic_eden_url,
fromApi.twitter_url
],
price: fromApi.price
}
}
function write(project, dir) {
const dest = resolve(dir, join(project.mintDate, project.name))
console.log(dest)
return stat(dirname(dest))
.catch(_ => mkdir(dirname(dest), {recursive: true}))
.then(_ => writeFile(dest, JSON.stringify(project)))
.then(_ => project)
}
function rawData(socket){
return new Promise(function(resolve,reject){
[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17].map(i =>
read(i).then(function(r){
var data = []
for(i in r){
if(r[i].__typename == "Nft"){
data.push(r[i])
}
}
data.map(item => {
var project = Project(item)
write(project,"./UpcomingCollections")
})
})
)
})
}

Node.js: Push File Names into an Array in an Async-Await Way?

I would like to get some help because I just can not figure out how to use an async-await methods recursively in Node.js.
I am trying to create a function that returns all the files in all subfolders as an array using the file-system module.
I did saw a lot of examples online, but none of these used an array and than waited for an answer.
Thanks!
function checkFiles () {
const files = []
const getFiles = async dir => fs.readdir(`./${dir}`, { withFileTypes: true }, (err, inners) => {
if (err) {
throw new Error (err)
}
else {
inners.forEach(inner => {
inner.isDirectory() ? getFiles(`${dir}/${inner.name}`) : files.push(`file: ${inner.name}`);
});
};
});
getFiles('.')
if (files.length === 0) {
return 'no files'
}
else {
return files
}
}
console.log(checkFiles())
You are invoking getFiles asynchronously, and within getFiles you pass a callback to readdir instead of awaiting it.
What you should try is adding await to both lines:
function checkFiles() {
const files = []
const getFiles = async dir => {
try {
inners = await fs.readdir(`./${dir}`, { withFileTypes: true })
inners.forEach(inner => {
inner.isDirectory() ? getFiles(`${dir}/${inner.name}`) : files.push(`file: ${inner.name}`);
});
} catch {
// handle error
}
};
await getFiles('.')
if (files.length === 0) {
return 'no files'
}
return files
}
This will cause the program to halt when reaching getFiles(), and continue the execution only after it has finished, meaning files is ready for usage.

How to wait for internal promises to finish

I am using fs.readdir to get a list of directories and then again in the callback to get a list of "subpages" in each of these directories. I would like for the first callback to wait until the second callback is completed but I'm not sure how to do that.
// Array to hold list of pages
const pageList = []
// Get contents of target directory (not recursive)
fs.readdir(targetDir, { withFileTypes: true }, (err, items) => {
// Stop and return if error
if (!!err) return err
// Go through found contents
const theseItems = items.map(item => {
const subpages = []
// Directory name
const dirName = item.name
// Set up published target for this directory
const thisTargetDir = targetDir + '/' + dirName + '/publish'
// Now get pages in the directory's published directory
// (assumes all files within subdirectories are .mdx pages to load)
return (
fs.readdir(thisTargetDir, { withFileTypes: true }, (err, pages) => {
const theseSubpages = pages.map(page => {
const mdxSuffix = /.mdx$/g
const pageName = page.name.replace(mdxSuffix, '')
return subpages.push({ name: pageName })
})
Promise.all(theseSubpages).then(() => {
// Add to page list array
pageList.push({ name: dirName, subpages: subpages })
})
})
)
})
Promise.all(theseItems).then(() => {
console.log('pageList at the end is: ')
console.log(pageList)
})
})
The Promise.all(theseSubpages) works as expected, however the Promise.all(theseItems) resolves before the former has a chance to cycle through. I understand why that's happening and I've tried to do things like return each item as a Promise.resolve(), etc. but these things aren't working.
Wondering if I'm doing something inherently wrong in this approach…
UPDATE
I tried using the fsPromises approach but kept running into the same wrong patterns. Ended up using the node-dir package to go through the directories recursively. Code below, not really the exact answer to what I was trying to do, but this gets the result I was looking for.
const dir = require('node-dir')
const targetDir = __dirname + '/../pages/stuff'
const pageList = []
dir.paths(targetDir, (err, paths) => {
if (err) throw err
const baseMatch = __dirname.replace('/lib', '') + '/pages/stuff'
paths.dirs.map(dir => {
// Only publish paths
if (dir.substr(-7) === 'publish') {
// Get the slug directly before publish path
const contentSlug = dir.split('/').slice(-2)[0]
// Add this to main pageList array as top level objects
pageList.push({ name: contentSlug, subpages: [] })
}
})
paths.files.map(file => {
const filePathArray = file.split('/')
// Only publish paths
if (filePathArray.slice(-2)[0] === 'publish') {
// Get parent content slug for matching purposes
const parentContentSlug = filePathArray.slice(-3)[0]
// Get file name (remove .mdx suffix)
const mdxSuffix = /.mdx$/g
const fileName = filePathArray.slice(-1)[0].replace(mdxSuffix, '')
// Loop through main page list, find match, then add file as subpage
pageList.find((obj, key) => {
if (obj.name === parentContentSlug) {
return pageList[key].subpages.push({ name: fileName })
}
})
}
})
console.log('pageList at end:')
console.log(pageList)
})
Promises work by chaining .then calls (Promise.then(doStuff)). If you start a promise but then don't chain, you can't know when it's done. In order to chain promises from inner functions you have to return promises.
Generally you don't want to mix callbacks and promises.
If I were to do this I would start by using just promises.
const readdir = (target, options) =>
// returns a promise that resolves or rejects when the call finishes
new Promise((resolve, reject) =>
fs.readdir(target, options, (err, result) => {
if (err) reject(err);
resolve(result);
})
);
const collectSubPages = pages =>
// Wait for all the promises in the array to resolve
Promise.all(
// for each page, return a promise that resolves to the page/subpage object
pages.map(({ name }) =>
readdir(targetDir + "/" + name + "/publish", {
withFileTypes: true
})
.then(subpages => subpages.map(({ name }) => ({ name })))
.then(subpages => ({ name, subpages }))
)
);
readdir(targetDir, { withFileTypes: true })
.then(pages => collectSubPages(pages))
.then(console.log);
#David Yeiser, your own "update" code can be written more concisely using Array methods .filter() and .map(), plus various optimisations, as follows :
const dir = require('node-dir');
const targetDir = __dirname + '/../pages/stuff';
dir.paths(targetDir, (err, paths) => {
if (err) {
throw err;
}
const baseMatch = __dirname.replace('/lib', '') + '/pages/stuff';
const mdxSuffix = /.mdx$/g; // define once, use many times
const fileList = paths.files
.map(fileName => fileName.split('/'))
.filter(filePathArray => filePathArray[filePathArray.length - 2] === 'publish'); // Only 'publish' paths
const pageList = paths.dirs
.filter(dir => dir.substr(-7) === 'publish') // Only 'publish' paths
.map(dir => {
const name = dir.split('/').slice(-2)[0];
const subpages = fileList
.filter(filePathArray => filePathArray[filePathArray.length - 3] === name) // select those files whose "parent content slug" matches 'name'
.map(filePathArray => filePathArray[filePathArray.length - 1].replace(mdxSuffix, ''));
return { name, subpages };
});
console.log('pageList at end:');
console.log(pageList);
});
You will see that :
fileList is constructed with a paths.files.map().filter() pattern.
pageList is constructed with a paths.dirs.filter().map() pattern.
for each entry in pageList, subpages is constructed with a fileList.filter().map() pattern.
Barring mistakes on my part, that should give the same result.
untested

How do I refactor this composed function with Ramda.js?

i am writing a small utility using ramda and data.task that reads image files out of a directory and outputs their size. I got it working like so:
const getImagePath = assetsPath => item => `${assetsPath}${item}`
function readImages(path) {
return new Task(function(reject, resolve) {
fs.readdir(path, (err, images) => {
if (err) reject(err)
else resolve(images)
})
})
}
const withPath = path => task => {
return task.map(function(images) {
return images.map(getImagePath(path))
})
}
function getSize(task) {
return task.map(function(images) {
return images.map(sizeOf)
})
}
const getImageSize = dirPath => compose(getSize, withPath(dirPath), readImages)
The problem is with the withPath function that adds the correct image path to the image file name but forces my api to pass in the directoryName twice: once for reading the files and second time for reading the path. This means I have to call the getImageSize function like so:
const portfolioPath = `${__dirname}/assets/`
getImageSize(portfolioPath)(portfolioPath).fork(
function(error) {
throw error
},
function(data) {
console.log(data)
}
)
Is there any way to pass the dirname as a parameter only once? I want the api to work like this:
getImageSize(portfolioPath).fork(
function(error) {
throw error
},
function(data) {
console.log(data)
}
)
You shouldn't be building paths manually like that
One of Node's better APIs is the Path module – I would recommend that your readImages wrapper is made a generic readdir wrapper, and instead resolve an Array of path.resolve'd file paths
const readdir = dir =>
new Task ((reject, resolve) =>
fs.readdir (dir, (err, files) =>
err
? reject (err)
: resolve (files.map (f => path.resolve (dir, f)))
const getImagesSizes = dir =>
readdir (dir) .map (R.map (sizeOf))
Wrapping the Node continuation-passing style APIs just to return a Task gets to be a bother, doesn't it?
const taskify = f => (...args) =>
Task ((reject, resolve) =>
f (...args, (err, x) =>
err ? reject (err) : resolve (x)))
const readdir = (dir, ...args) =>
taskify (fs.readdir) (dir, ...args)
.map (R.map (f => path.resolve (dir, f)))
const getImagesSizes = dir =>
readdir (dir) .map (R.map (sizeOf))
You should probably also take care to file out file paths that are directories – unless your sizeOf implementation handles that
I managed to solve this by passing the Task resolution a single object like so:
function readImages(path) {
return new Task(function(reject, resolve) {
fs.readdir(path, (err, images) => {
if (err) reject(err)
else resolve({ images, path })
})
})
}
const withPath = task => {
return task.map(function({ images, path }) {
return images.map(getImagePath(path))
})
}
...and then destructing it out of the task payload and now my compose function looks like this:
module.exports = (function getImageSize(dirPath) {
return compose(getSize, withPath, readImages)
})()
And my api call looks like this:
getImageSize(portfolioPath).fork(
function(error) {
throw error
},
function(data) {
console.log(data)
}
)

Async operation inside callback of event

I'm working with a module called findit, which recursive find files within a target folder and return the events "file", "error", "end":
const finder = findit("folder/");
finder.on("file", (file) => {});
finder.on("error", ((error) => {});
finder.on("end", () => {});
The end event will be called when the finder finds all files... but, inside the file event i'm making an async operation, that separate files only if they have something inside:
const finder = findit("folder/");
let neededFiles = [];
finder.on("file", (file) => {
// async operation here to store only the files that i want
// neededFiles = [...neededFiles, file];
});
finder.on("error", ((error) => {});
finder.on("end", () => {
console.log(neededFiles); // empty array
});
The neededFile will be empty because the async operation has not finished yet. My question is: What chances do i need to do to wait the async operation in the end event?
Thanks.
Since you've provided only a scratch of you app, I tried to build around that, and show how you could wrap that into a promise to handle the async part.
function find(folder){
return new Promise((resolve, reject) => {
const finder = findit(folder);
const files = [];
finder.on("file", (file) => {
//do stuff like
//files.push(valueOrPromise);
//or
//files.push( find(anotherPath) );
});
finder.on("error", reject);
finder.on("end", () => {
//this line finally "returns" the result.
//to this point you can modify files as you wish
resolve( Promise.all(files) );
//hint, if you're working with recursive stuff, you may have nested Arrays, so you should flatten the result
//resolve( Promise.all(files).then(values => values.reduce((a,b) => a.concat(b), [])) )
});
})
}
Usually people ask at this point: why do I need promises? Because they implement state management of async tasks; so why would you want to implement it yourself?
And why no use the 'end' inside the 'file'? some like:
finder.on("file", (file) => {
// async operation here to store only the files that i want
// neededFiles = [...neededFiles, file]; // GENERATE A PROMISE SO U CAN CHAIN THEN FUNCTION
neededFilesPromise.then(function(neededFiles){
finder.on("end", () => {
console.log(neededFiles); // empty array
});
}).catch(function() {
finder.on("error", ((error) => {});;
})
});
I would say this is the perfect use case for Promise.all(). So something like this. You might want to 'promisify' your file operations so it is even cleaner (if you are using something like bluebird to replace the native promises). I am not sure if your lib is return the content of the file or a filename
const fs = require('fs');
require('bluebird').promisifyAll(fs);
const finder = findit("folder/");
let neededFiles = [];
finder.on("file", (file) => {
// if file is the file content
neededFiles.push(/* result of your op above */);
// if file is a file name
neededFiles.push(fs.readFile(/* file name */));
});
finder.on("error", ((error) => {});
finder.on("end", () => {
Promise.all(neededFiles)
.then((nf) => {
console.log(nf); // you should now have something here...
});
});

Categories