Forked from mikermcneil/sync-vs-async-recursion.js
Created
September 28, 2016 16:32
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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