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
_.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);
};
}
};