hooks-fixed
Version:
Adds pre and post hook functionality to your JavaScript methods.
193 lines (175 loc) • 6.61 kB
JavaScript
// TODO Add in pre and post skipping options
module.exports = {
/**
* Declares a new hook to which you can add pres and posts
* @param {String} name of the function
* @param {Function} the method
* @param {Function} the error handler callback
*/
$hook: function (name, fn, errorCb) {
if (arguments.length === 1 && typeof name === 'object') {
for (var k in name) { // `name` is a hash of hookName->hookFn
this.$hook(k, name[k]);
}
return;
}
var proto = this.prototype || this
, pres = proto._pres = proto._pres || {}
, posts = proto._posts = proto._posts || {};
pres[name] = pres[name] || [];
posts[name] = posts[name] || [];
proto[name] = function () {
var self = this
, hookArgs // arguments eventually passed to the hook - are mutable
, lastArg = arguments[arguments.length-1]
, pres = this._pres[name]
, posts = this._posts[name]
, _total = pres.length
, _current = -1
, _asyncsLeft = proto[name].numAsyncPres
, _asyncsDone = function(err) {
if (err) {
return handleError(err);
}
--_asyncsLeft || _done.apply(self, hookArgs);
}
, handleError = function(err) {
if ('function' == typeof lastArg)
return lastArg(err);
if (errorCb) return errorCb.call(self, err);
throw err;
}
, _next = function () {
if (arguments[0] instanceof Error) {
return handleError(arguments[0]);
}
var _args = Array.prototype.slice.call(arguments)
, currPre
, preArgs;
if (_args.length && !(arguments[0] == null && typeof lastArg === 'function'))
hookArgs = _args;
if (++_current < _total) {
currPre = pres[_current]
if (currPre.isAsync && currPre.length < 2)
throw new Error("Your pre must have next and done arguments -- e.g., function (next, done, ...)");
if (currPre.length < 1)
throw new Error("Your pre must have a next argument -- e.g., function (next, ...)");
preArgs = (currPre.isAsync
? [once(_next), once(_asyncsDone)]
: [once(_next)]).concat(hookArgs);
try {
return currPre.apply(self, preArgs);
} catch (error) {
_next(error);
}
} else if (!_asyncsLeft) {
return _done.apply(self, hookArgs);
}
}
, _done = function () {
var args_ = Array.prototype.slice.call(arguments)
, ret, total_, current_, next_, done_, postArgs;
if (_current === _total) {
next_ = function () {
if (arguments[0] instanceof Error) {
return handleError(arguments[0]);
}
var args_ = Array.prototype.slice.call(arguments, 1)
, currPost
, postArgs;
if (args_.length) hookArgs = args_;
if (++current_ < total_) {
currPost = posts[current_]
if (currPost.length < 1)
throw new Error("Your post must have a next argument -- e.g., function (next, ...)");
postArgs = [once(next_)].concat(hookArgs);
return currPost.apply(self, postArgs);
} else if (typeof lastArg === 'function'){
// All post handlers are done, call original callback function
return lastArg.apply(self, arguments);
}
};
// We are assuming that if the last argument provided to the wrapped function is a function, it was expecting
// a callback. We trap that callback and wait to call it until all post handlers have finished.
if(typeof lastArg === 'function'){
args_[args_.length - 1] = once(next_);
}
total_ = posts.length;
current_ = -1;
ret = fn.apply(self, args_); // Execute wrapped function, post handlers come afterward
if (total_ && typeof lastArg !== 'function') return next_(); // no callback provided, execute next_() manually
return ret;
}
};
return _next.apply(this, arguments);
};
proto[name].numAsyncPres = 0;
return this;
},
pre: function (name, isAsync, fn, errorCb) {
if ('boolean' !== typeof arguments[1]) {
errorCb = fn;
fn = isAsync;
isAsync = false;
}
var proto = this.prototype || this
, pres = proto._pres = proto._pres || {};
this._lazySetupHooks(proto, name, errorCb);
if (fn.isAsync = isAsync) {
proto[name].numAsyncPres++;
}
(pres[name] = pres[name] || []).push(fn);
return this;
},
post: function (name, isAsync, fn) {
if (arguments.length === 2) {
fn = isAsync;
isAsync = false;
}
var proto = this.prototype || this
, posts = proto._posts = proto._posts || {};
this._lazySetupHooks(proto, name);
(posts[name] = posts[name] || []).push(fn);
return this;
},
removePre: function (name, fnToRemove) {
var proto = this.prototype || this
, pres = proto._pres || (proto._pres || {});
if (!pres[name]) return this;
if (arguments.length === 1) {
// Remove all pre callbacks for hook `name`
pres[name].length = 0;
} else {
pres[name] = pres[name].filter( function (currFn) {
return currFn !== fnToRemove;
});
}
return this;
},
removePost: function (name, fnToRemove) {
var proto = this.prototype || this
, posts = proto._posts || (proto._posts || {});
if (!posts[name]) return this;
if (arguments.length === 1) {
// Remove all post callbacks for hook `name`
posts[name].length = 0;
} else {
posts[name] = posts[name].filter( function (currFn) {
return currFn !== fnToRemove;
});
}
return this;
},
_lazySetupHooks: function (proto, methodName, errorCb) {
if ('undefined' === typeof proto[methodName].numAsyncPres) {
this.$hook(methodName, proto[methodName], errorCb);
}
}
};
function once (fn, scope) {
return function fnWrapper () {
if (fnWrapper.hookCalled) return;
fnWrapper.hookCalled = true;
fn.apply(scope, arguments);
};
}