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.
668 lines
18 KiB
668 lines
18 KiB
'use strict'; |
|
|
|
/** |
|
* Create a new instance |
|
*/ |
|
function Kareem() { |
|
this._pres = new Map(); |
|
this._posts = new Map(); |
|
} |
|
|
|
Kareem.skipWrappedFunction = function skipWrappedFunction() { |
|
if (!(this instanceof Kareem.skipWrappedFunction)) { |
|
return new Kareem.skipWrappedFunction(...arguments); |
|
} |
|
|
|
this.args = [...arguments]; |
|
}; |
|
|
|
Kareem.overwriteResult = function overwriteResult() { |
|
if (!(this instanceof Kareem.overwriteResult)) { |
|
return new Kareem.overwriteResult(...arguments); |
|
} |
|
|
|
this.args = [...arguments]; |
|
}; |
|
|
|
/** |
|
* Execute all "pre" hooks for "name" |
|
* @param {String} name The hook name to execute |
|
* @param {*} context Overwrite the "this" for the hook |
|
* @param {Array|Function} args Optional arguments or directly the callback |
|
* @param {Function} [callback] The callback to call when executing all hooks are finished |
|
* @returns {void} |
|
*/ |
|
Kareem.prototype.execPre = function(name, context, args, callback) { |
|
if (arguments.length === 3) { |
|
callback = args; |
|
args = []; |
|
} |
|
const pres = this._pres.get(name) || []; |
|
const numPres = pres.length; |
|
const numAsyncPres = pres.numAsync || 0; |
|
let currentPre = 0; |
|
let asyncPresLeft = numAsyncPres; |
|
let done = false; |
|
const $args = args; |
|
let shouldSkipWrappedFunction = null; |
|
|
|
if (!numPres) { |
|
return nextTick(function() { |
|
callback(null); |
|
}); |
|
} |
|
|
|
function next() { |
|
if (currentPre >= numPres) { |
|
return; |
|
} |
|
const pre = pres[currentPre]; |
|
|
|
if (pre.isAsync) { |
|
const args = [ |
|
decorateNextFn(_next), |
|
decorateNextFn(function(error) { |
|
if (error) { |
|
if (done) { |
|
return; |
|
} |
|
if (error instanceof Kareem.skipWrappedFunction) { |
|
shouldSkipWrappedFunction = error; |
|
} else { |
|
done = true; |
|
return callback(error); |
|
} |
|
} |
|
if (--asyncPresLeft === 0 && currentPre >= numPres) { |
|
return callback(shouldSkipWrappedFunction); |
|
} |
|
}) |
|
]; |
|
|
|
callMiddlewareFunction(pre.fn, context, args, args[0]); |
|
} else if (pre.fn.length > 0) { |
|
const args = [decorateNextFn(_next)]; |
|
const _args = arguments.length >= 2 ? arguments : [null].concat($args); |
|
for (let i = 1; i < _args.length; ++i) { |
|
if (i === _args.length - 1 && typeof _args[i] === 'function') { |
|
continue; // skip callbacks to avoid accidentally calling the callback from a hook |
|
} |
|
args.push(_args[i]); |
|
} |
|
|
|
callMiddlewareFunction(pre.fn, context, args, args[0]); |
|
} else { |
|
let maybePromiseLike = null; |
|
try { |
|
maybePromiseLike = pre.fn.call(context); |
|
} catch (err) { |
|
if (err != null) { |
|
return callback(err); |
|
} |
|
} |
|
|
|
if (isPromiseLike(maybePromiseLike)) { |
|
maybePromiseLike.then(() => _next(), err => _next(err)); |
|
} else { |
|
if (++currentPre >= numPres) { |
|
if (asyncPresLeft > 0) { |
|
// Leave parallel hooks to run |
|
return; |
|
} else { |
|
return nextTick(function() { |
|
callback(shouldSkipWrappedFunction); |
|
}); |
|
} |
|
} |
|
next(); |
|
} |
|
} |
|
} |
|
|
|
next.apply(null, [null].concat(args)); |
|
|
|
function _next(error) { |
|
if (error) { |
|
if (done) { |
|
return; |
|
} |
|
if (error instanceof Kareem.skipWrappedFunction) { |
|
shouldSkipWrappedFunction = error; |
|
} else { |
|
done = true; |
|
return callback(error); |
|
} |
|
} |
|
|
|
if (++currentPre >= numPres) { |
|
if (asyncPresLeft > 0) { |
|
// Leave parallel hooks to run |
|
return; |
|
} else { |
|
return callback(shouldSkipWrappedFunction); |
|
} |
|
} |
|
|
|
next.apply(context, arguments); |
|
} |
|
}; |
|
|
|
/** |
|
* Execute all "pre" hooks for "name" synchronously |
|
* @param {String} name The hook name to execute |
|
* @param {*} context Overwrite the "this" for the hook |
|
* @param {Array} [args] Apply custom arguments to the hook |
|
* @returns {void} |
|
*/ |
|
Kareem.prototype.execPreSync = function(name, context, args) { |
|
const pres = this._pres.get(name) || []; |
|
const numPres = pres.length; |
|
|
|
for (let i = 0; i < numPres; ++i) { |
|
pres[i].fn.apply(context, args || []); |
|
} |
|
}; |
|
|
|
/** |
|
* Execute all "post" hooks for "name" |
|
* @param {String} name The hook name to execute |
|
* @param {*} context Overwrite the "this" for the hook |
|
* @param {Array|Function} args Apply custom arguments to the hook |
|
* @param {*} options Optional options or directly the callback |
|
* @param {Function} [callback] The callback to call when executing all hooks are finished |
|
* @returns {void} |
|
*/ |
|
Kareem.prototype.execPost = function(name, context, args, options, callback) { |
|
if (arguments.length < 5) { |
|
callback = options; |
|
options = null; |
|
} |
|
const posts = this._posts.get(name) || []; |
|
const numPosts = posts.length; |
|
let currentPost = 0; |
|
|
|
let firstError = null; |
|
if (options && options.error) { |
|
firstError = options.error; |
|
} |
|
|
|
if (!numPosts) { |
|
return nextTick(function() { |
|
callback.apply(null, [firstError].concat(args)); |
|
}); |
|
} |
|
|
|
function next() { |
|
const post = posts[currentPost].fn; |
|
let numArgs = 0; |
|
const argLength = args.length; |
|
const newArgs = []; |
|
for (let 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 (isErrorHandlingMiddleware(posts[currentPost], numArgs)) { |
|
const _cb = decorateNextFn(function(error) { |
|
if (error) { |
|
if (error instanceof Kareem.overwriteResult) { |
|
args = error.args; |
|
if (++currentPost >= numPosts) { |
|
return callback.call(null, firstError); |
|
} |
|
return next(); |
|
} |
|
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) { |
|
if (error instanceof Kareem.overwriteResult) { |
|
args = error.args; |
|
if (++currentPost >= numPosts) { |
|
return callback.apply(null, [null].concat(args)); |
|
} |
|
return next(); |
|
} |
|
firstError = error; |
|
return next(); |
|
} |
|
|
|
if (++currentPost >= numPosts) { |
|
return callback.apply(null, [null].concat(args)); |
|
} |
|
|
|
next(); |
|
}); |
|
|
|
if (isErrorHandlingMiddleware(posts[currentPost], numArgs)) { |
|
// 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 maybePromiseLike; |
|
try { |
|
maybePromiseLike = post.apply(context, newArgs); |
|
} catch (err) { |
|
error = err; |
|
firstError = err; |
|
} |
|
|
|
if (isPromiseLike(maybePromiseLike)) { |
|
return maybePromiseLike.then( |
|
(res) => { |
|
_cb(res instanceof Kareem.overwriteResult ? res : null); |
|
}, |
|
err => _cb(err) |
|
); |
|
} |
|
|
|
if (maybePromiseLike instanceof Kareem.overwriteResult) { |
|
args = maybePromiseLike.args; |
|
} |
|
|
|
if (++currentPost >= numPosts) { |
|
return callback.apply(null, [error].concat(args)); |
|
} |
|
|
|
next(); |
|
} |
|
} |
|
} |
|
|
|
next(); |
|
}; |
|
|
|
/** |
|
* Execute all "post" hooks for "name" synchronously |
|
* @param {String} name The hook name to execute |
|
* @param {*} context Overwrite the "this" for the hook |
|
* @param {Array|Function} args Apply custom arguments to the hook |
|
* @returns {Array} The used arguments |
|
*/ |
|
Kareem.prototype.execPostSync = function(name, context, args) { |
|
const posts = this._posts.get(name) || []; |
|
const numPosts = posts.length; |
|
|
|
for (let i = 0; i < numPosts; ++i) { |
|
const res = posts[i].fn.apply(context, args || []); |
|
if (res instanceof Kareem.overwriteResult) { |
|
args = res.args; |
|
} |
|
} |
|
|
|
return args; |
|
}; |
|
|
|
/** |
|
* Create a synchronous wrapper for "fn" |
|
* @param {String} name The name of the hook |
|
* @param {Function} fn The function to wrap |
|
* @returns {Function} The wrapped function |
|
*/ |
|
Kareem.prototype.createWrapperSync = function(name, fn) { |
|
const _this = this; |
|
return function syncWrapper() { |
|
_this.execPreSync(name, this, arguments); |
|
|
|
const toReturn = fn.apply(this, arguments); |
|
|
|
const result = _this.execPostSync(name, this, [toReturn]); |
|
|
|
return result[0]; |
|
}; |
|
}; |
|
|
|
function _handleWrapError(instance, error, name, context, args, options, callback) { |
|
if (options.useErrorHandlers) { |
|
return instance.execPost(name, context, args, { error: error }, function(error) { |
|
return typeof callback === 'function' && callback(error); |
|
}); |
|
} else { |
|
return typeof callback === 'function' && callback(error); |
|
} |
|
} |
|
|
|
/** |
|
* Executes pre hooks, followed by the wrapped function, followed by post hooks. |
|
* @param {String} name The name of the hook |
|
* @param {Function} fn The function for the hook |
|
* @param {*} context Overwrite the "this" for the hook |
|
* @param {Array} args Apply custom arguments to the hook |
|
* @param {Object} [options] |
|
* @param {Boolean} [options.checkForPromise] |
|
* @returns {void} |
|
*/ |
|
Kareem.prototype.wrap = function(name, fn, context, args, options) { |
|
const lastArg = (args.length > 0 ? args[args.length - 1] : null); |
|
const argsWithoutCb = Array.from(args); |
|
typeof lastArg === 'function' && argsWithoutCb.pop(); |
|
const _this = this; |
|
|
|
options = options || {}; |
|
const checkForPromise = options.checkForPromise; |
|
|
|
this.execPre(name, context, args, function(error) { |
|
if (error && !(error instanceof Kareem.skipWrappedFunction)) { |
|
const numCallbackParams = options.numCallbackParams || 0; |
|
const errorArgs = options.contextParameter ? [context] : []; |
|
for (let i = errorArgs.length; i < numCallbackParams; ++i) { |
|
errorArgs.push(null); |
|
} |
|
return _handleWrapError(_this, error, name, context, errorArgs, |
|
options, lastArg); |
|
} |
|
|
|
const numParameters = fn.length; |
|
let ret; |
|
|
|
if (error instanceof Kareem.skipWrappedFunction) { |
|
ret = error.args[0]; |
|
return _cb(null, ...error.args); |
|
} else { |
|
try { |
|
ret = fn.apply(context, argsWithoutCb.concat(_cb)); |
|
} catch (err) { |
|
return _cb(err); |
|
} |
|
} |
|
|
|
if (checkForPromise) { |
|
if (isPromiseLike(ret)) { |
|
// 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 < argsWithoutCb.length + 1) { |
|
return _cb(null, ret); |
|
} |
|
} |
|
|
|
function _cb() { |
|
const argsWithoutError = Array.from(arguments); |
|
argsWithoutError.shift(); |
|
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 (lastArg === null) { |
|
return; |
|
} |
|
arguments[0] |
|
? lastArg(arguments[0]) |
|
: lastArg.apply(context, arguments); |
|
}); |
|
} |
|
} |
|
}); |
|
}; |
|
|
|
/** |
|
* Filter current instance for something specific and return the filtered clone |
|
* @param {Function} fn The filter function |
|
* @returns {Kareem} The cloned and filtered instance |
|
*/ |
|
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; |
|
}; |
|
|
|
/** |
|
* Check for a "name" to exist either in pre or post hooks |
|
* @param {String} name The name of the hook |
|
* @returns {Boolean} "true" if found, "false" otherwise |
|
*/ |
|
Kareem.prototype.hasHooks = function(name) { |
|
return this._pres.has(name) || this._posts.has(name); |
|
}; |
|
|
|
/** |
|
* Create a Wrapper for "fn" on "name" and return the wrapped function |
|
* @param {String} name The name of the hook |
|
* @param {Function} fn The function to wrap |
|
* @param {*} context Overwrite the "this" for the hook |
|
* @param {Object} [options] |
|
* @returns {Function} The wrapped function |
|
*/ |
|
Kareem.prototype.createWrapper = function(name, fn, context, options) { |
|
const _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() { |
|
nextTick(() => fn.apply(this, arguments)); |
|
}; |
|
} |
|
return function() { |
|
const _context = context || this; |
|
_this.wrap(name, fn, _context, Array.from(arguments), options); |
|
}; |
|
}; |
|
|
|
/** |
|
* Register a new hook for "pre" |
|
* @param {String} name The name of the hook |
|
* @param {Boolean} [isAsync] |
|
* @param {Function} fn The function to register for "name" |
|
* @param {never} error Unused |
|
* @param {Boolean} [unshift] Wheter to "push" or to "unshift" the new hook |
|
* @returns {Kareem} |
|
*/ |
|
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') { |
|
fn = isAsync; |
|
isAsync = false; |
|
} |
|
|
|
const pres = this._pres.get(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; |
|
}; |
|
|
|
/** |
|
* Register a new hook for "post" |
|
* @param {String} name The name of the hook |
|
* @param {Object} [options] |
|
* @param {Function} fn The function to register for "name" |
|
* @param {Boolean} [unshift] Wheter to "push" or to "unshift" the new hook |
|
* @returns {Kareem} |
|
*/ |
|
Kareem.prototype.post = function(name, options, fn, unshift) { |
|
const posts = this._posts.get(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) { |
|
posts.unshift(Object.assign({}, options, { fn: fn })); |
|
} else { |
|
posts.push(Object.assign({}, options, { fn: fn })); |
|
} |
|
this._posts.set(name, posts); |
|
return this; |
|
}; |
|
|
|
/** |
|
* Clone the current instance |
|
* @returns {Kareem} The cloned instance |
|
*/ |
|
Kareem.prototype.clone = function() { |
|
const n = new Kareem(); |
|
|
|
for (const 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 (const key of this._posts.keys()) { |
|
n._posts.set(key, this._posts.get(key).slice()); |
|
} |
|
|
|
return n; |
|
}; |
|
|
|
/** |
|
* Merge "other" into self or "clone" |
|
* @param {Kareem} other The instance to merge with |
|
* @param {Kareem} [clone] The instance to merge onto (if not defined, using "this") |
|
* @returns {Kareem} The merged instance |
|
*/ |
|
Kareem.prototype.merge = function(other, clone) { |
|
clone = arguments.length === 1 ? true : clone; |
|
const ret = clone ? this.clone() : this; |
|
|
|
for (const key of other._pres.keys()) { |
|
const sourcePres = ret._pres.get(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 (const key of other._posts.keys()) { |
|
const sourcePosts = ret._posts.get(key) || []; |
|
const deduplicated = other._posts.get(key). |
|
filter(p => sourcePosts.indexOf(p) === -1); |
|
ret._posts.set(key, sourcePosts.concat(deduplicated)); |
|
} |
|
|
|
return ret; |
|
}; |
|
|
|
function callMiddlewareFunction(fn, context, args, next) { |
|
let maybePromiseLike; |
|
try { |
|
maybePromiseLike = fn.apply(context, args); |
|
} catch (error) { |
|
return next(error); |
|
} |
|
|
|
if (isPromiseLike(maybePromiseLike)) { |
|
maybePromiseLike.then(() => next(), err => next(err)); |
|
} |
|
} |
|
|
|
function isPromiseLike(v) { |
|
return (typeof v === 'object' && v !== null && typeof v.then === 'function'); |
|
} |
|
|
|
function decorateNextFn(fn) { |
|
let called = false; |
|
const _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 nextTick(() => fn.apply(_this, arguments)); |
|
}; |
|
} |
|
|
|
const nextTick = typeof process === 'object' && process !== null && process.nextTick || function nextTick(cb) { |
|
setTimeout(cb, 0); |
|
}; |
|
|
|
function isErrorHandlingMiddleware(post, numArgs) { |
|
if (post.errorHandler) { |
|
return true; |
|
} |
|
return post.fn.length === numArgs + 2; |
|
} |
|
|
|
module.exports = Kareem;
|
|
|