mixing constructor and apply traps on the Javascript Proxy object - javascript

I have a class that I'd like to apply a proxy to, observing method calls and constructor calls:
Calculator.js
class Calc {
constructor(){}
add(a, b) {
return a+b;
}
minus(a, b) {
return a-b;
}
}
module.exports = Calc;
index.js
const Calculator = require('./src/Calculator');
const CalculatorLogger = {
construct: function(target, args, newTarget) {
console.log('Object instantiated');
return new target(...args);
},
apply: function(target, thisArg, argumentsList) {
console.log('Method called');
}
}
const LoggedCalculator = new Proxy(Calculator, CalculatorLogger);
const calculator = new LoggedCalculator();
console.log(calculator.add(1,2));
When this is called, I would expect for the output to be:
Object instantiated
Method called
however, the apply is not being called, I assume that this is because I am attaching the Proxy to the Calculator class, but not the instantiated object, and so doesn't know about the apply trap.
How can i build an all encompassing Proxy to "observe" on method calls and constructor calls.

I assume that this is because I am attaching the Proxy to the Calculator class, but not the instantiated object, and so doesn't know about the apply trap.
You are totally right, proxies act upon objects, so it won't call apply unless a function property of the Calculator class is called, as follows:
class Calculator {
constructor() {
this.x = 1;
}
instanceFunction() {
console.log('Instance function called');
}
static staticFun() {
console.log('Static Function called');
}
}
const calcHandler = {
construct(target, args) {
console.log('Calculator constructor called');
return new target(...args);
},
apply: function(target, thisArg, argumentsList) {
console.log('Function called');
return target(...argumentsList);
}
};
Calculator = new Proxy(Calculator, calcHandler);
Calculator.staticFun();
const obj = new Calculator();
obj.instanceFunction();
With that clear, what you could do to wrap an instance of Calculator with a proxy could be:
Have the class proxy to proxify instances on construct:
const CalculatorInstanceHandler = {
apply(target, thisArg, args) {
console.log('Function called');
return target(...args);
}
}
const CalculatorClassHandler = {
construct(target, args) {
const instance = new target(...args);
return new Proxy(instance, CalculatorInstanceHandler);
}
}
Have a factory function in the Calculator class in order to create proxified instances:
const CalculatorInstanceHandler = {
apply(target, thisArg, args) {
return target(...args);
}
};
class Calculator {
static getNewCalculator() {
const instance = new Calculator();
return new Proxy(instance, CalculatorInstanceHandler);
}
}

Instead of using handler.apply() on the class, modify what handler.construct() returns, adding a Proxy to that instead.
class originalClass {
constructor() {
this.c = 1;
}
add(a, b) {
return a + b + this.c;
}
}
const proxiedClass = new Proxy(originalClass, {
construct(target, args) {
console.log("constructor of originalClass called.");
return new Proxy(new target(...args), {
get(target, prop, receiver) {
console.log(prop + " accessed on an instance of originalClass");
const val = target[prop];
if (typeof target[prop] === "function") {
console.log(prop + " was a function");
return function(...args) {
console.log(prop + "() called");
return val.apply(this, args);
};
} else {
return val;
}
}
});
}
});
const proxiedInstance = new proxiedClass();
console.log(proxiedInstance.add(1, 2));
There's 2 proxies in play here:
A proxy to observe constructor calls, and wrap any instances created by that constructor with...
...a proxy to observe property accesses, and log when those properties are functions. It will also wrap any functions, so it can observe calls to that function.

Related

Change arguments of a function using a MethodDecorator, without altering the "this value"?

Imagine you have to change method arguments at runtime, using a decorator. A trivial example to make it simple: all arguments being set to "Hello World":
export const SillyArguments = (): MethodDecorator => {
return (
target: Object,
propertyKey: string | symbol,
descriptor: PropertyDescriptor
) => {
const originalMethod = descriptor.value;
descriptor.value = (...args: any[]) => {
Object.keys(args).forEach(i => {
args[i] = 'Hello World';
});
return originalMethod.apply(null, args);
};
return descriptor;
}
};
Example usage:
class TestClass {
private qux = 'qux';
#SillyArguments()
foo(val: any) {
console.log(val);
console.log(this.qux);
this.bar();
}
bar() {
console.log('bar');
}
}
const test = new TestClass();
test.foo('Ciao mondo'); // prints "Hello World"
TypeError: Cannot read property 'qux' of null
The problem here is apply(null, args), which changes the context of this. This makes impossible to call the instance variable named qux, from inside foo().
Another possibility is to change the call to originalMethod.apply(target, args), but this time qux is undefined, while bar() can be invoked.
Is there any possibility to invoke the originalMethod with the context of this correctly set to the instance?
Use a function function instead of an arrow function so that you receive the original this context and can pass it along:
export const SillyArguments = (): MethodDecorator => {
return (
target: Object,
propertyKey: string | symbol,
descriptor: PropertyDescriptor
) => {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
Object.keys(args).forEach(i => {
args[i] = 'Hello World';
});
return originalMethod.apply(this, args);
};
return descriptor;
}
};

reflection appears to be losing this

I'm trying to build an AOP logger for my classes... I'm having an issue where when i reflect back to the targeted function, the function loses access to this
so my AOP kinda looks like this
AOP.js
class AOP {
constructor() {
}
static ClassHandler(obj) {
const InstanceHandler = {
get(target, prop, receiver) {
console.log(target.constructor.name);
const origMethod = target[prop];
return function (...args) {
// let result = Reflect.apply(origMethod, this, args)
let result = Reflect.get(target, prop, receiver)
result = Reflect.apply(result, this, args);
console.log(prop + JSON.stringify(args)
+ ' -> ' + JSON.stringify(result));
return result;
};
},
apply(target, thisArg, argumentsList) {
console.log('actually applied');
}
}
const handler = {
construct(target, args) {
console.log(`${target.name} instantiated`);
console.log(args);
const instance = Reflect.construct(...arguments);
return new Proxy(instance, InstanceHandler);
}
}
return new Proxy(obj, handler);
}
}
module.exports = AOP;
A singleton
OtherClass.js
class OtherClass {
constructor() {
this._blah = 'this is a shoutout';
}
shoutOut() {
console.log(this._blah);
}
}
module.exports = new OtherClass();
and a class which requires the singleton
CalculatorDI.js
class Calculator {
constructor(otherClass) {
this.otherClass = otherClass;
}
add(a, b) {
this.otherClass.shoutOut();
return a+b;
}
minus(a, b) {
return a-b;
}
}
module.exports = Calculator;
bringing it all together like this:
const AOP = require('./src/aspects/AOP');
const Calculator = AOP.ClassHandler(require('./src/CalculatorDI'));
const otherClass = require('./src/OtherClass');
const calculator = new Calculator(otherClass);
calculator.add(1,1);
When running this, I get the error:
TypeError: this.otherClass.shoutOut is not a function
Your problem is that your proxy always returns a function, for any property that is accessed, including this.otherClass. You will need to use
const instanceHandler = {
get(target, prop, receiver) {
console.log(target.constructor.name);
const orig = Reflect.get(target, prop, receiver);
if (typeof orig == "function") {
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
return function (...args) {
const result = orig.apply(this, args);
console.log(prop + JSON.stringify(args) + ' -> ' + JSON.stringify(result));
return result;
};
} else {
return orig;
}
}
};
Also notice that you don't need an apply trap in the instanceHandler, as none of your instances is a function.

js apply decorator programmatically

I have a class decorator and would like to use it to apply another decorator to all methods within the class, but I am not quite sure how to apply a decorator programmatically without the # syntax:
#LogAllMethods
class User {
greet() {
console.log('Hello')
}
}
function LogAllMethods(target) {
for (const key in Object.keys(target.prototype)) {
// apply LogMethod decorator
}
}
function LogMethod(target, key, descriptor) {
let method = descriptor.value
decriptor.value = function(...args) {
console.log(args)
method.apply(this, ...args)
}
}
You basically just have to call the decorator function with the target, the key (method name) and the defined descriptor:
function LogAllMethods<T>(target: new (...params: any[]) => T) {
for (const key of Object.getOwnPropertyNames(target.prototype)) {
let descriptor = Object.getOwnPropertyDescriptor(target.prototype, key);
descriptor = LogMethod(target.prototype, key, descriptor);
if (descriptor) {
Object.defineProperty(target.prototype, key, descriptor);
}
}
}
function LogMethod(target: any, key: symbol | string, descriptor: TypedPropertyDescriptor<any> = undefined) {
if (descriptor) {
let method = descriptor.value;
if (method instanceof Function) {
descriptor.value = function (...args: any[]) {
console.log("Log", args)
method.apply(this, ...args);
}
}
return descriptor;
} else {
// descriptor is null for target es5 if the decorator is applied directly to the merhod
let method = target[key];
if (method instanceof Function) {
target[key] = function (...args: any[]) {
console.log("Log", args)
method.apply(this, ...args);
}
}
}
}

Fluent async api with ES6 proxy javascript

So... I have some methods. Each method returns a promise.
myAsyncMethods: {
myNavigate () {
// Imagine this is returning a webdriverio promise
return new Promise(function(resolve){
setTimeout(resolve, 1000);
})
},
myClick () {
// Imagine this is returning a webdriverio promise
return new Promise(function(resolve){
setTimeout(resolve, 2000);
})
}
}
I'm trying to make end to end tests, so the prom chain must be linear (first click, next navigate, etc)
For now, I can do this...
makeItFluent(myAsyncMethods)
.myNavigate()
.myClick()
.then(() => myAsyncMethods.otherMethod())
.then(() => /*do other stuff*/ )
...with ES6 proxy feature:
function makeItFluent (actions) {
let prom = Promise.resolve();
const builder = new Proxy(actions, {
get (target, propKey) {
const origMethod = target[propKey];
return function continueBuilding (...args) {
// keep chaining promises
prom = prom.then(() => (typeof origMethod === 'function') && origMethod(...args));
// return an augmented promise with proxied object
return Object.assign(prom, builder);
};
}
});
return builder;
};
But, the thing I cannot do is the following:
makeItFluent(myAsyncMethods)
.myNavigate()
.myClick()
.then(() => myAsyncMethods.otherMethod())
.then(() => /*do other stuff*/ )
.myNavigate()
Because then is not a proxied method, and thus it does not return myAsyncMethods. I tried to proxy then but with no results.
Any idea?
thanks devs ;)
I would return wrapped Promises from yourAsyncMethods which allows mixing of sync and async methods with Proxy and Reflect and executing them in the correct order :
/* WRAP PROMISE */
let handlers;
const wrap = function (target) {
if (typeof target === 'object' && target && typeof target.then === 'function') {
// The target needs to be stored internally as a function, so that it can use
// the `apply` and `construct` handlers.
var targetFunc = function () { return target; };
targetFunc._promise_chain_cache = Object.create(null);
return new Proxy(targetFunc, handlers);
}
return target;
};
// original was written in TS > 2.5, you might need a polyfill :
if (typeof Reflect === 'undefined') {
require('harmony-reflect');
}
handlers = {
get: function (target, property) {
if (property === 'inspect') {
return function () { return '[chainable Promise]'; };
}
if (property === '_raw') {
return target();
}
if (typeof property === 'symbol') {
return target()[property];
}
// If the Promise itself has the property ('then', 'catch', etc.), return the
// property itself, bound to the target.
// However, wrap the result of calling this function.
// This allows wrappedPromise.then(something) to also be wrapped.
if (property in target()) {
const isFn = typeof target()[property] === 'function';
if (property !== 'constructor' && !property.startsWith('_') && isFn) {
return function () {
return wrap(target()[property].apply(target(), arguments));
};
}
return target()[property];
}
// If the property has a value in the cache, use that value.
if (Object.prototype.hasOwnProperty.call(target._promise_chain_cache, property)) {
return target._promise_chain_cache[property];
}
// If the Promise library allows synchronous inspection (bluebird, etc.),
// ensure that properties of resolved
// Promises are also resolved immediately.
const isValueFn = typeof target().value === 'function';
if (target().isFulfilled && target().isFulfilled() && isValueFn) {
return wrap(target().constructor.resolve(target().value()[property]));
}
// Otherwise, return a promise for that property.
// Store it in the cache so that subsequent references to that property
// will return the same promise.
target._promise_chain_cache[property] = wrap(target().then(function (result) {
if (result && (typeof result === 'object' || typeof result === 'function')) {
return wrap(result[property]);
}
const _p = `"${property}" of "${result}".`;
throw new TypeError(`Promise chain rejection: Cannot read property ${_p}`);
}));
return target._promise_chain_cache[property];
},
apply: function (target, thisArg, args) {
// If the wrapped Promise is called, return a Promise that calls the result
return wrap(target().constructor.all([target(), thisArg]).then(function (results) {
if (typeof results[0] === 'function') {
return wrap(Reflect.apply(results[0], results[1], args));
}
throw new TypeError(`Promise chain rejection: Attempted to call ${results[0]}` +
' which is not a function.');
}));
},
construct: function (target, args) {
return wrap(target().then(function (result) {
return wrap(Reflect.construct(result, args));
}));
}
};
// Make sure all other references to the proxied object refer to the promise itself,
// not the function wrapping it
Object.getOwnPropertyNames(Reflect).forEach(function (handler) {
handlers[handler] = handlers[handler] || function (target, arg1, arg2, arg3) {
return Reflect[handler](target(), arg1, arg2, arg3);
};
});
You would use it with your methods like
myAsyncMethods: {
myNavigate () {
// Imagine this is returning a webdriverio promise
var myPromise = new Promise(function(resolve){
setTimeout(resolve, 1000);
});
return wrap(myPromise)
},
// ...
Please note two things :
You might need a polyfill for Reflect : https://www.npmjs.com/package/harmony-reflect
We need to check proxy get handlers for built-in Symbols, e.g. : https://github.com/nodejs/node/issues/10731 (but also some browsers)
You can now mix it like
FOO.myNavigate().mySyncPropertyOrGetter.myClick().mySyncMethod().myNavigate() ...
https://michaelzanggl.com/articles/end-of-chain/
A promise is nothing more than a "thenable" (an object with a then() method), which conforms to the specs. And await is simply a wrapper around promises to provide cleaner, concise syntax.
class NiceClass {
promises = [];
doOne = () => {
this.promises.push(new Promise((resolve, reject) => {
this.one = 1;
resolve();
}));
return this;
}
doTwo = () => {
this.promises.push(new Promise((resolve, reject) => {
this.two = 2;
resolve();
}));
return this;
}
async then(resolve, reject) {
let results = await Promise.all(this.promises);
resolve(results);
}
build = () => {
return Promise.all(this.promises)
}
}
Them you can call it in both ways.
(async () => {
try {
let nice = new NiceClass();
let result = await nice
.doOne()
.doTwo();
console.log(nice);
let nice2 = new NiceClass();
let result2 = await nice2
.doOne()
.doTwo()
.build();
console.log(nice2, result2);
} catch(error) {
console.log('Promise error', error);
}
})();

Chaining methods with javascript

I'm trying to create chaining with the javascript methods similar to what we have with jquery. Please let me know how to implement chaining with javascript.
var controller = {
currentUser: '',
fnFormatUserName: function(user) {
this.currentUser = user;
return this.currentUser.toUpperCase();
},
fnCreateUserId: function() {
return this.currentUser + Math.random();
}
}
var output = controller.fnFormatUserName('Manju').fnCreateUserId();
As I already explained, since you are returning a string from fnFormatUserName you cannot use it for chaining.
To enable chaining, you need to return the object which invoked method. So, you cannot use getter methods for chaining.
In your example, the way to handle it is to have getter methods and methods with updates the object which can be used for chaining like
var controller = {
currentUser: '',
fnFormatUserName: function(user) {
this.currentUser = user.toUpperCase();
return this;
},
fnCreateUserId: function() {
this.userId = this.currentUser + Math.random();
return this;
},
getUserId: function() {
return this.userId;
}
}
var output = controller.fnFormatUserName('Manju').fnCreateUserId().getUserId();
document.body.innerHTML = output;
Another version could be
var controller = {
currentUser: '',
fnFormatUserName: function(user) {
if (arguments.length == 0) {
return this.currentUser;
} else {
this.currentUser = user.toUpperCase();
return this;
}
},
fnCreateUserId: function() {
this.userId = this.currentUser + Math.random();
return this;
},
getUserId: function() {
return this.userId;
}
}
var output = controller.fnFormatUserName('Manju').fnCreateUserId().getUserId();
r1.innerHTML = output;
r2.innerHTML = controller.fnFormatUserName();
<div id="r1"></div>
<div id="r2"></div>
You can use proxies to decorate methods so that they will return the object itself ("this") instead of the actual method return value. Below is an implementation of a chainer function that will do just that for any object. The code also declares a special symbol "target" which can be used to access the original object (and unaltered method return values), discarding the chaining proxy.
const target = Symbol('Symbol for the target of the chainer proxy');
const targetSymbol = target;
const chainer = (target) =>
new Proxy(target, {
get: (_, prop, receiver) =>
prop === targetSymbol
? target
: typeof target[prop] === 'function'
? new Proxy(target[prop], {
apply: (f, _, args) => {
f.apply(target, args);
return receiver;
},
})
: target[prop],
});
const controller = {
currentUser: '',
fnFormatUserName: function(user) {
return this.currentUser = user.toUpperCase();
},
fnCreateUserId: function() {
return this.currentUser + Math.random();
}
}
const output = chainer(controller).fnFormatUserName('Manju')[target].fnCreateUserId();
console.log(output);
Another option would be that the decorated methods would always return an intermediate object with two properties: the this context ("this") and a reference to the original object with undecorated methods ("target"). See below.
const chainer = (target) =>
new Proxy(target, {
get: (_, prop, receiver) =>
typeof target[prop] === 'function'
? new Proxy(target[prop], {
apply: (f, _, args) => {
f.apply(target, args);
return {
this: receiver,
target,
}
},
})
: target[prop],
});
const counter = {
value: 0,
increment: function() {
return ++this.value;
}
}
const value = chainer(counter)
.increment().this
.increment().target
.increment();
console.log(value);
I suppose this might be seen as "cheating", but you could very easily achieve a similar result by extending the String.prototype like:
String.prototype.upper=function(){return this.toUpperCase()};
String.prototype.makeId=function(){return this+Math.random()};
// test
const str="abc";
console.log(str.upper().makeId(), str);
It will, of course, change the behaviour of all strings in the current session as they will now have the additional methods .upper() and .makeId() associated with them.

Categories

Resources