// 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); }; }