hooks.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  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. try {
  61. return currPre.apply(self, preArgs);
  62. } catch (error) {
  63. _next(error);
  64. }
  65. } else if (!_asyncsLeft) {
  66. return _done.apply(self, hookArgs);
  67. }
  68. }
  69. , _done = function () {
  70. var args_ = Array.prototype.slice.call(arguments)
  71. , ret, total_, current_, next_, done_, postArgs;
  72. if (_current === _total) {
  73. next_ = function () {
  74. if (arguments[0] instanceof Error) {
  75. return handleError(arguments[0]);
  76. }
  77. var args_ = Array.prototype.slice.call(arguments, 1)
  78. , currPost
  79. , postArgs;
  80. if (args_.length) hookArgs = args_;
  81. if (++current_ < total_) {
  82. currPost = posts[current_]
  83. if (currPost.length < 1)
  84. throw new Error("Your post must have a next argument -- e.g., function (next, ...)");
  85. postArgs = [once(next_)].concat(hookArgs);
  86. return currPost.apply(self, postArgs);
  87. } else if (typeof lastArg === 'function'){
  88. // All post handlers are done, call original callback function
  89. return lastArg.apply(self, arguments);
  90. }
  91. };
  92. // We are assuming that if the last argument provided to the wrapped function is a function, it was expecting
  93. // a callback. We trap that callback and wait to call it until all post handlers have finished.
  94. if(typeof lastArg === 'function'){
  95. args_[args_.length - 1] = once(next_);
  96. }
  97. total_ = posts.length;
  98. current_ = -1;
  99. ret = fn.apply(self, args_); // Execute wrapped function, post handlers come afterward
  100. if (total_ && typeof lastArg !== 'function') return next_(); // no callback provided, execute next_() manually
  101. return ret;
  102. }
  103. };
  104. return _next.apply(this, arguments);
  105. };
  106. proto[name].numAsyncPres = 0;
  107. return this;
  108. },
  109. pre: function (name, isAsync, fn, errorCb) {
  110. if ('boolean' !== typeof arguments[1]) {
  111. errorCb = fn;
  112. fn = isAsync;
  113. isAsync = false;
  114. }
  115. var proto = this.prototype || this
  116. , pres = proto._pres = proto._pres || {};
  117. this._lazySetupHooks(proto, name, errorCb);
  118. if (fn.isAsync = isAsync) {
  119. proto[name].numAsyncPres++;
  120. }
  121. (pres[name] = pres[name] || []).push(fn);
  122. return this;
  123. },
  124. post: function (name, isAsync, fn) {
  125. if (arguments.length === 2) {
  126. fn = isAsync;
  127. isAsync = false;
  128. }
  129. var proto = this.prototype || this
  130. , posts = proto._posts = proto._posts || {};
  131. this._lazySetupHooks(proto, name);
  132. (posts[name] = posts[name] || []).push(fn);
  133. return this;
  134. },
  135. removePre: function (name, fnToRemove) {
  136. var proto = this.prototype || this
  137. , pres = proto._pres || (proto._pres || {});
  138. if (!pres[name]) return this;
  139. if (arguments.length === 1) {
  140. // Remove all pre callbacks for hook `name`
  141. pres[name].length = 0;
  142. } else {
  143. pres[name] = pres[name].filter( function (currFn) {
  144. return currFn !== fnToRemove;
  145. });
  146. }
  147. return this;
  148. },
  149. removePost: function (name, fnToRemove) {
  150. var proto = this.prototype || this
  151. , posts = proto._posts || (proto._posts || {});
  152. if (!posts[name]) return this;
  153. if (arguments.length === 1) {
  154. // Remove all post callbacks for hook `name`
  155. posts[name].length = 0;
  156. } else {
  157. posts[name] = posts[name].filter( function (currFn) {
  158. return currFn !== fnToRemove;
  159. });
  160. }
  161. return this;
  162. },
  163. _lazySetupHooks: function (proto, methodName, errorCb) {
  164. if ('undefined' === typeof proto[methodName].numAsyncPres) {
  165. this.$hook(methodName, proto[methodName], errorCb);
  166. }
  167. }
  168. };
  169. function once (fn, scope) {
  170. return function fnWrapper () {
  171. if (fnWrapper.hookCalled) return;
  172. fnWrapper.hookCalled = true;
  173. fn.apply(scope, arguments);
  174. };
  175. }