Trees of Promises in ES6
This blog post shows how to handle trees of ES6 Promises, via an example where the contents of a directory are listed asynchronously.
The challenge
We’d like to implement a Promise-based asynchronous function listFile(dir)
whose result is an Array with the paths of the files in the directory dir
.
As an example, consider the following invocation:
listFiles('/tmp/dir')
.then(files => {
console.log(files.join('\n'));
});
One possible output is:
/tmp/dir/bar.txt
/tmp/dir/foo.txt
/tmp/dir/subdir/baz.txt
The solution
For our solution, we create Promise-based versions of the two Node.js functions fs.readdir()
and fs.stat()
:
readdirAsync(dirpath) : Promise<Array<string>>
statAsync(filepath) : Promise<Stats>
We do so via the library function denodify
:
import denodeify from 'denodeify';
import {readdir,stat} from 'fs';
const readdirAsync = denodeify(readdir);
const statAsync = denodeify(stat);
Additionally, we need path.resolve(p0, p1, p2, ···)
which starts with the path p0
and resolves p1
relatively to it to produce a new path. Then it continues with resolving p2
relatively to the new path. Et cetera.
import {resolve} from 'path';
listFiles()
is implemented as follows:
function listFiles(filepath) {
return statAsync(filepath) // (A)
.then(stats => {
if (stats.isDirectory()) { // (B)
return readdirAsync(filepath) // (C)
// Ensure result is deterministic:
.then(childNames => childNames.sort())
.then(sortedNames =>
Promise.all( // (D)
sortedNames.map(childName => // (E)
listFiles(resolve(filepath, childName)) ) ) )
.then(subtrees => {
// Concatenate the elements of `subtrees`
// into a single Array (explained later)
return flatten(subtrees); // (F)
});
} else {
return [ filepath ];
}
});
}
Two invocations of Promise-based functions are relatively straightforward:
statAsync()
(line A) returns an instance ofStats
readdirAsync()
(line C) returns an Array with filenames.
The interesting part is when listFiles()
calls itself, recursively, leading to an actual tree of Promises. It does so in several steps:
First, it maps the names of the child files to Promises that fulfill with Arrays of grandchild paths (line E).
It uses
Promise.all()
to wait until all results are in (line D).Once all results are in, it flattens the Array of Arrays of paths into an Array (line F). That Array fulfills the last Promise of the chain that starts in line C.
Note that synchronous programming constructs are used to compose Promises:
- The
if
statement in line B decides how to continue the asynchronous computation. - The
map()
method in line E is used to make recursive calls.
Helper function flatten()
The tool function flatten(arr)
concatenates all the elements of arr
into a single Array (one-level flattening). For example:
> flatten([[0], [], [1, [2]]])
[ 0, 1, [ 2 ] ]
It can be implemented like this:
function flatten(arr) {
return [].concat(...arr);
}
Further reading
- Chapter “Promises for asynchronous programming” in ”Exploring ES6”.
Comments
Post a Comment