hooks.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. // TODO Add in pre and post skipping options
  2. module.exports = {
  3. /**
  4. * Declares a new hook to which you can add pres and posts
  5. * @param {String} name of the function
  6. * @param {Function} the method
  7. * @param {Function} the error handler callback
  8. */
  9. hook: function (name, fn, errorCb) {
  10. if (arguments.length === 1 && typeof name === 'object') {
  11. for (var k in name) { // `name` is a hash of hookName->hookFn
  12. this.hook(k, name[k]);
  13. }
  14. return;
  15. }
  16. var proto = this.prototype || this
  17. , pres = proto._pres = proto._pres || {}
  18. , posts = proto._posts = proto._posts || {};
  19. pres[name] = pres[name] || [];
  20. posts[name] = posts[name] || [];
  21. proto[name] = function () {
  22. var self = this
  23. , hookArgs // arguments eventually passed to the hook - are mutable
  24. , lastArg = arguments[arguments.length-1]
  25. , pres = this._pres[name]
  26. , posts = this._posts[name]
  27. , _total = pres.length
  28. , _current = -1
  29. , _asyncsLeft = proto[name].numAsyncPres
  30. , _asyncsDone = function(err) {
  31. if (err) {
  32. return handleError(err);
  33. }
  34. --_asyncsLeft || _done.apply(self, hookArgs);
  35. }
  36. , handleError = function(err) {
  37. if ('function' == typeof lastArg)
  38. return lastArg(err);
  39. if (errorCb) return errorCb.call(self, err);
  40. throw err;
  41. }
  42. , _next = function () {
  43. if (arguments[0] instanceof Error) {
  44. return handleError(arguments[0]);
  45. }
  46. var _args = Array.prototype.slice.call(arguments)
  47. , currPre
  48. , preArgs;
  49. if (_args.length && !(arguments[0] == null && typeof lastArg === 'function'))
  50. hookArgs = _args;
  51. if (++_current < _total) {
  52. currPre = pres[_current]
  53. if (currPre.isAsync && currPre.length < 2)
  54. throw new Error("Your pre must have next and done arguments -- e.g., function (next, done, ...)");
  55. if (currPre.length < 1)
  56. throw new Error("Your pre must have a next argument -- e.g., function (next, ...)");
  57. preArgs = (currPre.isAsync
  58. ? [once(_next), once(_asyncsDone)]
  59. : [once(_next)]).concat(hookArgs);
  60. return currPre.apply(self, preArgs);
  61. } else if (!_asyncsLeft) {
  62. return _done.apply(self, hookArgs);
  63. }
  64. }
  65. , _done = function () {
  66. var args_ = Array.prototype.slice.call(arguments)
  67. , ret, total_, current_, next_, done_, postArgs;
  68. if (_current === _total) {
  69. next_ = function () {
  70. if (arguments[0] instanceof Error) {
  71. return handleError(arguments[0]);
  72. }
  73. var args_ = Array.prototype.slice.call(arguments, 1)
  74. , currPost
  75. , postArgs;
  76. if (args_.length) hookArgs = args_;
  77. if (++current_ < total_) {
  78. currPost = posts[current_]
  79. if (currPost.length < 1)
  80. throw new Error("Your post must have a next argument -- e.g., function (next, ...)");
  81. postArgs = [once(next_)].concat(hookArgs);
  82. return currPost.apply(self, postArgs);
  83. } else if (typeof lastArg === 'function'){
  84. // All post handlers are done, call original callback function
  85. return lastArg.apply(self, arguments);
  86. }
  87. };
  88. // We are assuming that if the last argument provided to the wrapped function is a function, it was expecting
  89. // a callback. We trap that callback and wait to call it until all post handlers have finished.
  90. if(typeof lastArg === 'function'){
  91. args_[args_.length - 1] = once(next_);
  92. }
  93. total_ = posts.length;
  94. current_ = -1;
  95. ret = fn.apply(self, args_); // Execute wrapped function, post handlers come afterward
  96. if (total_ && typeof lastArg !== 'function') return next_(); // no callback provided, execute next_() manually
  97. return ret;
  98. }
  99. };
  100. return _next.apply(this, arguments);
  101. };
  102. proto[name].numAsyncPres = 0;
  103. return this;
  104. },
  105. pre: function (name, isAsync, fn, errorCb) {
  106. if ('boolean' !== typeof arguments[1]) {
  107. errorCb = fn;
  108. fn = isAsync;
  109. isAsync = false;
  110. }
  111. var proto = this.prototype || this
  112. , pres = proto._pres = proto._pres || {};
  113. this._lazySetupHooks(proto, name, errorCb);
  114. if (fn.isAsync = isAsync) {
  115. proto[name].numAsyncPres++;
  116. }
  117. (pres[name] = pres[name] || []).push(fn);
  118. return this;
  119. },
  120. post: function (name, isAsync, fn) {
  121. if (arguments.length === 2) {
  122. fn = isAsync;
  123. isAsync = false;
  124. }
  125. var proto = this.prototype || this
  126. , posts = proto._posts = proto._posts || {};
  127. this._lazySetupHooks(proto, name);
  128. (posts[name] = posts[name] || []).push(fn);
  129. return this;
  130. },
  131. removePre: function (name, fnToRemove) {
  132. var proto = this.prototype || this
  133. , pres = proto._pres || (proto._pres || {});
  134. if (!pres[name]) return this;
  135. if (arguments.length === 1) {
  136. // Remove all pre callbacks for hook `name`
  137. pres[name].length = 0;
  138. } else {
  139. pres[name] = pres[name].filter( function (currFn) {
  140. return currFn !== fnToRemove;
  141. });
  142. }
  143. return this;
  144. },
  145. removePost: function (name, fnToRemove) {
  146. var proto = this.prototype || this
  147. , posts = proto._posts || (proto._posts || {});
  148. if (!posts[name]) return this;
  149. if (arguments.length === 1) {
  150. // Remove all post callbacks for hook `name`
  151. posts[name].length = 0;
  152. } else {
  153. posts[name] = posts[name].filter( function (currFn) {
  154. return currFn !== fnToRemove;
  155. });
  156. }
  157. return this;
  158. },
  159. _lazySetupHooks: function (proto, methodName, errorCb) {
  160. if ('undefined' === typeof proto[methodName].numAsyncPres) {
  161. this.hook(methodName, proto[methodName], errorCb);
  162. }
  163. }
  164. };
  165. function once (fn, scope) {
  166. return function fnWrapper () {
  167. if (fnWrapper.hookCalled) return;
  168. fnWrapper.hookCalled = true;
  169. fn.apply(scope, arguments);
  170. };
  171. }