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
			| 
											3 years ago
										 | '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; |