You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							513 lines
						
					
					
						
							13 KiB
						
					
					
				
			
		
		
	
	
							513 lines
						
					
					
						
							13 KiB
						
					
					
				| 'use strict';
 | |
| 
 | |
| function Kareem() {
 | |
|   this._pres = new Map();
 | |
|   this._posts = new Map();
 | |
| }
 | |
| 
 | |
| Kareem.prototype.execPre = function(name, context, args, callback) {
 | |
|   if (arguments.length === 3) {
 | |
|     callback = args;
 | |
|     args = [];
 | |
|   }
 | |
|   var pres = get(this._pres, name, []);
 | |
|   var numPres = pres.length;
 | |
|   var numAsyncPres = pres.numAsync || 0;
 | |
|   var currentPre = 0;
 | |
|   var asyncPresLeft = numAsyncPres;
 | |
|   var done = false;
 | |
|   var $args = args;
 | |
| 
 | |
|   if (!numPres) {
 | |
|     return process.nextTick(function() {
 | |
|       callback(null);
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   var next = function() {
 | |
|     if (currentPre >= numPres) {
 | |
|       return;
 | |
|     }
 | |
|     var pre = pres[currentPre];
 | |
| 
 | |
|     if (pre.isAsync) {
 | |
|       var args = [
 | |
|         decorateNextFn(_next),
 | |
|         decorateNextFn(function(error) {
 | |
|           if (error) {
 | |
|             if (done) {
 | |
|               return;
 | |
|             }
 | |
|             done = true;
 | |
|             return callback(error);
 | |
|           }
 | |
|           if (--asyncPresLeft === 0 && currentPre >= numPres) {
 | |
|             return callback(null);
 | |
|           }
 | |
|         })
 | |
|       ];
 | |
| 
 | |
|       callMiddlewareFunction(pre.fn, context, args, args[0]);
 | |
|     } else if (pre.fn.length > 0) {
 | |
|       var args = [decorateNextFn(_next)];
 | |
|       var _args = arguments.length >= 2 ? arguments : [null].concat($args);
 | |
|       for (var i = 1; i < _args.length; ++i) {
 | |
|         args.push(_args[i]);
 | |
|       }
 | |
| 
 | |
|       callMiddlewareFunction(pre.fn, context, args, args[0]);
 | |
|     } else {
 | |
|       let maybePromise = null;
 | |
|       try {
 | |
|         maybePromise = pre.fn.call(context);
 | |
|       } catch (err) {
 | |
|         if (err != null) {
 | |
|           return callback(err);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       if (isPromise(maybePromise)) {
 | |
|         maybePromise.then(() => _next(), err => _next(err));
 | |
|       } else {
 | |
|         if (++currentPre >= numPres) {
 | |
|           if (asyncPresLeft > 0) {
 | |
|             // Leave parallel hooks to run
 | |
|             return;
 | |
|           } else {
 | |
|             return process.nextTick(function() {
 | |
|               callback(null);
 | |
|             });
 | |
|           }
 | |
|         }
 | |
|         next();
 | |
|       }
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   next.apply(null, [null].concat(args));
 | |
| 
 | |
|   function _next(error) {
 | |
|     if (error) {
 | |
|       if (done) {
 | |
|         return;
 | |
|       }
 | |
|       done = true;
 | |
|       return callback(error);
 | |
|     }
 | |
| 
 | |
|     if (++currentPre >= numPres) {
 | |
|       if (asyncPresLeft > 0) {
 | |
|         // Leave parallel hooks to run
 | |
|         return;
 | |
|       } else {
 | |
|         return callback(null);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     next.apply(context, arguments);
 | |
|   }
 | |
| };
 | |
| 
 | |
| Kareem.prototype.execPreSync = function(name, context, args) {
 | |
|   var pres = get(this._pres, name, []);
 | |
|   var numPres = pres.length;
 | |
| 
 | |
|   for (var i = 0; i < numPres; ++i) {
 | |
|     pres[i].fn.apply(context, args || []);
 | |
|   }
 | |
| };
 | |
| 
 | |
| Kareem.prototype.execPost = function(name, context, args, options, callback) {
 | |
|   if (arguments.length < 5) {
 | |
|     callback = options;
 | |
|     options = null;
 | |
|   }
 | |
|   var posts = get(this._posts, name, []);
 | |
|   var numPosts = posts.length;
 | |
|   var currentPost = 0;
 | |
| 
 | |
|   var firstError = null;
 | |
|   if (options && options.error) {
 | |
|     firstError = options.error;
 | |
|   }
 | |
| 
 | |
|   if (!numPosts) {
 | |
|     return process.nextTick(function() {
 | |
|       callback.apply(null, [firstError].concat(args));
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   var next = function() {
 | |
|     var post = posts[currentPost].fn;
 | |
|     var numArgs = 0;
 | |
|     var argLength = args.length;
 | |
|     var newArgs = [];
 | |
|     for (var i = 0; i < argLength; ++i) {
 | |
|       numArgs += args[i] && args[i]._kareemIgnore ? 0 : 1;
 | |
|       if (!args[i] || !args[i]._kareemIgnore) {
 | |
|         newArgs.push(args[i]);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (firstError) {
 | |
|       if (post.length === numArgs + 2) {
 | |
|         var _cb = decorateNextFn(function(error) {
 | |
|           if (error) {
 | |
|             firstError = error;
 | |
|           }
 | |
|           if (++currentPost >= numPosts) {
 | |
|             return callback.call(null, firstError);
 | |
|           }
 | |
|           next();
 | |
|         });
 | |
| 
 | |
|         callMiddlewareFunction(post, context,
 | |
|           [firstError].concat(newArgs).concat([_cb]), _cb);
 | |
|       } else {
 | |
|         if (++currentPost >= numPosts) {
 | |
|           return callback.call(null, firstError);
 | |
|         }
 | |
|         next();
 | |
|       }
 | |
|     } else {
 | |
|       const _cb = decorateNextFn(function(error) {
 | |
|         if (error) {
 | |
|           firstError = error;
 | |
|           return next();
 | |
|         }
 | |
| 
 | |
|         if (++currentPost >= numPosts) {
 | |
|           return callback.apply(null, [null].concat(args));
 | |
|         }
 | |
| 
 | |
|         next();
 | |
|       });
 | |
| 
 | |
|       if (post.length === numArgs + 2) {
 | |
|         // Skip error handlers if no error
 | |
|         if (++currentPost >= numPosts) {
 | |
|           return callback.apply(null, [null].concat(args));
 | |
|         }
 | |
|         return next();
 | |
|       }
 | |
|       if (post.length === numArgs + 1) {
 | |
|         callMiddlewareFunction(post, context, newArgs.concat([_cb]), _cb);
 | |
|       } else {
 | |
|         let error;
 | |
|         let maybePromise;
 | |
|         try {
 | |
|           maybePromise = post.apply(context, newArgs);
 | |
|         } catch (err) {
 | |
|           error = err;
 | |
|           firstError = err;
 | |
|         }
 | |
| 
 | |
|         if (isPromise(maybePromise)) {
 | |
|           return maybePromise.then(() => _cb(), err => _cb(err));
 | |
|         }
 | |
| 
 | |
|         if (++currentPost >= numPosts) {
 | |
|           return callback.apply(null, [error].concat(args));
 | |
|         }
 | |
| 
 | |
|         next(error);
 | |
|       }
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   next();
 | |
| };
 | |
| 
 | |
| Kareem.prototype.execPostSync = function(name, context, args) {
 | |
|   const posts = get(this._posts, name, []);
 | |
|   const numPosts = posts.length;
 | |
| 
 | |
|   for (let i = 0; i < numPosts; ++i) {
 | |
|     posts[i].fn.apply(context, args || []);
 | |
|   }
 | |
| };
 | |
| 
 | |
| Kareem.prototype.createWrapperSync = function(name, fn) {
 | |
|   var kareem = this;
 | |
|   return function syncWrapper() {
 | |
|     kareem.execPreSync(name, this, arguments);
 | |
| 
 | |
|     var toReturn = fn.apply(this, arguments);
 | |
| 
 | |
|     kareem.execPostSync(name, this, [toReturn]);
 | |
| 
 | |
|     return toReturn;
 | |
|   };
 | |
| }
 | |
| 
 | |
| function _handleWrapError(instance, error, name, context, args, options, callback) {
 | |
|   if (options.useErrorHandlers) {
 | |
|     var _options = { error: error };
 | |
|     return instance.execPost(name, context, args, _options, function(error) {
 | |
|       return typeof callback === 'function' && callback(error);
 | |
|     });
 | |
|   } else {
 | |
|     return typeof callback === 'function' ?
 | |
|       callback(error) :
 | |
|       undefined;
 | |
|   }
 | |
| }
 | |
| 
 | |
| Kareem.prototype.wrap = function(name, fn, context, args, options) {
 | |
|   const lastArg = (args.length > 0 ? args[args.length - 1] : null);
 | |
|   const argsWithoutCb = typeof lastArg === 'function' ?
 | |
|     args.slice(0, args.length - 1) :
 | |
|     args;
 | |
|   const _this = this;
 | |
| 
 | |
|   options = options || {};
 | |
|   const checkForPromise = options.checkForPromise;
 | |
| 
 | |
|   this.execPre(name, context, args, function(error) {
 | |
|     if (error) {
 | |
|       const numCallbackParams = options.numCallbackParams || 0;
 | |
|       const errorArgs = options.contextParameter ? [context] : [];
 | |
|       for (var i = errorArgs.length; i < numCallbackParams; ++i) {
 | |
|         errorArgs.push(null);
 | |
|       }
 | |
|       return _handleWrapError(_this, error, name, context, errorArgs,
 | |
|         options, lastArg);
 | |
|     }
 | |
| 
 | |
|     const end = (typeof lastArg === 'function' ? args.length - 1 : args.length);
 | |
|     const numParameters = fn.length;
 | |
|     const ret = fn.apply(context, args.slice(0, end).concat(_cb));
 | |
| 
 | |
|     if (checkForPromise) {
 | |
|       if (ret != null && typeof ret.then === 'function') {
 | |
|         // Thenable, use it
 | |
|         return ret.then(
 | |
|           res => _cb(null, res),
 | |
|           err => _cb(err)
 | |
|         );
 | |
|       }
 | |
| 
 | |
|       // If `fn()` doesn't have a callback argument and doesn't return a
 | |
|       // promise, assume it is sync
 | |
|       if (numParameters < end + 1) {
 | |
|         return _cb(null, ret);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function _cb() {
 | |
|       const args = arguments;
 | |
|       const argsWithoutError = Array.prototype.slice.call(arguments, 1);
 | |
|       if (options.nullResultByDefault && argsWithoutError.length === 0) {
 | |
|         argsWithoutError.push(null);
 | |
|       }
 | |
|       if (arguments[0]) {
 | |
|         // Assume error
 | |
|         return _handleWrapError(_this, arguments[0], name, context,
 | |
|           argsWithoutError, options, lastArg);
 | |
|       } else {
 | |
|         _this.execPost(name, context, argsWithoutError, function() {
 | |
|           if (arguments[0]) {
 | |
|             return typeof lastArg === 'function' ?
 | |
|               lastArg(arguments[0]) :
 | |
|               undefined;
 | |
|           }
 | |
| 
 | |
|           return typeof lastArg === 'function' ?
 | |
|             lastArg.apply(context, arguments) :
 | |
|             undefined;
 | |
|         });
 | |
|       }
 | |
|     }
 | |
|   });
 | |
| };
 | |
| 
 | |
| Kareem.prototype.filter = function(fn) {
 | |
|   const clone = this.clone();
 | |
| 
 | |
|   const pres = Array.from(clone._pres.keys());
 | |
|   for (const name of pres) {
 | |
|     const hooks = this._pres.get(name).
 | |
|       map(h => Object.assign({}, h, { name: name })).
 | |
|       filter(fn);
 | |
| 
 | |
|     if (hooks.length === 0) {
 | |
|       clone._pres.delete(name);
 | |
|       continue;
 | |
|     }
 | |
| 
 | |
|     hooks.numAsync = hooks.filter(h => h.isAsync).length;
 | |
| 
 | |
|     clone._pres.set(name, hooks);
 | |
|   }
 | |
| 
 | |
|   const posts = Array.from(clone._posts.keys());
 | |
|   for (const name of posts) {
 | |
|     const hooks = this._posts.get(name).
 | |
|       map(h => Object.assign({}, h, { name: name })).
 | |
|       filter(fn);
 | |
| 
 | |
|     if (hooks.length === 0) {
 | |
|       clone._posts.delete(name);
 | |
|       continue;
 | |
|     }
 | |
| 
 | |
|     clone._posts.set(name, hooks);
 | |
|   }
 | |
| 
 | |
|   return clone;
 | |
| };
 | |
| 
 | |
| Kareem.prototype.hasHooks = function(name) {
 | |
|   return this._pres.has(name) || this._posts.has(name);
 | |
| };
 | |
| 
 | |
| Kareem.prototype.createWrapper = function(name, fn, context, options) {
 | |
|   var _this = this;
 | |
|   if (!this.hasHooks(name)) {
 | |
|     // Fast path: if there's no hooks for this function, just return the
 | |
|     // function wrapped in a nextTick()
 | |
|     return function() {
 | |
|       process.nextTick(() => fn.apply(this, arguments));
 | |
|     };
 | |
|   }
 | |
|   return function() {
 | |
|     var _context = context || this;
 | |
|     var args = Array.prototype.slice.call(arguments);
 | |
|     _this.wrap(name, fn, _context, args, options);
 | |
|   };
 | |
| };
 | |
| 
 | |
| Kareem.prototype.pre = function(name, isAsync, fn, error, unshift) {
 | |
|   let options = {};
 | |
|   if (typeof isAsync === 'object' && isAsync != null) {
 | |
|     options = isAsync;
 | |
|     isAsync = options.isAsync;
 | |
|   } else if (typeof arguments[1] !== 'boolean') {
 | |
|     error = fn;
 | |
|     fn = isAsync;
 | |
|     isAsync = false;
 | |
|   }
 | |
| 
 | |
|   const pres = get(this._pres, name, []);
 | |
|   this._pres.set(name, pres);
 | |
| 
 | |
|   if (isAsync) {
 | |
|     pres.numAsync = pres.numAsync || 0;
 | |
|     ++pres.numAsync;
 | |
|   }
 | |
| 
 | |
|   if (typeof fn !== 'function') {
 | |
|     throw new Error('pre() requires a function, got "' + typeof fn + '"');
 | |
|   }
 | |
| 
 | |
|   if (unshift) {
 | |
|     pres.unshift(Object.assign({}, options, { fn: fn, isAsync: isAsync }));
 | |
|   } else {
 | |
|     pres.push(Object.assign({}, options, { fn: fn, isAsync: isAsync }));
 | |
|   }
 | |
| 
 | |
|   return this;
 | |
| };
 | |
| 
 | |
| Kareem.prototype.post = function(name, options, fn, unshift) {
 | |
|   const hooks = get(this._posts, name, []);
 | |
| 
 | |
|   if (typeof options === 'function') {
 | |
|     unshift = !!fn;
 | |
|     fn = options;
 | |
|     options = {};
 | |
|   }
 | |
| 
 | |
|   if (typeof fn !== 'function') {
 | |
|     throw new Error('post() requires a function, got "' + typeof fn + '"');
 | |
|   }
 | |
| 
 | |
|   if (unshift) {
 | |
|     hooks.unshift(Object.assign({}, options, { fn: fn }));
 | |
|   } else {
 | |
|     hooks.push(Object.assign({}, options, { fn: fn }));
 | |
|   }
 | |
|   this._posts.set(name, hooks);
 | |
|   return this;
 | |
| };
 | |
| 
 | |
| Kareem.prototype.clone = function() {
 | |
|   const n = new Kareem();
 | |
| 
 | |
|   for (let key of this._pres.keys()) {
 | |
|     const clone = this._pres.get(key).slice();
 | |
|     clone.numAsync = this._pres.get(key).numAsync;
 | |
|     n._pres.set(key, clone);
 | |
|   }
 | |
|   for (let key of this._posts.keys()) {
 | |
|     n._posts.set(key, this._posts.get(key).slice());
 | |
|   }
 | |
| 
 | |
|   return n;
 | |
| };
 | |
| 
 | |
| Kareem.prototype.merge = function(other, clone) {
 | |
|   clone = arguments.length === 1 ? true : clone;
 | |
|   var ret = clone ? this.clone() : this;
 | |
| 
 | |
|   for (let key of other._pres.keys()) {
 | |
|     const sourcePres = get(ret._pres, key, []);
 | |
|     const deduplicated = other._pres.get(key).
 | |
|       // Deduplicate based on `fn`
 | |
|       filter(p => sourcePres.map(_p => _p.fn).indexOf(p.fn) === -1);
 | |
|     const combined = sourcePres.concat(deduplicated);
 | |
|     combined.numAsync = sourcePres.numAsync || 0;
 | |
|     combined.numAsync += deduplicated.filter(p => p.isAsync).length;
 | |
|     ret._pres.set(key, combined);
 | |
|   }
 | |
|   for (let key of other._posts.keys()) {
 | |
|     const sourcePosts = get(ret._posts, key, []);
 | |
|     const deduplicated = other._posts.get(key).
 | |
|       filter(p => sourcePosts.indexOf(p) === -1);
 | |
|     ret._posts.set(key, sourcePosts.concat(deduplicated));
 | |
|   }
 | |
| 
 | |
|   return ret;
 | |
| };
 | |
| 
 | |
| function get(map, key, def) {
 | |
|   if (map.has(key)) {
 | |
|     return map.get(key);
 | |
|   }
 | |
|   return def;
 | |
| }
 | |
| 
 | |
| function callMiddlewareFunction(fn, context, args, next) {
 | |
|   let maybePromise;
 | |
|   try {
 | |
|     maybePromise = fn.apply(context, args);
 | |
|   } catch (error) {
 | |
|     return next(error);
 | |
|   }
 | |
| 
 | |
|   if (isPromise(maybePromise)) {
 | |
|     maybePromise.then(() => next(), err => next(err));
 | |
|   }
 | |
| }
 | |
| 
 | |
| function isPromise(v) {
 | |
|   return v != null && typeof v.then === 'function';
 | |
| }
 | |
| 
 | |
| function decorateNextFn(fn) {
 | |
|   var called = false;
 | |
|   var _this = this;
 | |
|   return function() {
 | |
|     // Ensure this function can only be called once
 | |
|     if (called) {
 | |
|       return;
 | |
|     }
 | |
|     called = true;
 | |
|     // Make sure to clear the stack so try/catch doesn't catch errors
 | |
|     // in subsequent middleware
 | |
|     return process.nextTick(() => fn.apply(_this, arguments));
 | |
|   };
 | |
| }
 | |
| 
 | |
| module.exports = Kareem;
 |