Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save johnny4young/77b042f441556241da9bf71df40b4551 to your computer and use it in GitHub Desktop.
Save johnny4young/77b042f441556241da9bf71df40b4551 to your computer and use it in GitHub Desktop.
Two examples demonstrating how to implement recursion using synchronous and/or asynchronous calls in Sails.js/Node.js.
// I frequently advise against defining inline functions.
// There are, of course, two obvious exceptions:
// 1. The "I'm done" callback(s) that you pass in to `.exec()`, or as the last argmt to any asynchronous library method in Node core.
// 2. The iteratee callbacks that you pass in to functions like `_.each()`, `_.reduce()`, and `async.each()`
//
// But there are still a few cases where definining inline functions can feel very necessary.
// One of those is recursion.
//
// I made this gist to demonstrate how you can implement recursion using synchronous and/or asynchronous calls.
// We'll go through two examples, starting with synchronous recursion over a virtual filesystem that lives in-memory
// in a JavaScript dictionary (like a simple file browser app running in a web UI served from your home internet router).
// Then we'll reapproach the same example, but with the scenario tweaked a bit, so that the filesystem lives in the
// database, which can only be accessed asynchronously (like Dropbox).
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// Synchronous recursion:
var virtualDesktop = {
kind: 'directory',
contents: [...]
};
var emptyDirectories = (function _recursivelyCheckForEmptyDirectories(iNode){
if (iNode.kind === 'file') { return []; }
else if (iNode.kind === 'directory') {
if (iNode.contents.length === 0) {
// Empty directory found!
return iNode;
}
else {
var allMatchingDescendants;
_.each(iNode.contents, function (childINode){
var theseMatchingDescendants = _recursivelyCheckForEmptyDirectories(childINode);
allMatchingDescendants = allMatchingDescendants.concat(theseMatchingDescendants);
});
return allMatchingDescendants;
}
}
else { throw new Error('Consistency violation: iNode with unexpected `kind` detected in virtual filesystem.'); }
})(virtualDesktop);
// ...anything else you want to do, then...
return res.json(emptyDirectories);
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// Asynchronous recursion:
// We'll assume that in our system, the topmost browseable directory ("INode") record always
// exists, and that it can be quickly identified by looking for the record without a parent (`parent: null`).
// We'll also assume that we have a policy protecting this action, so that it is only accessible
// to logged-in users.
INode.findOne({
parent: null,
owner: req.session.me
}).exec(function (err, desktop) {
if (err) { return res.serverError(err); }
if (!desktop) { return res.serverError(new Error('Consistency violation: Every user should have a desktop, but the desktop iNode is missing for the requesting user (`'+req.session.me+'`).')); }
// Now check to see if there are any empty directories, including the desktop itself.
(function _recursivelyCheckForEmptyDirectories(iNode, done){
try {
if (iNode.kind === 'file') { return done(undefined, []); }
if (iNode.kind !=== 'directory') {
return done(new Error('Consistency violation: iNode with unexpected `kind` detected in database.'));
}
if (iNode.contents.length === 0) {
// Empty directory found!
return done(undefined, iNode);
}
// --• If we made it here, this must be a NON-empty directory.
// So we look up the first level of files and/or directories it contains (its "children").
INode.find({ parent: iNode.id }).exec(function (err, childINodes) {
if (err) { return done(err); }
// And now we'll recursively investigate each child iNode to see if it is an empty directory,
// or if any of its descendants are empty directories:
var allMatchingDescendants;
async.each(iNode.contents, function (childINode, next){
if (err) { return next(err); }
_recursivelyCheckForEmptyDirectories(childINode, function (err, theseMatchingDescendants) {
if (err) { return next(err); }
allMatchingDescendants = allMatchingDescendants.concat(theseMatchingDescendants);
return next();
});//</after checking this child iNode for empty directory descendants>
}, function afterCheckingEachChildINode(err) {
if (err) { return done(err); }
return done(undefined, allMatchingDescendants);
});//</after checking each child iNode for empty directory descendants>
});//</after fetching child iNodes>
} catch (e) { return done(e); }//<< explained a bit more below.
})(desktop, function afterwards(err, emptyDirectories) {
if (err) { return res.serverError(err); }
// ...anything else you want to do, then...
return res.json(emptyDirectories);
});//</after recursively searching for empty directories>
});//</after initially fetching the desktop iNode>
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// The try/catch above is just here in case we messed up. When working with any
// asynchronous logic that is beginning to become complex, it is usually a good
// idea to protect yourself. Remember: throwing uncaught exceptions inside of
// asynchronous callbacks is super bad news.
//
// For more on error handling in async vs. synchronous usages, see:
// https://gist.github.com/mikermcneil/755a2ae7cc62d9a59656ab3ba9076cc1
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment