Skip to content

Instantly share code, notes, and snippets.

@crrobinson14
Last active June 14, 2016 12:46
Show Gist options
  • Save crrobinson14/1750f3f6e8c0a941751b to your computer and use it in GitHub Desktop.
Save crrobinson14/1750f3f6e8c0a941751b to your computer and use it in GitHub Desktop.
Wrap ActionHero methods in async() to simplify their internal await() handling.
exports.action = {
name: 'myAction',
description: 'Perform some work with multiple async steps.',
async: true,
run: function(api, data) {
var firstRecord = api.await(api.orm.getFirstRecord());
// We will async await this result here before proceeding. No callback nesting or promise chaining!
if (!firstRecord) {
throw new NotFoundError();
}
var secondRecord = api.await(api.orm.getSecondRecord(firstRecord.someKey));
data.response.status = 'OK';
data.response.record = secondRecord;
}
};
exports.task = {
name: 'asyncTask',
description: 'Illustrate how to make an asyncawait-driven task.',
queue: 'default',
frequency: 30000,
async: true,
run: function(api) {
var record = api.await(api.orm.processRecord());
// This will be logged as the task's result in AH's task processing logs
return '1 record processed.';
}
};
var async = require('asyncawait/async'),
await = require('asyncawait/await'),
Promise = require('bluebird'),
publicApi = {},
api;
/**
* Helper that logs SQL queries.
*
* @example
* api.models.MYMODEL.findXYZ({ where: {...}, logging: api.helpers.logSQL });
*/
publicApi.logSQL = function(sql) {
if (process.env.NODE_ENV === 'test') {
console.log(sql); // eslint-disable-line no-console
} else {
api.log('SQL', 'info', sql);
}
};
/**
* Wrap ActionHero's typical call pattern to clean up async functions. Note that we no longer pass next() in
* to the caller. The caller is expected to act in a Promise-style async manner and throw() on an error.
* We handle the call to next() ourselves.
* @private
*/
function asyncActionWrapper(api, data, next) {
var error;
async(this.originalRun)(api, data).catch(function(e) {
// NOTE: In my environment I have typed errors that I can throw. As an additional nice-to-have I use their
// names here to set up specific HTTP response codes for specific error types. This is nice because actions
// don't need a lot of crufty code to do the same job. If a DB query doesn't find the record the action wants
// to work with, it can just throw NotFoundError() and a 404 will be emitted with it automatically.
if (e.name === 'NotFoundError') {
data.connection.rawConnection.responseHttpCode = 404;
} else if (e.name === 'SessionError') {
data.connection.rawConnection.responseHttpCode = 401;
} else if (e.name === 'RequestError') {
data.connection.rawConnection.responseHttpCode = 405;
} else if (e.name === 'ValidationError') {
data.connection.rawConnection.responseHttpCode = 406;
} else {
data.connection.rawConnection.responseHttpCode = 500;
}
error = e;
}).finally(function() {
next(error);
});
}
function asyncTaskWrapper(_api, params, next) {
async(this.originalRun)(_api, params).then(function(result) {
next(null, result);
}).catch(function(e) {
api.log('Task ' + this.name + ' failed', 'error', e);
next(e);
});
}
module.exports = {
initialize: function(_api, next) {
api = _api;
api.helpers = publicApi;
api.Promise = Promise;
api.async = async;
api.await = await;
convertAsyncActions();
convertAsyncTasks();
Object.keys(api.actions.actions).map(function(name) {
Object.keys(api.actions.actions[name]).map(function(version) {
var actionTemplate = api.actions.actions[name][version];
if (actionTemplate.async) {
api.log('Making action ' + name + ':' + version + ' async.', 'info');
actionTemplate.originalRun = actionTemplate.run;
actionTemplate.run = asyncApiWrapper.bind(actionTemplate);
}
});
});
Object.keys(api.tasks.tasks).map(function(name) {
var taskTemplate = api.tasks.tasks[name];
if (taskTemplate.async) {
api.log('Making task ' + name + ' async.', 'info');
taskTemplate.originalRun = taskTemplate.run;
taskTemplate.run = asyncTaskWrapper.bind(taskTemplate);
}
});
},
start: function(_api, next) {
next();
},
stop: function(_api, next) {
next();
}
};
var action = {
name: 'originalAction',
description: 'What the action looked like before it was cleaned up.'
};
action.run = function(api, data) {
// Note that this is technically only 7 lines longer - but that grows for every promise in the chain. An action with
// 5-10 promises chained together, or nested callbacks, can have a lot of function(){} closure boilerplate for all
// those promises. Asyncawait does away with that boilerplate.
api.orm.getFirstRecord().then(function(firstRecord) {
if (!firstRecord) {
throw new NotFoundError();
}
return api.orm.getSecondRecord(firstRecord.someKey);
}).then(function(secondRecord) {
data.response.status = 'OK';
data.response.record = secondRecord;
next();
}).catch(function(e) {
next(e);
});
};
exports.action = action;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment