@contrast/patcher
Advanced tools
Comparing version 1.9.0 to 1.10.0
242
lib/index.js
@@ -19,2 +19,3 @@ /* | ||
const { threadId } = require('worker_threads'); | ||
const { hrtime } = require('process'); | ||
@@ -32,2 +33,3 @@ /** | ||
function isFunction(fn) { | ||
// why do both need to be checked? | ||
return fn instanceof Function || typeof fn === 'function'; | ||
@@ -56,2 +58,4 @@ } | ||
const promisifyCustom = Symbol.for('nodejs.util.promisify.custom'); | ||
const perf = new core.Perf('patcher'); | ||
const perfIsEnabled = core.Perf.isEnabled; | ||
@@ -85,45 +89,2 @@ /** | ||
/** | ||
* Run the given hooks with the supplied arguments and `this` target | ||
* | ||
* @param {any} data arguments for the hooks execution | ||
* @param {Object} thisTarget this target for the `apply` call of the hooks | ||
* @param {Array} fnHooksArr array of hooks to execute | ||
*/ | ||
function runHooks(data, thisTarget, fnHooksArr) { | ||
fnHooksArr.forEach((hook) => { | ||
hook.apply(thisTarget, [data]); | ||
}); | ||
} | ||
/** | ||
* Get the result of a function call | ||
* | ||
* @this {Function} the hooked function | ||
* @param {Function} fn the original function | ||
* @param {any} target the constructor being called (if called with new) | ||
* @param {Object} fnHooks an object containing maps of the hooks set for this function | ||
* @param {Object} data | ||
*/ | ||
function getResult(fn, data, target, fnHooks) { | ||
const self = this; | ||
const arounds = Array.from(fnHooks.around.values()); | ||
if (arounds.length) { | ||
const pipe = arounds.reverse().reduce( | ||
(innerNext, around) => | ||
function next() { | ||
return around(innerNext, data); | ||
}, | ||
function next() { | ||
return runOriginalFunction.call(self, fn, data, target); | ||
} | ||
); | ||
return pipe(); | ||
} | ||
return runOriginalFunction.call(this, fn, data, target); | ||
} | ||
/** | ||
* Invoke the original function in the current context with the | ||
@@ -166,33 +127,6 @@ * passed-in arguments and store its result | ||
function hooked(...args) { | ||
const target = new.target; | ||
// get the hooked function. it will be either the hooked function with perf | ||
// or without. | ||
const hooked = makeHookedFunction(fn, options); | ||
const data = { | ||
hooked, | ||
orig: fn, | ||
funcKey: options.funcKey, | ||
obj: new.target || this, | ||
name: options.name, | ||
args, | ||
result: undefined, | ||
}; | ||
const fnHooks = hooks.get(hooked); | ||
// Run pre hooks if there are some | ||
if (fnHooks && fnHooks.pre.size) { | ||
runHooks(data, this, Array.from(fnHooks.pre.values())); | ||
} | ||
// Run original function | ||
data.result = getResult.call(this, fn, data, target, fnHooks); | ||
// Run post hooks if there are some | ||
if (fnHooks && fnHooks.post.size) { | ||
runHooks(data, this, Array.from(fnHooks.post.values())); | ||
} | ||
return data.result; | ||
} | ||
/** | ||
@@ -305,2 +239,161 @@ * copy over all properties if there are properties on the original function. | ||
/** | ||
* factory to make a hooked function either with or without perf data. the | ||
* arguments are exactly the same as `hookFunction`. it exists to create a | ||
* closure around the `fn` and `options` arguments and to isolate the | ||
* logic differences in the hooked function to a single place. | ||
* | ||
* @param {Function} fn original function being hooked | ||
* @param {Object} options | ||
* @param {string} options.name - name of the function (usually signature) | ||
* @param {boolean} options.usePerf - collect perf data for this hooked function | ||
* @returns {Function} the hooked function | ||
* | ||
*/ | ||
function makeHookedFunction(fn, options) { | ||
// | ||
// the hooked function that does not capture perf data. this is very close | ||
// to the original hooked function. the primary difference is that it does | ||
// the pre/post hooks in a loop rather than calling a function. | ||
// | ||
function hooked(...args) { | ||
const target = new.target; | ||
const data = { | ||
hooked, | ||
orig: fn, | ||
funcKey: options.funcKey, | ||
obj: new.target || this, | ||
name: options.name, | ||
args, | ||
result: undefined, | ||
}; | ||
const fnHooks = hooks.get(hooked); | ||
if (!fnHooks) { | ||
return (data.result = runOriginalFunction.call(this, fn, data, target)); | ||
} | ||
// Run pre hooks | ||
if (fnHooks.pre.size) { | ||
for (const pre of fnHooks.pre.values()) { | ||
pre.call(this, data); | ||
} | ||
} | ||
if (fnHooks.around.size === 0) { | ||
data.result = runOriginalFunction.call(this, fn, data, target); | ||
} else { | ||
// chain around hooks, from last to first to original, via recursion | ||
const self = this; | ||
const arounds = Array.from(fnHooks.around.values()).reverse(); | ||
const pipe = arounds.reduce( | ||
(innerNext, around) => | ||
function next() { | ||
return around(innerNext, data); | ||
}, | ||
function next() { | ||
return runOriginalFunction.call(self, fn, data, target); | ||
} | ||
); | ||
data.result = pipe(); | ||
} | ||
// Run post hooks | ||
if (fnHooks.post.size) { | ||
for (const post of fnHooks.post.values()) { | ||
post.call(this, data); | ||
} | ||
} | ||
return data.result; | ||
} | ||
// | ||
// a functional duplicate of hooked, but has perf measurement of the hooked | ||
// and original functions. | ||
// | ||
function perfHooked(...args) { | ||
// do perf work before getting the start time | ||
const outerTag = `patcher${options.name}:wrapper`; | ||
const innerTag = `${options.name}:native`; // name is patcher:original-function-name | ||
const start = hrtime.bigint(); | ||
const target = new.target; | ||
const data = { | ||
hooked: perfHooked, | ||
orig: fn, | ||
funcKey: options.funcKey, | ||
obj: new.target || this, | ||
name: options.name, //String.prototype.substring | ||
args, | ||
result: undefined, | ||
}; | ||
// create functions to run the original function. this replaces the original | ||
// `runOriginalFunction()` with one that doesn't require passing arguments. my | ||
// thinking is that minimizes wrapping overhead. | ||
let runOriginalFunction; | ||
if (target) { | ||
runOriginalFunction = () => Reflect.construct(fn, args, options.name !== 'Object' ? target : unwrap(target)); | ||
} else { | ||
runOriginalFunction = () => fn.apply(this, args); | ||
} | ||
runOriginalFunction = perf.wrapSync(runOriginalFunction, innerTag); | ||
// if the function isn't hooked, just run the original function and records the | ||
// time with a modified tag indicating that it wasn't hooked. i don't think this | ||
// should happen (why are we in the hooked function if there are no hooks?), but | ||
// it's here just in case. | ||
const fnHooks = hooks.get(perfHooked); | ||
if (!fnHooks) { | ||
const result = (data.result = runOriginalFunction()); | ||
perf.record(`${outerTag}:unhooked`, start); | ||
return result; | ||
} | ||
// Run any pre hooks | ||
if (fnHooks.pre.size) { | ||
for (const pre of fnHooks.pre.values()) { | ||
pre.call(this, data); | ||
} | ||
} | ||
// if there are no around hooks, then no need to do the work to chain them. | ||
if (fnHooks.around.size === 0) { | ||
data.result = runOriginalFunction(); | ||
} else { | ||
// chain around hooks, from last to first to original, via recursion | ||
const arounds = Array.from(fnHooks.around.values()).reverse(); | ||
const pipe = arounds.reduce( | ||
(innerNext, around) => | ||
function next() { | ||
return around(innerNext, data); | ||
}, | ||
function next() { | ||
return runOriginalFunction(); | ||
} | ||
); | ||
// execute the chained hooks then the original function | ||
data.result = pipe(); | ||
} | ||
// Run any post hooks | ||
if (fnHooks.post.size) { | ||
for (const post of fnHooks.post.values()) { | ||
post.call(this, data); | ||
} | ||
} | ||
perf.record(outerTag, start); | ||
return data.result; | ||
} | ||
return (perfIsEnabled && options.usePerf) ? perfHooked : hooked; | ||
} | ||
/** | ||
* Patch a given function or object's property with 'pre', 'post', | ||
@@ -349,2 +442,5 @@ * and/or 'around' hooks | ||
// hmmm. if we wrap the fn then hooks.get(fn) won't work if it's patched again. | ||
// should we hook the wrapped function? | ||
// if no property, hook a function directly. | ||
@@ -351,0 +447,0 @@ const fn = prop ? hook(obj, prop, options) : hookFunction(obj, options); |
@@ -7,2 +7,3 @@ 'use strict'; | ||
const mocks = require('@contrast/test/mocks'); | ||
const Perf = require('@contrast/perf'); | ||
@@ -32,2 +33,3 @@ const sym = Symbol('foo'); | ||
core.logger = mocks.logger(); | ||
core.Perf = Perf; | ||
patcher = require('.')(core); | ||
@@ -34,0 +36,0 @@ }); |
{ | ||
"name": "@contrast/patcher", | ||
"version": "1.9.0", | ||
"version": "1.10.0", | ||
"description": "Advanced monkey patching--registers hooks to run in and around functions", | ||
@@ -20,4 +20,4 @@ "license": "SEE LICENSE IN LICENSE", | ||
"dependencies": { | ||
"@contrast/logger": "1.10.0" | ||
"@contrast/logger": "1.11.0" | ||
} | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
47455
1332
+ Added@contrast/common@1.26.0(transitive)
+ Added@contrast/config@1.34.0(transitive)
+ Added@contrast/logger@1.11.0(transitive)
- Removed@contrast/common@1.25.0(transitive)
- Removed@contrast/config@1.33.0(transitive)
- Removed@contrast/logger@1.10.0(transitive)
Updated@contrast/logger@1.11.0