var _ = require('lodash'), meetExpectations, backpack; /** * ensure the specified keys are functions in subject * * @param {Object} subject * @param {Array} expectations * @param {Array=} [defaults] * @returns {Object} */ meetExpectations = function (subject, expectations, defaults) { // provided that the subject is an object, we meet expectations that the keys in array must be a function // eslint-disable-next-line lodash/prefer-lodash-chain _.isObject(subject) && _.union(defaults, expectations).forEach(function (expect) { !_.isFunction(subject[expect]) && (subject[expect] = _.noop); }); return subject; }; module.exports = backpack = { /** * Ensures that the given argument is a callable. * * @param {*} arg * @param {Object=} ctx * @returns {boolean|*} */ ensure: function (arg, ctx) { return (typeof arg === 'function') && (ctx ? arg.bind(ctx) : arg) || undefined; }, /** * accept the callback parameter and convert it into a consistent object interface * * @param {Function|Object} cb * @param {Array} [expect=] * @returns {Object} * * @todo - write tests */ normalise: function (cb, expect) { if (_.isFunction(cb) && cb.__normalised) { return meetExpectations(cb, expect); } var userback, // this var will be populated and returned // keep a reference of all initial callbacks sent by user callback = (_.isFunction(cb) && cb) || (_.isFunction(cb && cb.done) && cb.done), callbackError = _.isFunction(cb && cb.error) && cb.error, callbackSuccess = _.isFunction(cb && cb.success) && cb.success; // create master callback that calls these user provided callbacks userback = _.assign(function (err) { // if common callback is defined, call that callback && callback.apply(this, arguments); // for special error and success, call them if they are user defined if (err) { callbackError && callbackError.apply(this, arguments); } else { // remove the extra error param before calling success callbackSuccess && callbackSuccess.apply(this, (Array.prototype.shift.call(arguments), arguments)); } }, _.isPlainObject(cb) && cb, { // override error, success and done error: function () { return userback.apply(this, arguments); }, success: function () { // inject null to arguments and call the main callback userback.apply(this, (Array.prototype.unshift.call(arguments, null), arguments)); }, done: function () { return userback.apply(this, arguments); }, __normalised: true }); return meetExpectations(userback, expect); }, /** * Convert a callback into a function that is called multiple times and the callback is actually called when a set * of flags are set to true * * @param {Array} flags * @param {Function} callback * @param {Array} args * @param {Number} ms * @returns {Function} */ multiback: function (flags, callback, args, ms) { var status = {}, sealed; // ensure that the callback times out after a while callback = backpack.timeback(callback, ms, null, function () { sealed = true; }); return function (err, flag, value) { if (sealed) { return; } // do not proceed of it is sealed status[flag] = value; if (err) { // on error we directly call the callback and seal subsequent calls sealed = true; status = null; callback.call(status, err); return; } // if any flag is not defined, we exit. when all flags hold a value, we know that the end callback has to be // executed. for (var i = 0, ii = flags.length; i < ii; i++) { if (!status.hasOwnProperty(flags[i])) { return; } } sealed = true; status = null; callback.apply(status, args); }; }, /** * Ensures that a callback is executed within a specific time. * * @param {Function} callback * @param {Number=} [ms] * @param {Object=} [scope] * @param {Function=} [when] - function executed right before callback is called with timeout. one can do cleanup * stuff here * @returns {Function} */ timeback: function (callback, ms, scope, when) { ms = Number(ms); // if np callback time is specified, just return the callback function and exit. this is because we do need to // track timeout in 0ms if (!ms) { return callback; } var sealed = false, irq = setTimeout(function () { // irq = interrupt request sealed = true; irq = null; when && when.call(scope || this); callback.call(scope || this, new Error('callback timed out')); }, ms); return function () { // if sealed, it means that timeout has elapsed and we accept no future callback if (sealed) { return undefined; } // otherwise we clear timeout and allow the callback to be executed. note that we do not seal the function // since we should allow multiple callback calls. irq && (irq = clearTimeout(irq)); return callback.apply(scope || this, arguments); }; } };