0

I'm working on an API client that allows to call specific API methods when providing the ID of a foo, like so:

apiClient.myApiMethod('myFooId', 'firstApiArg', 'nthApiArg');

For developer convenience, I'm trying to implement custom proxy objects:

var myFoo = apiClient.registerFoo('myFoo', 'myFooId');
myFoo.myApiMethod('firstApiArg', 'nthApiArg');

After searching for a while, I figured ES6 proxies may be best suited for that, as the fooId needs to be inserted as the first argument of the method call to support both ways of working.
Therefore, I created the following code. If an object property of Foo.myFoos is called (eg. Foo.myFoos.example), its is searched for in _myFooItems, and if it exists there, another Proxy object is returned.
Now if a method is called on that object, it is searched for in the properties of Foo, and if found, the Foo method is called with myFooId as its first argument.
That means, you should be able to Foo.myFoos.example.parentMethodX('bar', 'baz').

var Foo = function() {

  // parent instance
  _self = this;

  // custom elements dictionary
  _myFooItems = {};

  // to call parent methods directly on custom elements
  this.myFoos = Object.create(new Proxy({}, {

      // property getter function (proxy target and called property name as params)
      get: function(target, myFooName) {

        // whether called property is a registered foo
        if (_myFooItems.hasOwnProperty(myFooName)) {

          // create another proxy to intercept method calls on previous one
          return Object.create(new Proxy({}, {

              // property getter function (proxy target and called property name as params)
              get: function(target, methodName) {

                // whether parent method exists
                if (_self.hasOwnProperty(methodName)) {

                  return function(/* arguments */) {

                    // insert custom element ID into args array
                    var args = Array.prototype.slice.call(arguments);
                    args.unshift(_myFooItems[ myFooName ]);

                    // apply parent method with modified args array
                    return _self[ methodName ].apply(_self, args);
                  };
                } else {

                  // parent method does not exist
                  return function() {
                    throw new Error('The method ' + methodName + ' is not implemented.');
                  }
                }
              }
            }
          ));
        }
      }
    }
  ));


  // register a custom foo and its ID
  this.registerFoo = function(myFooName, id) {

    // whether the foo has already been registered
    if (_myFooItems.hasOwnProperty(myFooName)) {
      throw new Error('The Foo ' + myFooName + ' is already registered in this instance.');
    }

    // register the foo
    _myFooItems[ myFooName ] = id;

    // return the created foo for further use
    return this.myFoos[ myFooName ];
  };
};

module.exports = Foo;

Though what happens if you run the code and try to register a foo (the above code is working as is in Node>=6.2.0), is that the following Error is thrown:

> var exampleFoo = Foo.registerFoo('exampleFoo', 123456)
Error: The method inspect is not implemented.
  at null.<anonymous> (/path/to/module/nestedProxyTest.js:40:31)
  at formatValue (util.js:297:21)
  at Object.inspect (util.js:147:10)
  at REPLServer.self.writer (repl.js:366:19)
  at finish (repl.js:487:38)
  at REPLServer.defaultEval (repl.js:293:5)
  at bound (domain.js:280:14)
  at REPLServer.runBound [as eval] (domain.js:293:12)
  at REPLServer.<anonymous> (repl.js:441:10)
  at emitOne (events.js:101:20)

After spending way to much time thinking about why the second proxy even tries to call a method if none is given to it, I eventually gave up. I'd expect exampleFoo to be a Proxy object that accepts Foo methods if called.
What causes the actual behaviour here?

6
  • 1
    At first glance this kind of architecture is hard to follow. Would the bind method not do what you need? Commented Jun 3, 2016 at 8:58
  • 1
    "I figured ES6 proxies may be best suited for that" - no, absolutely not. Use a simple class which closes over the apiClient. Please start over. Commented Jun 3, 2016 at 9:00
  • @Bergi JS classes add no functionality. Since I don't want to write modified inherited functions with the ID as their first parameter for some 200+ API methods, I need a way of dynamically calling them. As far as I know, Proxies are the only way to intercept function calls and modify the argument list while keeping dynamic property names. Commented Jun 3, 2016 at 9:28
  • @trincot Tried that too but then I'm unable to bind to the current instance of Foo (which holds API credentials and configuration) Commented Jun 3, 2016 at 9:31
  • @MoFriedrich: If you've got 200+ API methods that take the ID as their first parameter, you can easily enumerate the original class to dynamically create the new methods (also the API is bloated). You don't need and should not use proxies here. Commented Jun 3, 2016 at 9:37

2 Answers 2

3

I don't think you should use proxies here at all. Assuming you have that API with a monstrous

class Foo {
    …
    myApiMethod(id, …) { … }
    … // and so on
}

then the cleanest way to achieve what you are looking for is

const cache = new WeakMap();
Foo.prototype.register = function(id) {
    if (!cache.has(this))
        cache.set(this, new Map());
    const thisCache = cache.get(this);
    if (!thisCache.get(id))
        thisCache.set(id, new IdentifiedFoo(this, id));
    return thisCache.get(id);
};

class IdentifiedFoo {
    constructor(foo, id) {
        this.foo = foo;
        this.id = id;
    }
}
Object.getOwnPropertyNames(Foo.prototype).forEach(function(m) {
    if (typeof Foo.prototype[m] != "function" || m == "register") // etc
        return;
    IdentifiedFoo.prototype[m] = function(...args) {
        return this.foo[m](this.id, ...args);
    };
});

so that you can do

var foo = new Foo();
foo.myApiMethod(id, …);
foo.register(id).myApiMethod(…);
Sign up to request clarification or add additional context in comments.

1 Comment

This achieves exactly what I aimed for, and even more... Thank you!
1

First of all, I am not sure the Proxy pattern is the most efficient and clean way to deal with your issue, but it should of course be possible to do.

The first issue I see is that your actual test tries to call registerFoo on the Foo prototype (class) itself, while you have only defined it for instance(s) of Foo. So you would have to first create an instance, like this:

var foo = new Foo();
var exampleFoo = foo.registerFoo('exampleFoo', 123456);

And then to complete the test, you would have to call a method, which should exist. So for testing it, I would add to Foo something like this:

  // Define an example method on a Foo instance:
  this.myMethod = function (barName /* [, arguments] */) {
    var args = Array.prototype.slice.call(arguments);
    return 'You called myMethod(' + args + ') on a Foo object';
  }

Although not a problem, I think it is unnecessary to apply Object.create on new Proxy(...), as the latter already creates an object, and I don't see a benefit in using that as a prototype instead of using it as your object directly.

So with these minor adjustments, I get to this code which seems to produce the correct result in a browser (using FireFox here):

var Foo = function() {
  // parent instance
  _self = this;
  // example method
  this.myMethod = function (barName /* [, arguments] */) {
    var args = Array.prototype.slice.call(arguments);
    return 'You called myMethod(' + args + ') on a Foo object';
  }
  // custom elements dictionary
  _myFooItems = {};
  // to call parent methods directly on custom elements
  this.myFoos = new Proxy({}, {
      // property getter function (proxy target and called property name as params)
      get: function(target, myFooName) {
        // whether called property is a registered foo
        if (_myFooItems.hasOwnProperty(myFooName)) {
          // create another proxy to intercept method calls on previous one
          return new Proxy({}, {
              // property getter function (proxy target and called property name as params)
              get: function(target, methodName) {
                // whether parent method exists
                if (_self.hasOwnProperty(methodName)) {
                  return function(/* arguments */) {
                    // insert custom element ID into args array
                    var args = Array.prototype.slice.call(arguments);
                    args.unshift(_myFooItems[ myFooName ]);
                    // apply parent method with modified args array
                    return _self[ methodName ].apply(_self, args);
                  };
                } else {
                  // parent method does not exist
                  return function() {
                    throw new Error('The method ' + methodName + ' is not implemented.');
                  }
                }
              }
          });
        }
      }
  });

  // register a custom foo and its ID
  this.registerFoo = function(myFooName, id) {
    // whether the foo has already been registered
    if (_myFooItems.hasOwnProperty(myFooName)) {
      throw new Error('The Foo ' + myFooName + ' is already registered in this instance.');
    }
    // register the foo
    _myFooItems[ myFooName ] = id;
    // return the created foo for further use
    return this.myFoos[ myFooName ];
  };
};

// Test it:
var foo = new Foo();
var exampleFoo = foo.registerFoo('exampleFoo', 123456);
var result = exampleFoo.myMethod(13);
console.log(result);

2 Comments

Well, addressing the first two issues - I dumbed my example down a bit, maybe too much... I just tried out your code in Chrome, and indeed it works, but not in NodeJS. Turns out, once the inspect method is created as a property of Foo, I get no more exceptions. I guess this is some bug in NodeJS' Proxy implementation.
OK, interesting "bug" ;-) Anyway, @Bergie has given the better solution to the original challenge.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.