I want to write a class, that deals with undefined properties. I also want to return this to be able to chain methods to create a domain specific language (DSL).
I return a Proxy from the constructor, to handle undefined properties. Now when testing the instance, it does happen, that return this does not prove to be identical with the instance. I fear bugs resulting from this, although I can chain the methods as intended.
This is a mocha chai test to show the behaviour. Replace o.that().should.not.equal(o); with o.that().should.equal(o); in the last instruction to see how it fails.
require('chai').should();
describe('chai testing classes using Proxy', () => {
it('asserts object identity, if an instance method returns this', () => {
const o = new class NormalClass{ }
o.that = function() { return this; }
o.that().should.equal(o);
});
it('observes the same behaviour for constructors returning a dummy Proxy', () => {
const o = new class ProxyClass{
constructor() { return new Proxy(this, {}); }
}
o.that = function() { return this; }
o.that().should.equal(o);
});
it('requires deep.equal on the other hand, if the Proxy handles get', () => {
const o = new class ProxyClassPlusGet{
constructor() {
return new Proxy(this, {
get: function(target, prop) { return target[prop]; },
});
}
}
o.that = function() { return this; }
o.that().should.deep.equal(o);
o.that().should.not.equal(o);
});
});
Your implementation works insofar as o.that() === o yields true.
But it does not work with getters, which interferes with chai's should. You can reproduce this with
const o = new Proxy({
get self() { return this; },
that() { return this; },
}, {
get(target, prop) { return target[prop]; },
});
console.log(o.self === o);
console.log(o.that() === o);
Why is that? Because your get trap is broken, ignoring the receiver of the property access. It will hold the proxy, o, but when you do return target[prop] then target will be the receiver. You can fix it by using Reflect.get:
const o = new Proxy({
get self() { return this; },
that() { return this; },
}, {
get(target, prop, receiver) {
return Reflect.get(target, prop, receiver);
// ^^^^^^^^
},
});
console.log(o.self === o);
console.log(o.that() === o);
In addition to the concise answer given by #Bergi I add another test. It proves, that his solution shows a sound behaviour:
It returns the identity of the proxy object in all cases.
It stores the data into the underling original object.
It doesn't create a vicious circle with the proxy as receiver.
It works consistently with chai expect and chai should.
I still don't fully understand the proxying stuff. Doing this tests, already gives a deep insight. Kudos to #Bergi for the right solution.
require('chai').should();
const expect = require('chai').expect;
describe('general Proxy behaviour', () => {
describe('Proxy with empty handler', () => {
it('shows that original and proxy are two different objects' , () => {
const original = {};
const proxy = new Proxy(original, {});
expect(proxy).not.equal(original);
});
it('evaluates proxy and origianl to be deep equal' , () => {
const original = {};
const proxy = new Proxy(original, {});
expect(proxy).to.deep.equal(original);
});
it('sets and gets down to the original', () => {
const original = {};
const proxy = new Proxy(original, {});
proxy.x = 1;
expect(original.x).to.equal(1);
original.x = 2;
expect(proxy.x).to.equal(2);
});
describe('consistent when accessing this', () => {
const original = {
getThat() { return this; },
get that() { return this; },
};
const proxy = new Proxy(original, { });
it('evaluates getters to the proxy', () => {
expect(proxy.that).to.be.equal(proxy)
});
it('evaluates methods to the proxy, too', () => {
expect(proxy.getThat()).to.be.equal(proxy)
});
it('chai should works like chai expect', () => {
proxy.that.should.equal(proxy)
proxy.getThat().should.equal(proxy)
});
});
});
describe('Proxy without reflection', () => {
it('sets and gets down to the original like the empty handler', () => {
const original = {};
const proxy = new Proxy(original, {
set: function(target, prop, value) { target[prop]=value; },
get: function(target, prop) { return target[prop]; },
});
proxy.x = 1;
expect(original.x).to.equal(1);
original.x = 2;
expect(proxy.x).to.equal(2);
});
it('detects original as the target in get and set',
() => {
const original = {};
let targetInSet;
let targetInGet;
const proxy = new Proxy(original, {
set: function(target, prop, value, receiver) {
targetInSet = target;
target[prop]=value;
},
get: function(target, prop, receiver) {
targetInGet = target;
expect(target).to.equal(original);
return target[prop];
},
});
proxy.x = 1;
proxy.x;
expect(targetInSet).to.equal(original);
expect(targetInGet).to.equal(original);
});
it('detects proxy as the receiver in get and set',
() => {
const original = {};
let receiverInSet;
let receiverInGet;
const proxy = new Proxy(original, {
set: function(target, prop, value, receiver) {
receiverInSet = receiver;
target[prop]=value;
},
get: function(target, prop, receiver) {
receiverInGet = receiver;
return target[prop];
},
});
proxy.x = 1;
proxy.x;
expect(receiverInSet).to.equal(proxy);
expect(receiverInGet).to.equal(proxy);
});
describe('chaos when accessing this', () => {
const original = {
getThat() { return this; },
get that() { return this; },
};
const proxy = new Proxy(original, {
get: function(target, prop, receiver) {
receiverInGet = receiver;
return target[prop];
},
});
it('evaluates getters to the original', () => {
expect(proxy.that).to.be.equal(original)
});
it('evaluates methods to the proxy', () => {
expect(proxy.getThat()).to.be.equal(proxy)
});
it('chai should differs from chai expect', () => {
expect(proxy.getThat()).to.be.equal(proxy)
proxy.getThat().should.equal(original)
});
it('chai should evaluates to original in both cases', () => {
proxy.getThat().should.equal(original)
proxy.that.should.equal(original)
});
});
});
describe('Reflect in Proxy without the receiver being set', () => {
it('sets and gets down to the original like the empty handler', () => {
const original = {};
const proxy = new Proxy(original, {
get: function(target, prop) {
return Reflect.get(target, prop);
},
set: function(target, prop, value) {
Reflect.set(target, prop, value);
},
});
proxy.x = 1;
expect(original.x).to.equal(1);
original.x = 2;
expect(proxy.x).to.equal(2);
});
describe('chaos when accessing this', () => {
const original = {
getThat() { return this; },
get that() { return this; },
};
const proxy = new Proxy(original, {
get: function(target, prop, receiver) {
return Reflect.get(target, prop);
},
});
it('evaluates getters to the original', () => {
expect(proxy.that).to.be.equal(original)
});
it('evaluates methods to the proxy', () => {
expect(proxy.getThat()).to.be.equal(proxy)
});
it('chai should differs from chai expect', () => {
expect(proxy.getThat()).to.be.equal(proxy)
proxy.getThat().should.equal(original)
});
it('chai should evaluates to original in both cases', () => {
proxy.getThat().should.equal(original)
proxy.that.should.equal(original)
});
});
});
describe('Reflect in Proxy with the receiver being set to original', () => {
it('sets and gets down to the original like the empty handler', () => {
const original = {};
const proxy = new Proxy(original, {
get: function(target, prop, receiver) {
return Reflect.get(target, prop, target);
},
set: function(target, prop, value, receiver) {
Reflect.set(target, prop, value, target);
},
});
proxy.x = 1;
expect(original.x).to.equal(1);
original.x = 2;
expect(proxy.x).to.equal(2);
});
describe('chaos when accessing this', () => {
const original = {
getThat() { return this; },
get that() { return this; },
};
const proxy = new Proxy(original, {
get: function(target, prop, receiver) {
return Reflect.get(target, prop, target);
},
});
it('evaluates getters to the original', () => {
expect(proxy.that).to.be.equal(original)
});
it('evaluates methods to the proxy', () => {
expect(proxy.getThat()).to.be.equal(proxy)
});
it('chai should differs from chai expect', () => {
expect(proxy.getThat()).to.be.equal(proxy)
proxy.getThat().should.equal(original)
});
it('chai should evaluates to original in both cases', () => {
proxy.getThat().should.equal(original)
proxy.that.should.equal(original)
});
});
});
describe('Reflect in Proxy with the receiver being set to proxy', () => {
it('sets and gets down to the original like the empty handler', () => {
const original = {};
const proxy = new Proxy(original, {
get: function(target, prop, receiver) {
return Reflect.get(target, prop, receiver);
},
set: function(target, prop, value, receiver) {
Reflect.set(target, prop, value, receiver);
},
});
proxy.x = 1;
expect(original.x).to.equal(1);
original.x = 2;
expect(proxy.x).to.equal(2);
});
it('does not cause a vicious circle in the proxy', () => {
const original = {};
const proxy = new Proxy(original, {
get: function(target, prop, receiver) {
return Reflect.get(target, prop, receiver);
},
set: function(target, prop, value, receiver) {
Reflect.set(target, prop, value, receiver);
},
});
proxy.x = 1;
expect(proxy.x).to.equal(1);
});
describe('consistent when accessing this', () => {
const original = {
getThat() { return this; },
get that() { return this; },
};
const proxy = new Proxy(original, {
get: function(target, prop, receiver) {
return Reflect.get(target, prop, receiver);
},
});
it('evaluates getters to the proxy', () => {
expect(proxy.that).to.be.equal(proxy)
});
it('evaluates methods to the proxy, too', () => {
expect(proxy.getThat()).to.be.equal(proxy)
});
it('chai should works like chai expect', () => {
proxy.that.should.equal(proxy)
proxy.getThat().should.equal(proxy)
});
});
});
});
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.
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.
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);
}
})();
I have a factory object that contain private object, which is used to cache result retrieved from the api using the factory available functions.
global.mainApp.factory('SessionFactory', function (UserEndpointsResource, SiteEndpointsResource) {
var data = {
/**
* #type {boolean|null}
*/
isLoggedIn: null
};
return {
isUserLoggedIn: function (callback) {
if (data.isLoggedIn != null) {
callback(data.isLoggedIn);
}
else {
UserEndpointsResource.isLoggedIn().$promise.then(function (res) {
var isUserLoggedIn = res.status == 1;
// Trying to set the result to the outer scope data variable
data.isLoggedIn = isUserLoggedIn;
callback(isUserLoggedIn);
}, function (failureData) {
data.isLoggedIn = false;
callback(false);
});
}
},
...
};
});
The problem that every time I call the isUserLoggedIn function, data.isLoggedIn is always null.
How can I alter the factory data object inside the promise then function?
Thanks.
Using the suggestion supplied in the comments, aka do not store promise results, store promises themselves, this is the modified working code!
global.mainApp.factory('SessionFactory', function (UserEndpointsResource, SiteEndpointsResource) {
var data = {
/**
* #type {boolean|null}
*/
isLoggedIn: null
};
return {
isUserLoggedIn: function (callback) {
if (data.isLoggedIn != null) {
data.isLoggedIn.then(function (isLoggedIn) {
callback(isLoggedIn.status == 1);
});
}
else {
data.isLoggedIn = UserEndpointsResource.isLoggedIn().$promise.then(function (res) {
var isUserLoggedIn = res.status == 1;
callback(isUserLoggedIn);
return isUserLoggedIn;
}, function (failureData) {
data.isLoggedIn = false;
callback(false);
return null;
});
}
}
};
});