Created
December 28, 2018 14:24
-
-
Save mrharel/592df0228cebc017ca413f2f763acc5f to your computer and use it in GitHub Desktop.
Using Proxy to Track Javascript Class
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const callerMap = {}; | |
function getCaller(error) { | |
if (error && error.stack) { | |
const lines = error.stack.split('\n'); | |
if (lines.length > 2) { | |
let match = lines[2].match(/at ([a-zA-Z\-_$.]+) (.*)/); | |
if (match) { | |
return { | |
name: match[1].replace(/^Proxy\./, ''), | |
file: match[2], | |
}; | |
} else { | |
match = lines[2].match(/at (.*)/); | |
if (match) { | |
return { | |
name: 'unknown', | |
file: match[1], | |
}; | |
} | |
} | |
} | |
} | |
return { | |
name: 'unknown', | |
file: '', | |
}; | |
} | |
function getFunctionName(fn, context) { | |
let contextName = ''; | |
if (typeof context === 'function') { | |
contextName = `{context.name}.`; | |
} else if (context && context.constructor && context.constructor.name !== 'Object') { | |
contextName = `${context.constructor.name}.`; | |
} | |
return `${contextName}${fn.name}`; | |
} | |
function trackFunctionCall(options = {}) { | |
return function(target, thisArg, argumentsList) { | |
const { trackTime, trackCaller, trackCount, stdout, filter } = options; | |
const error = trackCaller && new Error(); | |
const caller = getCaller(error); | |
const name = getFunctionName(target, thisArg); | |
if (trackCount) { | |
if (!callerMap[name]) { | |
callerMap[name] = 1; | |
} else { | |
callerMap[name]++; | |
} | |
} | |
let start, end; | |
if (trackTime) { | |
start = Date.now(); | |
} | |
const retVal = target.apply(thisArg, argumentsList); | |
if (trackTime) { | |
end = Date.now(); | |
} | |
let output = `${name} was called`; | |
if (trackCaller) { | |
output += ` by ${caller.name}`; | |
} | |
if (trackCount) { | |
output += ` for the ${callerMap[name]} time`; | |
} | |
if (trackTime) { | |
output += ` and took ${end-start} mils.`; | |
} | |
let canReport = true; | |
if (filter) { | |
canReport = filter({ | |
type: 'function', | |
name, | |
caller, | |
count: callerMap[name], | |
time: end - start, | |
}); | |
} | |
if (canReport) { | |
if (stdout) { | |
stdout(output); | |
} else { | |
console.log(output); | |
} | |
} | |
return retVal; | |
}; | |
} | |
function trackPropertySet(options = {}) { | |
return function set(target, prop, value, receiver) { | |
const { trackCaller, trackCount, stdout, filter } = options; | |
const error = trackCaller && new Error(); | |
const caller = getCaller(error); | |
const contextName = target.constructor.name === 'Object' ? '' : `${target.constructor.name}.`; | |
const name = `${contextName}${prop}`; | |
const hashKey = `set_${name}`; | |
if (trackCount) { | |
if (!callerMap[hashKey]) { | |
callerMap[hashKey] = 1; | |
} else { | |
callerMap[hashKey]++; | |
} | |
} | |
let output = `${name} is being set`; | |
if (trackCaller) { | |
output += ` by ${caller.name}`; | |
} | |
if (trackCount) { | |
output += ` for the ${callerMap[hashKey]} time`; | |
} | |
let canReport = true; | |
if (filter) { | |
canReport = filter({ | |
type: 'get', | |
prop, | |
name, | |
caller, | |
count: callerMap[hashKey], | |
value, | |
}); | |
} | |
if (canReport) { | |
if (stdout) { | |
stdout(output); | |
} else { | |
console.log(output); | |
} | |
} | |
return Reflect.set(target, prop, value, receiver); | |
}; | |
} | |
function trackPropertyGet(options = {}) { | |
return function get(target, prop, receiver) { | |
const { trackCaller, trackCount, stdout, filter } = options; | |
if (typeof target[prop] === 'function' || prop === 'prototype') { | |
return target[prop]; | |
} | |
const error = trackCaller && new Error(); | |
const caller = getCaller(error); | |
const contextName = target.constructor.name === 'Object' ? '' : `${target.constructor.name}.`; | |
const name = `${contextName}${prop}`; | |
const hashKey = `get_${name}`; | |
if (trackCount) { | |
if (!callerMap[hashKey]) { | |
callerMap[hashKey] = 1; | |
} else { | |
callerMap[hashKey]++; | |
} | |
} | |
let output = `${name} is being get`; | |
if (trackCaller) { | |
output += ` by ${caller.name}`; | |
} | |
if (trackCount) { | |
output += ` for the ${callerMap[hashKey]} time`; | |
} | |
let canReport = true; | |
if (filter) { | |
canReport = filter({ | |
type: 'get', | |
prop, | |
name, | |
caller, | |
count: callerMap[hashKey], | |
}); | |
} | |
if (canReport) { | |
if (stdout) { | |
stdout(output); | |
} else { | |
console.log(output); | |
} | |
} | |
return target[prop]; | |
}; | |
} | |
function proxyFunctions(trackedEntity, options) { | |
if (typeof trackedEntity === 'function') return; | |
Object.getOwnPropertyNames(trackedEntity).forEach((name) => { | |
if (typeof trackedEntity[name] === 'function') { | |
trackedEntity[name] = new Proxy(trackedEntity[name], { | |
apply: trackFunctionCall(options), | |
}); | |
} | |
}); | |
} | |
function trackObject(obj, options = {}) { | |
const { trackFunctions, trackProps } = options; | |
let resultObj = obj; | |
if (trackFunctions) { | |
proxyFunctions(resultObj, options); | |
} | |
if (trackProps) { | |
resultObj = new Proxy(resultObj, { | |
get: trackPropertyGet(options), | |
set: trackPropertySet(options), | |
}); | |
} | |
return resultObj; | |
} | |
const defaultOptions = { | |
trackFunctions: true, | |
trackProps: true, | |
trackTime: true, | |
trackCaller: true, | |
trackCount: true, | |
filter: null, | |
}; | |
function trackClass(cls, options = {}) { | |
cls.prototype = trackObject(cls.prototype, options); | |
cls.prototype.constructor = cls; | |
return new Proxy(cls, { | |
construct(target, args) { | |
const obj = new target(...args); | |
return new Proxy(obj, { | |
get: trackPropertyGet(options), | |
set: trackPropertySet(options), | |
}); | |
}, | |
apply: trackFunctionCall(options), | |
}); | |
} | |
export function proxyTrack(entity, options = defaultOptions) { | |
if (typeof entity === 'function') return trackClass(entity, options); | |
return trackObject(entity, options); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment