Skip to content

Instantly share code, notes, and snippets.

@mikermcneil
Last active March 31, 2017 23:13
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mikermcneil/32391da94cbf212611933fabe88486e3 to your computer and use it in GitHub Desktop.
Save mikermcneil/32391da94cbf212611933fabe88486e3 to your computer and use it in GitHub Desktop.
What about async.if()? What if you need to take an asynchronous detour? Or to do something asynchronous under some circumstances, but something synchronous under others?
// Whether you're building a Sails app, working with Waterline or Node Machines in a standalone Node.js project,
// or frankly doing anything asynchronous in Node.js, it's pretty common to end up in a situation where you need
// to do something asynchronous... but only in some cases! This gist shows a couple of ways to handle that.
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// ╔═╗╦ ╦╔╗╔╔═╗╦ ╦╦═╗╔═╗╔╗╔╔═╗╦ ╦╔═╗ ┌─ ┌┬┐┬─┐┌─┐┌┬┐┬┌┬┐┬┌─┐┌┐┌┌─┐┬ ─┐
// ╚═╗╚╦╝║║║║ ╠═╣╠╦╝║ ║║║║║ ║║ ║╚═╗ │ │ ├┬┘├─┤ │││ │ ││ ││││├─┤│ │
// ╚═╝ ╩ ╝╚╝╚═╝╩ ╩╩╚═╚═╝╝╚╝╚═╝╚═╝╚═╝ └─ ┴ ┴└─┴ ┴─┴┘┴ ┴ ┴└─┘┘└┘┴ ┴┴─┘ ─┘
// ┬┌─┐┌┬┐┬ ┬┌─┐┌┐┌ ┌─┐┬┌┐┌┌─┐┬ ┬ ┬ ┬
// │├┤ │ ├─┤├┤ │││ ├┤ ││││├─┤│ │ └┬┘
// ┴└oo ┴ ┴ ┴└─┘┘└┘oo└ ┴┘└┘┴ ┴┴─┘┴─┘┴
// Synchronous if/then/finally:
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
if (!_.isUndefined(req.param('p')) && !_.isFinite(req.param('p'))) {
return res.badRequest(new Error('If specified, `p` must be a positive integer.'));
}
var guestOrUserInfo;
// If not logged in, then use some generic guest information.
if (_.isUndefined(req.session.userId)) {
guestOrUserInfo = {
isLoggedIn: false,
displayName: 'Guest',
avatarSrc: 'https://placekitten.com/g/100/100'
};
}
// Otherwise the current user is logged in.
else {
guestOrUserInfo = {
isLoggedIn: false,
displayName: req.session.fullName,
avatarSrc: req.session.avatarSrc
};
}
// >- Now either way...
Products.find({
where: { category: 'lawmowers' },
limit: 30,
skip: (req.param('p')||1) * 30,
sort: 'popularity DESC'
}).exec(function (err, products) {
if (err) { return res.serverError(err); }
return res.view('pages/browse/products/lawnmowers', {
me: guestOrUserInfo,
products: products
});
});//</after fetching products>
// Easy enough, right?
//
// But what if we don't store the user data in the session?
// What if the user's data is stored in the database?
//
// > (Which is, by the way, the preferable way to do things, IMO.
// > I've found that it's best to keep _only_ the `id` of the logged-in
// > user in the session-- e.g. as `req.session.userId`.)
//
// If we need to talk to the database, or the filesystem, or a third party API, then that means
// it's necessary to perform an asynchronous call (`User.findOne()`). But only sometimes...
//
// The example below shows one way to go about it.
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// ╔═╗╔═╗╦ ╦╔╗╔╔═╗ ┬ ╔═╗╦ ╦╔╗╔╔═╗╦ ╦╦═╗╔═╗╔╗╔╔═╗╦ ╦╔═╗
// ╠═╣╚═╗╚╦╝║║║║ ┌┼─ ╚═╗╚╦╝║║║║ ╠═╣╠╦╝║ ║║║║║ ║║ ║╚═╗
// ╩ ╩╚═╝ ╩ ╝╚╝╚═╝ └┘ ╚═╝ ╩ ╝╚╝╚═╝╩ ╩╩╚═╚═╝╝╚╝╚═╝╚═╝╚═╝
// ┬┌─┐┌┬┐┬ ┬┌─┐┌┐┌ ┌─┐┬┌┐┌┌─┐┬ ┬ ┬ ┬
// │├┤ │ ├─┤├┤ │││ ├┤ ││││├─┤│ │ └┬┘
// ┴└oo ┴ ┴ ┴└─┘┘└┘oo└ ┴┘└┘┴ ┴┴─┘┴─┘┴
// Asynchronous & synchronous if/then/finally:
if (!_.isUndefined(req.param('p')) && !_.isFinite(req.param('p'))) {
return res.badRequest(new Error('If specified, `p` must be a positive integer.'));
}
// We'll use a self-calling function (`_getUserOrGuestInfo`) to implement this.
//
// > Side note:
// > Self-calling functions are a powerful pattern in asynchronous JavaScript, and they
// > can solve many of the common problems and limitations that you may be quite familiar with.
// > Not just detours and conditional branching, but also even trickier problems like async recursion.
// > Our use here of a self-calling function is actually a very simple case-- we're using it purely
// > to pass in a callback function (`done`). See below (line 111) for where I actually define that
// > callback (I gave it a name, `afterwards`, to make it easier to see).
//
(function _getUserOrGuestInfo(done){
// If not logged in, then use some generic guest information.
if (_.isUndefined(req.session.userId)) {
return done(undefined, {
isLoggedIn: false,
displayName: 'Guest',
avatarSrc: 'https://placekitten.com/g/100/100'
});
}
// --• Otherwise, the user is logged in.
User.findOne({ id: req.session.userId }).exec(function (err, loggedInUser) {
if (err) { return done(err); }
if (!user) { return done(new Error('Consistency violation: The requesting user is logged in, but their session refers to a user which does not exist in the database. Seems like something funny is going on here.'))
return done(undefined, {
isLoggedIn: true,
displayName: loggedInUser.fullName,
avatarSrc: loggedInUser.avatarSrc
});
});//</after looking up the user record for the currently logged-in user>
})(function afterwards(err, guestOrUserInfo) {
if (err) { return res.serverError(err); }
// >- Now either way...
// Look up the relevant products.
// Note that we could have looked this up simultaneously using `async.auto()`.
// (but it's best not to do that unless you actually need to for performance reasons,
// because doing so makes your code harder to read.)
Products.find({
where: { category: 'lawmowers' },
limit: 30,
skip: (req.param('p')||1) * 30,
sort: 'popularity DESC'
}).exec(function (err, products) {
if (err) { return res.serverError(err); }
return res.view('pages/browse/products/lawnmowers', {
me: guestOrUserInfo,
products: products
});
});//</after looking up the appropriate page of product data>
});//</after pulling together a uniform dictionary of info about the requesting user (they're either a guest or a logged-in user)>
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// Finally, one last thing to point out:
//
// For convenience, the rest of the Sails.js team and I packaged up the above behavior (using
// a self-calling function to do an asynchronous `if`) into a helper machine, called "If..Then..Finally".
// (see http://node-machine.org/machinepack-ifthen/if-then-finally)
// Here's how to take advantage of that:
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// ╔═╗╔═╗╔═╗╦╔═╗╔╦╗╔═╗╔╦╗ ┌─ ┬ ┬┌─┐┬┌┐┌┌─┐ ┌┬┐┌─┐ ┬┌─┐┌┬┐┬ ┬┌─┐┌┐┌ ─┐
// ╠═╣╚═╗╚═╗║╚═╗ ║ ║╣ ║║ │ │ │└─┐│││││ ┬ │││├─┘───│├┤ │ ├─┤├┤ │││ │
// ╩ ╩╚═╝╚═╝╩╚═╝ ╩ ╚═╝═╩╝ └─ └─┘└─┘┴┘└┘└─┘ ┴ ┴┴ ┴└ ┴ ┴ ┴└─┘┘└┘ ─┘
// ┬┌─┐┌┬┐┬ ┬┌─┐┌┐┌ ┌─┐┬┌┐┌┌─┐┬ ┬ ┬ ┬
// │├┤ │ ├─┤├┤ │││ ├┤ ││││├─┤│ │ └┬┘
// ┴└oo ┴ ┴ ┴└─┘┘└┘oo└ ┴┘└┘┴ ┴┴─┘┴─┘┴
// Assisted asynchronous if/then/finally:
var IfThen = require('machinepack-ifthen');
if (!_.isUndefined(req.param('p')) && !_.isFinite(req.param('p'))) {
return res.badRequest(new Error('If specified, `p` must be a positive integer.'));
}
// If the provided value is truthy, then run `then`. Otherwise run `else`.
// Either way, exit "success" and continue onwards.
IfThen.ifThenFinally({
bool: !!req.session.userId,
then: function (done){
User.findOne({ id: req.session.userId }).exec(function (err, loggedInUser) {
if (err) { return done(err); }
if (!user) { return done(new Error('Consistency violation: The requesting user is logged in, but their session refers to a user which does not exist in the database. Seems like something funny is going on here.'))
return done(undefined, {
isLoggedIn: true,
displayName: loggedInUser.fullName,
avatarSrc: loggedInUser.avatarSrc
});
});//</after looking up the user record for the currently logged-in user>
},
orElse: function (done){
return done(undefined, {
isLoggedIn: false,
displayName: 'Guest',
avatarSrc: 'https://placekitten.com/g/100/100'
});
}
}).exec(function (err, guestOrUserInfo){
if (err) { return res.serverError(err); }
// >- Now either way...
Products.find({
where: { category: 'lawnmowers' },
limit: 30,
skip: (req.param('p')||1) * 30,
sort: 'popularity DESC'
}).exec(function (err, products) {
if (err) { return res.serverError(err); }
return res.view('pages/browse/products/lawnmowers', {
me: guestOrUserInfo,
products: products
});
});//</after fetching products>
});//</after pulling together a uniform dictionary of info about the requesting user (they're either a guest or a logged-in user)>
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment